Files
Compass/docs/react.md

15 KiB

React, TypeScript & JavaScript — A Deep Dive


Part 1: JavaScript Fundamentals

Everything is an Object

JavaScript has one foundational truth: everything is an object, or behaves like one. Numbers, strings, functions, arrays, classes — they all ultimately descend from Object.

typeof {} // "object"
typeof [] // "object"
typeof function () {} // "function" (but instanceof Object === true)
typeof 42 // "number" (primitive, but auto-boxed to Number object)

Primitives (string, number, boolean, null, undefined, symbol, bigint) are the exception — they're not objects — but JavaScript automatically "boxes" them into their object wrappers when you access properties on them:

'hello'.toUpperCase() // JS temporarily wraps "hello" in a String object

Functions Are Objects

Functions in JS are first-class objects — they can be assigned to variables, passed as arguments, and have properties attached to them.

function greet() {
  return 'hello'
}

greet instanceof Function // true
greet instanceof Object // true

// Functions can have properties like any object
greet.version = '1.0'
console.log(greet.version) // "1.0"

This is why you can pass functions as callbacks, store them in arrays, and return them from other functions. It's also why React can accept both function components and class components — they're both just callable objects.


Classes Are Just Functions

class syntax in JavaScript is syntactic sugar over constructor functions and prototypes. Under the hood, they compile down to the same thing.

// Modern class syntax:
class Dog {
  constructor(name) {
    this.name = name
  }

  bark() {
    console.log('woof')
  }
}

// Is identical to this older style:
function Dog(name) {
  this.name = name
}

Dog.prototype.bark = function () {
  console.log('woof')
}

// Proof:
typeof Dog // "function" — classes ARE functions

The prototype chain is how JavaScript does inheritance — when you call dog.bark(), JS looks up the chain: instance → Dog.prototypeObject.prototype.


this — The Most Confusing Part of JS

this refers to the object that called the function, not where the function was defined. This causes bugs when passing methods as callbacks.

class Timer {
  constructor() {
    this.count = 0
  }

  tick() {
    this.count++
  } // `this` works fine here
}

const t = new Timer()
setTimeout(t.tick, 1000) // ❌ `this` is undefined — tick lost its context
setTimeout(() => t.tick(), 1000) // ✅ arrow function preserves `this`

Arrow functions don't have their own this — they inherit it from the surrounding scope. This is why they're preferred for callbacks and class methods in React.


The Event Loop

JavaScript is single-threaded — it can only do one thing at a time. The event loop is how it handles async operations without blocking:

Call Stack → runs sync code
     ↓
Web APIs → handles async (setTimeout, fetch, etc.)
     ↓
Callback Queue → waits for stack to be empty
     ↓
Event Loop → moves callbacks back to stack when ready
console.log('1')
setTimeout(() => console.log('2'), 0) // async — goes to queue
console.log('3')

// Output: 1, 3, 2
// Even with 0ms delay, setTimeout is async and waits for stack to clear

Async / Await and Promises

async/await is syntactic sugar over Promises. A Promise represents a value that will be available in the future.

// Promise style:
fetch('/api/user')
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

// Async/await style (same thing, cleaner):
async function getUser() {
  try {
    const res = await fetch('/api/user')
    const data = await res.json()
    console.log(data)
  } catch (err) {
    console.error(err)
  }
}

async functions always return a Promise, even if you return a plain value.


Arrow Functions vs Function Declarations

// Function declaration — hoisted, has own `this`
function getUser() {}

getUser() // ✅ callable before definition

// Arrow function — not hoisted, inherits `this`
const getUser = () => {}
getUser() // ❌ ReferenceError if called before declaration

For React components and utility functions the two are functionally equivalent — the choice is mostly style. Arrow functions are preferred for callbacks; function declarations are conventional for top-level utilities and Next.js exports.


Part 2: TypeScript

TypeScript is JavaScript with a type system layered on top. It compiles down to plain JavaScript — the browser never sees TypeScript.


