Overview

benefits

  • break down the UI codebase into individual, manageable files.
  • describe UI with HTML-like markup within TypeScript files (JSX/TSX).
  • import and consume third-party components.
  • entrust React to manage the DOM and keep it synced with data and state.

JSX

overview

JSX is a variant of JS that allows inline HTML-like snippets:

const hiText = <div>Hi</div>

We can interweave HTML and JS:

const hiText = <div>Hi {nickname}</div>

start an inline HTML-like snippet

HTML-like syntax starts at the first HTML element and ends on the matching closing one.

const hiText = <div>Hi</div>

embed JS

Within the snippet, we embed some JS within curly braces.

const hiText = <div>Hi {nickname}</div>
const hiText = <div style={styleObject}>Hi</div>

transpile

The transpiler transforms HTML-like snippets into JS code: HTML elements are transformed to function calls:

const hi = <div>Hi</div>
const hi = _jsx("div", { children: "Hi" })

.tsx

TSX is the equivalent of JSX for TypeScript. Files have the tsx extension.

React components

A React component describes a piece of UI, and defines it through a tree of markup.

  • It can receive data from its parent and make use of it (props)
  • It can declare its own state, scoped to itself. Scoped state improves encapsulation.
  • It can import and use other components.
// describe a piece of UI
function Home() {
    return <div>Hi</div>
}
// encapsulate some state
function Counter() {
    // state
    const [count, setCount] = React.useState<number>(0)
    function onClick() {
        setCount(count + 1)
    }
    // UI
    return (
        <div>
            <button onClick={onClick}>add 1</button>
            <div>{count}</div>
        </div>
    )
}
// use other components
import { Header, Footer } from "./components"
function Home() {
    return (
        <>
            <Header />
            // ...
            <Footer />
        </>
    )
}

Props

React components, as functions, work with a single parameter, a plain object, that we call props by convention.

function Home(props: T) {}

The props object contains the props passed by the parent. Its type reflects what is expected from the parent:

type T = { x: number; y: number }
//
function Home(props: T) {
    //   const {x, y} = props
}

At call site, we pass the expected props:

<Home x={3} y={6} />

Note: we frequently inline destructure props:

function Home({ x, y }: T) {
    //
}

parameter-less components

parameter-less components produce the same markup unconditionally.

function Home() {
    //
}

children

We can nest elements under the component:

<Container>
    <p>Hello</p>
</Container>

React passes such elements to the component as a prop, the children prop, whose type is ReactNode.

If the component is to use it, it declares a children prop:

function Container(props: { children: ReactNode }) {
    return <div>{props.children}</div>
}

Note: we can technically nest elements by passing them to the children prop, but it is much less readable:

<Container children={<p>Hello</p>} />

conceptual

props as dependencies

Props are dependencies. When the parent calls the component with different props, its content changes.

conceptual: a contract

We set up a contract between the component and its caller: The component declares a list of props it expects to receive. It is then up to the caller to provide them.

useState

We request React to:

  • create and store a state variable
  • give us a view on it (immutable view)
  • give us a way to request change, though a dispatch function. It is up to React to acknowledge the request and update the document when it deems appropriate.

initial value and type

useState<number>(0)

receive the view and a dispatch function

const [count, setCount] = React.useState<number>(0)

call the dispatch function with a raw value

In its simplest form, the dispatch function expects a raw value:

setCount(1)

Note: if we use state we have in scope, it still resolves to a raw value. When calling it multiple times, we set the state to the same value (no effect)

setCount(count + 1)
// resolves to:
setCount(1)

queue state mutation

If we want to derive a value from the current state, we either:

  • refer to the state value managed by React. We pass a callback, to which React passes the state value as it keeps track of.
  • This pattern supports queuing state changes that resolve on state update. React correctly accounts for the queued mutations:
// note: count will increase by 2
setCount((count) => count + 1)
setCount((count) => count + 1)

useRef

controlled vs uncontrolled element

In principle, we follow the pattern where React manages the state and update the DOM elements on its own. This pattern works with controlled elements, that is, elements on which we plug the value prop that React sees and registers. The pattern is to plug state to the value prop and to request state change in the onChange prop.

Otherwise, DOM elements evolve independently (uncontrolled element): their value doesn't derive from the state, and evolve outside of the React realm, and they do not require nor trigger React re-renders on change. We can set the initial value with defaultValue.

In scenarios where we want to imperatively read or manage an uncontrolled element, we must get a stable reference to it, so that we can read or set its value at arbitrary times using regular DOM APIs. The combination of useRef and ref allows requesting a reference to such an uncontrolled element.

Such reference persists across renders and is only discarded when the component unmounts, which discards the DOM element.

get a reference to an uncontrolled DOM element

In this example, we request a reference to the <input> DOM element, so that keystrokes do not trigger a React re-render.

const myInput = useRef<HTMLInputElement>(null)
// ..
<input ref={myInput}/>

We indicate the element's DOM type as a type parameter. The type can be as broad as HTMLElement, but in this example it is narrowed down to the more accurate HTMLInputElement.

We don't need to indicate null as a type parameter. React already adds it on our behalf.

use the reference

the current property starts as null because there is no way to get a valid DOM reference before the element is mounted (the component has not be painted, the DOM node doesn't exist yet). Later, React assigns the valid DOM reference to current.

myInput.current // the DOM element

// use DOM APIs (not React specific)
if (!myInput.current) return
myInput.current.innerText
myInput.current.innerHTML
myInput.current.value

other uses

We can use useRef to hold any kind of value that persists across renders, and that is only discarded on unmount. For example, we store a counter that increments when the component renders. Because it is not part of the React's state, its incrementation does not trigger a re-render.

useEffect

An effect is a function tied to a specific component which runs on specific events or conditions.

non-blocking

The effect never delays the component being painted on-screen: neither the first paint nor the subsequent ones. Instead, effects run after the first paint and, if eligible, run after subsequent re-renders. This pattern aims to prioritize time-to-paint and time-to-render.

If the effect fetches data to be displayed on-screen, we have to deal with a first paint which doesn't have such data. We either use a default value, or hide the UI altogether and display a loader, a skeleton or nothing.

one or multiple runs

An effect runs at least once, after the first paint. The subsequent runs are conditionals. We can ask for the effect to run:

  • after every render
  • after renders where one or several variables changed in value.

dependencies

useEffect(effect, []) // once
useEffect(effect, [x]) // once, then when x changes
useEffect(effect) // on every render

clean-up on unmount

The component can unmount at any time. We perform clean-up in certain conditions:

  • we have set-up a subscription and we want to cancel it.
  • we have initiated a network fetch, and we want to disable the callback. We do that by setting a flag such as isStale to false, and by ensuring the callback doesn't mutate state when such flag is false.

clean-up on dependency change

The dependency change also triggers the clean-up function. That is why network fetches should also cancel when the effect changes in nature because of the dependency change, so we can use the term isStale instead of isMounted.

synopsis

useEffect(f, [])

synchronous function

Even though the effect can start asynchronous tasks, it must itself come as a non-async function, that is, it must run and return immediately. As such, we may not use await directly in its body, but we may define an async function which uses await in its body and call that function instead.

function myEffect() {
    /* effect content */
    return /* clean up content */
}

Context and Providers

A provider exposes a value to its descendant components, without the need to pass props:

<FooContext.Provider value={3}>
    <X>
        <X1 />
    </X>
    <Y />
    <Z />
</FooContext.Provider>

Descendants read the value by referring to the context (through the use of useContext):

// X1.tsx
const value = useContext(FooContext) // 3

We define the context object beforehand. Note: the initial value is overridden by the provider.

const FooContext = createContext<number>(0)

optional pattern: helper hook

We make a helper function that reads the appropriate context. The consumer doesn't have to import the Context object anymore:

// FooContext.tsx
export function useFoo() {
    return useContext(FooContext)
}
// X1.tsx
const value = useFoo()

optional pattern: custom provider

We make a custom component that initializes the .Provider component with the correct value, encapsulating away the management of the value prop. The component also hosts the state if applicable. It can then expose the state and the dispatch function, if applicable.

In this example, we expose the state and the dispatch function. We start the context with undefined. In the custom hook, we check against it.

Note: Instead of inlining value={{ foo, setFoo }} in the value prop, which is an object that is created on every renders, and thus signaling the value has changed, we instead memoize an object and use it instead. This is only useful when the value is an object that is created inside the component. It isn't needed when we use a stable reference to the state, such as when doing value={ foo }. It is also theoretically not needed when the provider itself doesn't re-render, which occurs when it's stable at the root of the tree. Having parent providers that change doesn't mean that the nested child provider re-render. We can still use it for extra-safety.

// FooContext.tsx
type T = {
    foo: number
    setFoo: Dispatch<SetStateAction<number>>
}
// 1. context
const FooContext = createContext<T | undefined>(undefined)

// 2. custom provider
function FooProvider({ children }: { children: ReactNode }) {
    const [foo, setFoo] = useState<number>(0)
    const value = useMemo(() => ({ foo, setFoo }), [foo])
    return <FooContext.Provider value={value}>{children}</FooContext.Provider>
}
// 3. custom reader (helper hook)
export function useFoo() {
    const value = useContext(FooContext)
    if (!value) throw new Error("useFoo must be used within the Provider")
    return value
}

We can then use the component instead of the .Provider. The consumer doesn't have to import the Context object anymore:

// App.tsx
<FooProvider>
    <X />
    <Y />
</FooProvider>
// X.tsx
const { foo, setFoo } = useFoo()

Global props

React changes the behavior of some global props:

the style prop

<div
    style={{
        border: "2px solid black",
        borderRadius: "5px",
        width: "100px",
        height: "200px",
        color: "black",
        display: "inline-block",
        backgroundColor: "beige",
    }}
>
    {/* ... */}
</div>

onX props

earlymorning logo

Overview

benefits

  • break down the UI codebase into individual, manageable files.
  • describe UI with HTML-like markup within TypeScript files (JSX/TSX).
  • import and consume third-party components.
  • entrust React to manage the DOM and keep it synced with data and state.

JSX

overview

JSX is a variant of JS that allows inline HTML-like snippets:

const hiText = <div>Hi</div>

We can interweave HTML and JS:

const hiText = <div>Hi {nickname}</div>

start an inline HTML-like snippet

HTML-like syntax starts at the first HTML element and ends on the matching closing one.

const hiText = <div>Hi</div>

embed JS

Within the snippet, we embed some JS within curly braces.

const hiText = <div>Hi {nickname}</div>
const hiText = <div style={styleObject}>Hi</div>

transpile

The transpiler transforms HTML-like snippets into JS code: HTML elements are transformed to function calls:

const hi = <div>Hi</div>
const hi = _jsx("div", { children: "Hi" })

.tsx

TSX is the equivalent of JSX for TypeScript. Files have the tsx extension.

React components

A React component describes a piece of UI, and defines it through a tree of markup.

  • It can receive data from its parent and make use of it (props)
  • It can declare its own state, scoped to itself. Scoped state improves encapsulation.
  • It can import and use other components.
// describe a piece of UI
function Home() {
    return <div>Hi</div>
}
// encapsulate some state
function Counter() {
    // state
    const [count, setCount] = React.useState<number>(0)
    function onClick() {
        setCount(count + 1)
    }
    // UI
    return (
        <div>
            <button onClick={onClick}>add 1</button>
            <div>{count}</div>
        </div>
    )
}
// use other components
import { Header, Footer } from "./components"
function Home() {
    return (
        <>
            <Header />
            // ...
            <Footer />
        </>
    )
}

Props

React components, as functions, work with a single parameter, a plain object, that we call props by convention.

function Home(props: T) {}

The props object contains the props passed by the parent. Its type reflects what is expected from the parent:

type T = { x: number; y: number }
//
function Home(props: T) {
    //   const {x, y} = props
}

At call site, we pass the expected props:

<Home x={3} y={6} />

Note: we frequently inline destructure props:

function Home({ x, y }: T) {
    //
}

parameter-less components

parameter-less components produce the same markup unconditionally.

function Home() {
    //
}

children

We can nest elements under the component:

<Container>
    <p>Hello</p>
</Container>

React passes such elements to the component as a prop, the children prop, whose type is ReactNode.

If the component is to use it, it declares a children prop:

function Container(props: { children: ReactNode }) {
    return <div>{props.children}</div>
}

Note: we can technically nest elements by passing them to the children prop, but it is much less readable:

<Container children={<p>Hello</p>} />

conceptual

props as dependencies

Props are dependencies. When the parent calls the component with different props, its content changes.

conceptual: a contract

We set up a contract between the component and its caller: The component declares a list of props it expects to receive. It is then up to the caller to provide them.

useState

We request React to:

  • create and store a state variable
  • give us a view on it (immutable view)
  • give us a way to request change, though a dispatch function. It is up to React to acknowledge the request and update the document when it deems appropriate.

initial value and type

useState<number>(0)

receive the view and a dispatch function

const [count, setCount] = React.useState<number>(0)

call the dispatch function with a raw value

In its simplest form, the dispatch function expects a raw value:

setCount(1)

Note: if we use state we have in scope, it still resolves to a raw value. When calling it multiple times, we set the state to the same value (no effect)

setCount(count + 1)
// resolves to:
setCount(1)

queue state mutation

If we want to derive a value from the current state, we either:

  • refer to the state value managed by React. We pass a callback, to which React passes the state value as it keeps track of.
  • This pattern supports queuing state changes that resolve on state update. React correctly accounts for the queued mutations:
// note: count will increase by 2
setCount((count) => count + 1)
setCount((count) => count + 1)

useRef

controlled vs uncontrolled element

In principle, we follow the pattern where React manages the state and update the DOM elements on its own. This pattern works with controlled elements, that is, elements on which we plug the value prop that React sees and registers. The pattern is to plug state to the value prop and to request state change in the onChange prop.

Otherwise, DOM elements evolve independently (uncontrolled element): their value doesn't derive from the state, and evolve outside of the React realm, and they do not require nor trigger React re-renders on change. We can set the initial value with defaultValue.

In scenarios where we want to imperatively read or manage an uncontrolled element, we must get a stable reference to it, so that we can read or set its value at arbitrary times using regular DOM APIs. The combination of useRef and ref allows requesting a reference to such an uncontrolled element.

Such reference persists across renders and is only discarded when the component unmounts, which discards the DOM element.

get a reference to an uncontrolled DOM element

In this example, we request a reference to the <input> DOM element, so that keystrokes do not trigger a React re-render.

const myInput = useRef<HTMLInputElement>(null)
// ..
<input ref={myInput}/>

We indicate the element's DOM type as a type parameter. The type can be as broad as HTMLElement, but in this example it is narrowed down to the more accurate HTMLInputElement.

We don't need to indicate null as a type parameter. React already adds it on our behalf.

use the reference

the current property starts as null because there is no way to get a valid DOM reference before the element is mounted (the component has not be painted, the DOM node doesn't exist yet). Later, React assigns the valid DOM reference to current.

myInput.current // the DOM element

// use DOM APIs (not React specific)
if (!myInput.current) return
myInput.current.innerText
myInput.current.innerHTML
myInput.current.value

other uses

We can use useRef to hold any kind of value that persists across renders, and that is only discarded on unmount. For example, we store a counter that increments when the component renders. Because it is not part of the React's state, its incrementation does not trigger a re-render.

useEffect

An effect is a function tied to a specific component which runs on specific events or conditions.

non-blocking

The effect never delays the component being painted on-screen: neither the first paint nor the subsequent ones. Instead, effects run after the first paint and, if eligible, run after subsequent re-renders. This pattern aims to prioritize time-to-paint and time-to-render.

If the effect fetches data to be displayed on-screen, we have to deal with a first paint which doesn't have such data. We either use a default value, or hide the UI altogether and display a loader, a skeleton or nothing.

one or multiple runs

An effect runs at least once, after the first paint. The subsequent runs are conditionals. We can ask for the effect to run:

  • after every render
  • after renders where one or several variables changed in value.

dependencies

useEffect(effect, []) // once
useEffect(effect, [x]) // once, then when x changes
useEffect(effect) // on every render

clean-up on unmount

The component can unmount at any time. We perform clean-up in certain conditions:

  • we have set-up a subscription and we want to cancel it.
  • we have initiated a network fetch, and we want to disable the callback. We do that by setting a flag such as isStale to false, and by ensuring the callback doesn't mutate state when such flag is false.

clean-up on dependency change

The dependency change also triggers the clean-up function. That is why network fetches should also cancel when the effect changes in nature because of the dependency change, so we can use the term isStale instead of isMounted.

synopsis

useEffect(f, [])

synchronous function

Even though the effect can start asynchronous tasks, it must itself come as a non-async function, that is, it must run and return immediately. As such, we may not use await directly in its body, but we may define an async function which uses await in its body and call that function instead.

function myEffect() {
    /* effect content */
    return /* clean up content */
}

Context and Providers

A provider exposes a value to its descendant components, without the need to pass props:

<FooContext.Provider value={3}>
    <X>
        <X1 />
    </X>
    <Y />
    <Z />
</FooContext.Provider>

Descendants read the value by referring to the context (through the use of useContext):

// X1.tsx
const value = useContext(FooContext) // 3

We define the context object beforehand. Note: the initial value is overridden by the provider.

const FooContext = createContext<number>(0)

optional pattern: helper hook

We make a helper function that reads the appropriate context. The consumer doesn't have to import the Context object anymore:

// FooContext.tsx
export function useFoo() {
    return useContext(FooContext)
}
// X1.tsx
const value = useFoo()

optional pattern: custom provider

We make a custom component that initializes the .Provider component with the correct value, encapsulating away the management of the value prop. The component also hosts the state if applicable. It can then expose the state and the dispatch function, if applicable.

In this example, we expose the state and the dispatch function. We start the context with undefined. In the custom hook, we check against it.

Note: Instead of inlining value={{ foo, setFoo }} in the value prop, which is an object that is created on every renders, and thus signaling the value has changed, we instead memoize an object and use it instead. This is only useful when the value is an object that is created inside the component. It isn't needed when we use a stable reference to the state, such as when doing value={ foo }. It is also theoretically not needed when the provider itself doesn't re-render, which occurs when it's stable at the root of the tree. Having parent providers that change doesn't mean that the nested child provider re-render. We can still use it for extra-safety.

// FooContext.tsx
type T = {
    foo: number
    setFoo: Dispatch<SetStateAction<number>>
}
// 1. context
const FooContext = createContext<T | undefined>(undefined)

// 2. custom provider
function FooProvider({ children }: { children: ReactNode }) {
    const [foo, setFoo] = useState<number>(0)
    const value = useMemo(() => ({ foo, setFoo }), [foo])
    return <FooContext.Provider value={value}>{children}</FooContext.Provider>
}
// 3. custom reader (helper hook)
export function useFoo() {
    const value = useContext(FooContext)
    if (!value) throw new Error("useFoo must be used within the Provider")
    return value
}

We can then use the component instead of the .Provider. The consumer doesn't have to import the Context object anymore:

// App.tsx
<FooProvider>
    <X />
    <Y />
</FooProvider>
// X.tsx
const { foo, setFoo } = useFoo()

Global props

React changes the behavior of some global props:

the style prop

<div
    style={{
        border: "2px solid black",
        borderRadius: "5px",
        width: "100px",
        height: "200px",
        color: "black",
        display: "inline-block",
        backgroundColor: "beige",
    }}
>
    {/* ... */}
</div>

onX props