What TypeScript Adds

// JavaScript — no safety
function add(a, b) {
  return a + b
}

add('hello', 42) // runs, returns "hello42" — probably a bug

// TypeScript — caught at compile time
function add(a: number, b: number): number {
  return a + b
}

add('hello', 42) // ❌ Error: Argument of type 'string' is not assignable to 'number'

TypeScript catches bugs before runtime, makes refactoring safer, and provides autocomplete in editors.


interface vs type

Both describe the shape of an object, but they have key differences:

// interface — extendable, mergeable, best for object shapes
interface User {
  name: string
  age: number
}

interface Admin extends User {
  role: string
}

// Declaration merging — only interfaces can do this
interface User {
  email: string
}

// User now has: name, age, email ✅

// type — more flexible, supports unions, tuples, primitives
type ID = string | number // union — impossible with interface
type Pair = [string, number] // tuple — impossible with interface
type User = {name: string} // object shape — works like interface

// type uses intersection instead of extends:
type Admin = User & {role: string}

// type CANNOT be merged:
type User = {email: string} // ❌ Duplicate identifier error

Rule of thumb: Use interface for component props and object shapes; use type for unions, tuples, or complex compositions.


Generics

Generics let you write reusable code that works with multiple types while remaining type-safe:

// Without generics — loses type info
function first(arr: any[]): any {
  return arr[0]
}

// With generics — type is preserved
function first<T>(arr: T[]): T {
  return arr[0]
}

const num = first([1, 2, 3]) // TypeScript knows: num is number
const str = first(['a', 'b']) // TypeScript knows: str is string

Utility Types

TypeScript ships with built-in type transformers:

interface User {
  name: string
  age: number
  email: string
}

Partial<User> // all fields optional
Required<User> // all fields required
Pick<User, 'name'> // only { name: string }
Omit<User, 'email'> // everything except email
Readonly<User> // all fields immutable
Record<string, User> // { [key: string]: User }

These are heavily used in React for prop types — e.g., Partial<ButtonProps> for a component where all props are optional.


Part 3: React

React is a UI library built on one core idea: your UI is a function of your state.

UI = f(state)

When state changes, React re-runs your component function and figures out the minimal DOM updates needed.


What React Actually Does

React never touches the real DOM directly. It maintains a Virtual DOM — a lightweight JavaScript object tree representing the UI. When state changes:

  1. React re-renders the component (calls the function again)
  2. Compares new Virtual DOM to previous (called diffing or reconciliation)
  3. Applies only the minimal changes to the real DOM (patching)

This is fast because DOM operations are expensive; JS object comparisons are cheap.


JSX

JSX looks like HTML in JavaScript but it's neither — it's syntactic sugar for React.createElement() calls:

// What you write:
const el = <div className="box">Hello</div>

// What it compiles to:
const el = React.createElement("div", {className: "box"}, "Hello")

// Which produces a plain JS object:
{
    type: "div",
        props
:
    {
        className: "box",
            children
    :
        "Hello"
    }
}

React reads these objects to build and update the DOM.


Components — The Building Block

A component is any function that returns JSX. React identifies components by their capital letter.

// Function component (modern standard)
function Button({label, onClick}: {label: string; onClick: () => void}) {
  return <button onClick={onClick}>{label}</button>
}

// Class component (legacy)
class Button extends React.Component<{label: string}> {
  render() {
    return <button>{this.props.label}</button>
  }
}

Why both work: React calls Component(props) for function components and new Component(props).render() for class components. Both return JSX objects, which is all React needs. Since JavaScript classes are just functions with prototypes, they're both "callable things that return JSX" at the engine level.


The Component Lifecycle

Every component goes through: mount → update → unmount

Mount    → component appears in the DOM
Update   → state or props change, component re-renders
Unmount  → component is removed from the DOM

In function components, useEffect handles all lifecycle stages:

useEffect(() => {
  // Runs after mount (and after updates if deps change)
  const subscription = subscribe()

  return () => {
    // Cleanup — runs before unmount (or before next effect)
    subscription.unsubscribe()
  }
}, [dependency]) // runs again whenever `dependency` changes
// [] = run once on mount only
// no array = run after every render

Props vs State

// Props — data passed IN from a parent. Read-only.
function Greeting({name}: {name: string}) {
  return <h1>Hello, {name}</h1>
}

// State — data owned BY the component. Triggers re-render when changed.
function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

The key distinction: props flow down (parent → child), state lives inside the component.


Hooks

Hooks are a special category of function — not a component, not a plain method. They let function components access React features like state and lifecycle. They have strict rules: only call them at the top level of a component (not inside conditions or loops), and only inside React functions.

useState() // local component state
useEffect() // side effects & lifecycle
useRef() // reference a DOM element or persist a value without re-render
useContext() // consume a React Context
useMemo() // memoize an expensive calculation
useCallback() // memoize a function reference
useReducer() // complex state logic (like a mini Redux)

Custom hooks let you extract and reuse stateful logic:

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)
  useEffect(() => {
    const handler = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])
  return width
}

// Reusable in any component:
function Layout() {
  const width = useWindowWidth()
  return <div>{width > 768 ? <Sidebar /> : null}</div>
}

Context — Avoiding Prop Drilling

Passing props through many layers of components is called prop drilling. Context solves it by broadcasting data to any component in the tree.

// Create context
const ThemeContext = createContext<'light' | 'dark'>('light')

// Provide it high in the tree
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  )
}

// Consume it anywhere below — no props needed
function Button() {
  const theme = useContext(ThemeContext)
  return <button className={theme}>Click</button>
}

React's Different "Types" of Things

Type Returns Naming Purpose
Component JSX Capitalized UI building block
Hook Data / functions use prefix Stateful logic
Event Handler void / anything handle prefix Responds to events
Utility Function Anything Anything Pure logic, no React rules
HOC A component with prefix Wraps and enhances components
Custom Hook Data / functions use prefix Reusable stateful logic
Context Cross-tree data sharing
Ref Mutable object DOM access / stable values
Portal Render outside DOM tree

A Complete Component in Context

// Types
interface ProfileProps {
  profile: Profile
  invertedMbtiChoice: string // ✅ a prop, not a second parameter
}

// Component
export function ProfilePersonality({profile, invertedMbtiChoice}: ProfileProps) {
  // Hook — stateful logic
  const [isExpanded, setIsExpanded] = useState(false)

  // Derived value — plain calculation, not a hook
  const mbtiType = profile.mbti ? invertedMbtiChoice : null

  // Effect — side effect (lifecycle)
  useEffect(() => {
    document.title = `Profile: ${profile.name}`
    return () => {
      document.title = 'App'
    }
  }, [profile.name])

  // Event handler
  const handleToggle = () => setIsExpanded((prev) => !prev)

  // JSX — the UI
  return (
    <div>
      {mbtiType && <span>{mbtiType}</span>}
      <button onClick={handleToggle}>{isExpanded ? 'Show less' : 'Show more'}</button>
    </div>
  )
}

The Big Picture

JavaScript
├── Everything is an object (or behaves like one)
├── Classes are functions with prototype chains
├── Functions are first-class objects
└── Async model: single-threaded + event loop

      ↓ adds

TypeScript
├── Static type checking at compile time
├── interfaces & types for describing shapes
├── Generics for reusable type-safe code
└── Compiles away — browser sees plain JS

      ↓ builds

React
├── UI = f(state) — components are functions of their data
├── Virtual DOM + diffing = efficient real DOM updates
├── JSX compiles to React.createElement() calls
├── Hooks give functions lifecycle + state (no class needed)
└── Props flow down, state lives inside, context broadcasts

The layering is intentional: TypeScript makes JavaScript safer; React gives JavaScript a model for building reactive UIs. Understanding JS fundamentals (objects, functions, this, async) makes React's behaviour — re-renders, closures in hooks, stale state bugs — much more predictable.