react
React Basics
When to use state
The most important mental model in React to keep in mind is that the view (UI) is a function of the state of a component:
This means that you should declare variables as states using the useState() hook if the value of the variable is used to render the UI. Else, it should not be state.
React rendering pipeline
The react rendering pipeline consists of three components:
- component code: The code that defines the state and JSX that a component has
- snapshot: Before any state update takes place that would trigger a re-render, React creates a snapshot of your component which captures everything React needs to update the view at that particular moment in time. props, state, event handlers, and a description of the UI (based on those props and state) are all captured in this snapshot.
- view: Based on the snapshot, React takes that description of the UI and uses it to update the View eventually, doing it after batching state calls.
React rerendering
React will only re-render when the state of a component changes. This unintiuitively, however, leads to multiple caveat of React rerendering:
- entire component tree re-renders: If a component has state changes requiring a re-render, then the entire subtree of that component (all subcomponents and JSX) will also re-render, regardless if those sub-components have state changes or not.
When some event handler or effect changes state, React does the re-render pipeline as so:
- React takes a snapshot of the current values of the props and state inside the component and freezes those for the evaluation of what happens during the event handler.
- After all state updates in the triggered event-handlers or effects are run, then React takes a new snapshot of all current prop values and state values and compares it to the previous snapshot, and if anything is different (using value equality), then React re-renders. Else it doesn't.
event handlers and effects
State changes can only occur in the context of event handlers or effects, so React treats them specially by taking a snapshot before an event handler or effect that has a state change gets invoked:
NOTE
When an event handler is invoked, that event handler has access to the props and state as they were in the moment in time when the snapshot was created.
Another way you can think about this is whenever an event occurs, regardless of when or how it was triggered, that event will have the same props and state as the snapshot that was created with the render that event is associated with.
if an event handler or effect contains an invocation of useState's updater function and React sees that the new state is different than the state in the snapshot, React will trigger a re-render of the component – creating a new snapshot and updating the view.
Take a look at this example:
import * as React from "react"
export default function VibeCheck () {
const [status, setStatus] = React.useState("clean")
const handleClick = () => {
setStatus("dirty")
alert(status)
}
return (
<button onClick={handleClick}>
{status}
</button>
)
}
What gets alerted is the string "clean" due to this line of reasoning:
- Event handlers that contain state updates use variables in the snapshot captured by react right before the invocation of the event handler.
- In the snapshot, the
statusstate has a value of"clean", thus even though we do the state update withsetStatus("dirty"), the state update doesn't happen until all event handler and effects scheduled to run have finished. Thus what gets alerted is"clean"
Even if an event handler has a state update, the component will only re-render if the state update changes the state to a value that is unequal (value checking) to the snapshot state value.
In the below example, even though the handleClick() event handler has a state update, because the updated state doesn't differ between snapshots, the component does not re-render.
import * as React from "react"
export default function Counter () {
console.count("Rendering Counter")
const [count, setCount] = React.useState(0)
const handleClick = () => {
console.count("click")
setCount(count)
}
return (
<button onClick={handleClick}>
🤨
</button>
)
}
IMPORTANT
Again, React will only re-render if the event handler contains an invocation of useState's updater function (✅) and React sees that the new state is different than the state in the snapshot (❌).
The new snapshot is taken AFTER all state update functions in the invoked event handlers or effects have run. This prevents unnecessary re-renders after each state change, where React lazily takes the snapshot after all state-update functions have run, and uses only the last snapshot to update the UI.
This leads into an algorithm called batching:
batching
NOTE
Basically, after a bunch of setState() calls, there is only one re-render with all new updated state values after all effects and event handler executions have completed.
So in an event handler or effect, if multiple setState() calls for a specific state are invoked, then only the last set state call will be set as the updated state. This is called batching.
React will only re-render once per event handler invocation or effect change, after all state updates are completed.
Whenever React encounters multiple invocations of the same updater function (e.g. setCount in our example), it will keep track of each of them, but only the result of the last invocation will be used as the new state.
const handleClick = () => {
// queued snapshot: count = 1
setCount(1)
// queued snapshot: count = 2
setCount(2)
// queued snapshot: count = 3
setCount(3)
// final snapshot: count = 3
}
So in this example, the new state will of course be 3.
overriding batching
there is a way to tell React to use the value of the previous invocation of the updater function instead of replacing it. To do that, you pass the updater function a function itself that will take in the value from the most recent invocation as its argument.
const handleClick = () => {
// queued snapshot: count = 1
setCount(1)
// queued snapshot: count = 2
setCount(2)
// queued snapshot: count = count + 3 -> count = 5
setCount((c) => c + 3)
// final snapshot: count = 5
}
Memoization
If a component has state changes requiring a re-render, then the entire subtree of that component (all subcomponents and JSX) will also re-render, regardless if those sub-components have state changes or not.
A React performance enhancement to mitigate this excessive re-rendering of child components is called memoization, where we memoize components by wrapping them in the memo() function from React.
Any memoized child component will only re-render during a parent's re-render cycle if the props passed to that child component change (or state in the child component changes), where props are compared via primitive value equality.
Strict Mode
React strict mode is used for development use cases where by wrapping your entire app in the <React.StrictMode> component, you enable strict mode, where every component will re-render at least twice regardless of what memoization tactics you use.
TIP
This is helpful in unveiling common bugs.
In production, strict mode is disabled.
Referential equality
- primitive values: their in-memory values are the variable value themselves, liek a string, boolean, int, etc.
- reference values: their in-memory values are their memory addresses, not the values.
This means value equality checking is intuitive and works well for primitive values, but only works for reference values when two variables share the exact same reference.
React uses primitive values to check if props change, state changes, and if dependencies have changed, so it's important to cache object references to avoid constant re-renders by constantly creating new objects (which have different memory addresses).
useCallback(func, deps): returns a cached version of the function which is only recreated on re-renders where one of the dependencies in its dependency array changes.useMemo(func, deps): invokes the function and gets what it returns, and it value is only recreated on re-renders where one of the dependencies in its dependency array changes.
This is the correct way to prevent re-renders of memoized components but have object props:
import * as React from "react"
import Wave from "./Wave"
function Greeting ({ name }) {
const [index, setIndex] = React.useState(0)
const [waveIndex, setWaveIndex] = React.useState(0)
const greetings = ['Hello', "Hola", "Bonjour"]
const handleClick = () => {
const nextIndex = index === greetings.length - 1
? 0
: index + 1
setIndex(nextIndex)
}
const handleWaveClick = React.useCallback(() => {
setWaveIndex((i) => {
return i === 5 ? 0 : i + 1
})
}, [])
const options = React.useMemo(() => {
return {
animate: true,
tone: waveIndex
}
}, [waveIndex])
return (
<main>
<h1>{greetings[index]}, {name}</h1>
<button onClick={handleClick}>
Next Greeting
</button>
<Wave onClick={handleWaveClick} options={options} />
</main>
)
}
export default function App () {
return <Greeting name="Tyler" />
}
Effects in React
Rules of side effects in components
- components must be pure: To keep the component function pure, no side effects can be executed within the main body of the component. When a component renders, it should do so without running into any side effects
- side effects triggered by an event should be put in that event handler
- If a side effect is synchronizing your component with some external system, put that side effect in a
useEffect()hook.
NOTE
Basically, the main rule of having side effects in components is that they should be encapsulated into code that doesn't deal with render cycle, like in useEffect blocks and event handlers.
No, this state hack doesn't work
Take a look at this example below. You might think that it's fine to use a side-effect like fetching from local storage if it's for initializing state, but it's still a side effect and thus will rerun on every re-render of the component, not behaving as planned.
- what you think should happen: the
indexstate gets initialized with the locally stored value - what actually happens: on every re-render, the side effect runs again, thus the state gets reinitialized every time with the locally stored value, even if that's not what you want.
import * as React from "react"
function Greeting ({ name }) {
const [index, setIndex] = React.useState(
Number(localStorage.getItem("index"))
)
const greetings = ['Hello', "Hola", "Bonjour"]
const handleClick = () => {
const nextIndex = index === greetings.length - 1
? 0
: index + 1
setIndex(nextIndex)
localStorage.setItem("index", nextIndex)
}
return (
<main>
<h1>{greetings[index]}, {name}</h1>
<button onClick={handleClick}>Next Greeting</button>
</main>
)
}
What's the solution for initializing state with local storage? Put that side effect in a useEffect() with an empty dependency array so it only runs once.
useEffect
The useEffect() hook can contain side-effects and state update invocations, all of which run after every render or re-render.
NOTE
when to use useEffect()
If a side effect is synchronizing your component with some external system, put that side effect inside useEffect. Here are some use cases:
- Syncing with local storage
- Using
fetch()API or fetching external data - Querying DOM data
However, to prevent unnecessary re-renders, we make use of the dependency array to let React know which variables the side effect code in our useEffect() invocation is dependent on, and to only run the side effect when those variables change (value equality or reference equality).
useEffect lifecycle
useEffect works by removing the side effect from React's rendering flow and delaying its execution until after the component has rendered.
Here is the simplified lifecycle.
- React re-renders
- All
useEffectblocks will run, executing any side-effects
However, if you have a dependency array, then this changes and the useEffect block will only run after a re-render if one of its dependencies have changed.
NOTE
Another way to think about it
The whole goal of useEffect is to synchronize your component with some external system. Whenever any of the dependencies that the effect needs in order to synchronize change, React, should resynchronize.
useEffect antipattern and reactive values
useEffect blocks should NOT be used for reacting to changes in values. That's what event handlers are for. Rather, you should only use useEffect blocks for synchronizing UI and state to external systems.
Reactive values are any values that change between re-renders, for example, like state.
WARNING
Having reactive values in your dependency array is a red flag that you might be in the anti-pattern of reacting to state changes, but it's fine to have reactive values in the dependency array if you are using that value to sync to an external system, like the network or local storage.
NOTE
A reactive value is any value that can change between re-renders. Props, state, or any variables defined inside of a component are all reactive values.
useEffect cleanup function
The cleanup function in a useEffect block runs in two scenarios:
- Scenario 1 (component unmounts): When the component unmounts, the cleanup function will run
- Scenario 2 (effect is scheduled to rerun): If any of the values in the dependency array changes, then the effect is scheduled to rerun, and thus the cleanup function is called before the rerun of that effect.
NOTE
If you return a function from your effect, React will call that function each time before it ever calls your effect again, and then one final time when the component is removed from the DOM.
Here is the general lifecycle of rerendering and running effects:
- React renders for first time
- If scheduled to run,
useEffectblock runs - On all subsequent re-renders: if
useEffectis scheduled to run again, then cleanup function executes first with previous snapshot, then the effect runs.
Complete Data Fetching Example
First, notice that our dependency array only includes one element, id. Again, this isn't telling React to re-run the effect when id changes (though, that's what happens). Instead, we're giving React an array of all of the dependencies our effect needs to re-synchronize with the outside system. As a byproduct of that, whenever React sees that id has changed between renders, it will re-run the effect.
import React, { useState } from 'react'
async function fetchPokemon(id) {
const abortController = new AbortController()
try {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`,
{
signal: abortController.signal
}
)
if (res.ok === true) {
return {
error: null,
response: await res.json(),
abortController
}
}
throw new Error(`Error fetching pokemon #${id}`)
} catch (e) {
return {
error: e,
response: null,
abortController
}
}
}
// export default function MyApp() {
// return <div>Hello World</div>;
// }
// Main App
export default function App() {
const [id, setId] = React.useState(1)
const [pokemon, setPokemon] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
const handleFetchPokemon = async () => {
setLoading(true)
setError(null)
const { error, response } = await fetchPokemon(id)
if (error) {
setError(error.message)
} else {
setPokemon(response)
}
setLoading(false)
}
handleFetchPokemon()
}, [id])
if (loading) {
return <p>loading...</p>
}
if (error) {
return <p>error {error}</p>
}
if (!pokemon && !loading) {
return "no pokemon"
}
return (
<main>
{JSON.stringify({ id, pokemon }, null, 2)}
</main>
)
}
However, there is still a main problem we have to deal with.
Whenever we call fetchPokemon, because it's an asynchronous request, we have no idea how long that request will take to resolve. It's completely possible that, while we're in the process of waiting for a response, our Carousel updates id, which causes a re-render, which causes our effect to run again with a different id.
In this scenario, we now have two requests in flight, both with different ids. Worse, we have no way of knowing which one will resolve first. In both scenarios, we're calling setPokemon when the request resolves. That means, because we don't know in which order they'll resolve, pokemon, and therefor our UI, will eventually be whatever request was resolved last. To make it worse, you'll also get a flash of the Pokémon that resolves first, before the second one does.
To solve this, we will use a cleanup function in the effect which will cleanup the stale fetch requests in the previous snapshot by aborting them.
Notice that the cleanup function is only called for ids that are no longer relevant. This makes sense because the cleanup function for the most recent effect won't be called until either another effect runs (making it stale) or the component has been removed from the DOM (irrelevant in this scenario).
import React, { useState } from 'react'
import Carousel from "./Carousel"
import PokemonCard from "./PokemonCard"
function fetchPokemon(id) {
const abortController = new AbortController()
const res = fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`,
{
signal: abortController.signal
}
)
return {
response: res,
abortController
}
}
// Main App
export default function App() {
const [id, setId] = React.useState(1)
const [pokemon, setPokemon] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
const handlePrevious = () => {
if (id > 1) {
setId(Math.max(id - 1, 1))
}
}
const handleNext = () => setId(id + 1)
React.useEffect(() => {
// 1. Create the controller instance *inside* the effect
const controller = new AbortController();
const handleFetchPokemon = async () => {
setLoading(true);
setError(null);
try {
// Pass the signal directly to the fetch request
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${id}`,
{ signal: controller.signal }
);
if (!res.ok) throw new Error(`Error fetching pokemon #${id}`);
const data = await res.json();
setPokemon(data);
} catch (e) {
// 3. When a fetch is aborted, it throws an 'AbortError'.
// We ignore it so we don't flash error states for stale requests.
if (e.name !== 'AbortError') {
console.error(e);
setError(e);
setPokemon(null);
}
} finally {
// Only turn off loading if this request wasn't aborted
if (!controller.signal.aborted) {
setLoading(false);
}
}
};
handleFetchPokemon();
// 2. The Cleanup Function: Runs when 'id' changes or component unmounts
return () => {
controller.abort();
};
}, [id]);
if (loading) {
return <p>loading...</p>
}
if (error) {
return <p>error</p>
}
if (!pokemon && !loading) {
return "no pokemon"
}
return (
<Carousel onPrevious={handlePrevious} onNext={handleNext}>
{JSON.stringify({
name: pokemon.name,
id
}, null, 2)}
</Carousel>
)
}
Refs in React
Sometimes you need to preserve a variable value across renders but you don't use that variable to render the UI. Making this value a state would be an antipattern, as React re-renders when state gets updated.
The useRef hook creates a value that is preserved across renders but won't trigger a re-render when it changes. Here are some use cases:
setInterval()orsetTimeout()Id: It's important to keep track of the id so you can cancel intervals or timeouts, but you never actually directly render the id in the UI, so it should NOT be state. This is the perfect use case for a Ref.- DOM element: If you want to keep track of some DOM element across re-renders, you should use refs rather than state because you never directly render this DOM element, so it should not be state, otherwise it would cause unnecessary re-renders.
- debouncing and throttling: stores debouncing timers in a ref to avoid unnecessary rerenders
ref vs state example
Here is an example where we incorrectly use state when we should be using a ref:
- main issue: The
idstate is not rendered anywhere in the view, so invokingsetIdcauses unnecessary re-renders and is thus an antipattern.
import * as React from "react"
import { formatTime } from "./utils"
export default function Stopwatch () {
const [seconds, setSeconds] = React.useState(0)
const [running, setRunning] = React.useState(false)
const [id, setId] = React.useState(null)
const handleClick = () => {
if (running === false) {
const id = window.setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
setId(id)
setRunning(true)
} else {
window.clearInterval(id)
setId(null)
setRunning(false)
}
}
return (
<main>
<h1>{formatTime(seconds)}</h1>
<button
style={{background: running === true ? 'var(--red)' : 'var(--green)'}}
onClick={handleClick}>
{running === true ? 'Stop' : 'Start'}
</button>
</main>
)
}
This is the improved version of this example, where we use a ref for the id state as to preserve the variable across re-renders but not use it for the view:
import * as React from "react"
import { formatTime } from "./utils"
export default function Stopwatch () {
const [seconds, setSeconds] = React.useState(0)
const [running, setRunning] = React.useState(false)
const ref = React.useRef(null)
const handleClick = () => {
if (running === false) {
ref.current = window.setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
setRunning(true)
} else {
window.clearInterval(ref.current)
setRunning(false)
}
}
return (
<main>
<h1>{formatTime(seconds)}</h1>
<button
style={{background: running === true ? 'var(--red)' : 'var(--green)'}}
onClick={handleClick}>
{running === true ? 'Stop' : 'Start'}
</button>
</main>
)
}
TIP
Basically use useState for a persistent variable whenever you reference it anywhere in the JSX (even if you don't directly render it). Else use useRef, if it's nowhere in the JSX return rendering portion.
ref vs effects summary
- When to use
useEffect: If you have a side effect that is triggered by an event, put it in an event handler. If you have a side effect that is synchronizing your component with some outside system, put it inside ofuseEffect. - When to use
useRef: If you need to preserve a value across renders, but that value has nothing to do with the view, and therefore, React doesn't need to re-render when it changes, put it in arefusinguseRef.
Refs with DOM elements
By putting the ref= prop on a JSX element or React component and passing a ref variable, you can control that JSX element or component via DOM APIs through the ref variable.
import * as React from "react"
import Toolbar from "./Toolbar"
export default function App() {
const white = React.useRef(null)
const black = React.useRef(null)
const yellow = React.useRef(null)
const handleClick = (type) => {
let ref = null
if (type === "white") ref = white
if (type === "yellow") ref = yellow
if (type === "black") ref = black
ref.current.scrollIntoView({
behavior: "smooth",
block: "nearest",
})
}
return (
<div>
<Toolbar handleClick={handleClick} />
<div ref={white} className="white" />
<div ref={yellow} className="yellow" />
<div ref={black} className="black" />
</div>
)
}
Here is a ref example with multiple moving parts, where the main goal is to provide a dynamic debounce timing where the user cannot switch the toggle while the element is animating.
activestate: updates the toggle switch, thus should be state since it renders to the view.noderef: attaches to the animating element so we can hook into its animations and wait for them to finish.animatingref: persistent boolean flag used to mark whether the element is currently animating or not. If the element is not currently animating (animating.current === false) then we are allowed to change the toggle.
import * as React from "react"
import Diagram from "./Diagram"
import ReactLogo from "./ReactLogo"
export default function App() {
const [active, setActive] = React.useState(false)
const node = React.useRef(null)
const animating = React.useRef(false)
React.useEffect(() => {
const checkAnimations = async () => {
// mark animating as true
animating.current = true;
// await all animations of node.current to finish
const animations = node.current.getAnimations({ subtree: true })
const promises = animations.map((animation) => animation.finished)
await Promise.all(promises)
// mark animating as false
animating.current = false;
}
checkAnimations()
})
const handleToggle = () => {
// if element is not animating, then user can change toggle state.
if (animating.current === false) {
setActive(!active)
}
}
return (
<div
ref={node}
className={`diagram ${active ? "react-approach" : "trad-approach"}`}
>
<Diagram />
<div className="toggle-diagram">
<input
id="toggle"
className="toggle-input"
type="checkbox"
checked={active}
onChange={handleToggle}
/>
<label htmlFor="toggle" className="toggle-label">
<div
className="toggle-options"
data-checked="React"
data-unchecked="Traditional"
>
<div className="toggle-switch">
<span className="toggle-marker">
<ReactLogo />
</span>
</div>
</div>
</label>
</div>
</div>
)
}
React context
When to use context
IMPORTANT
This is a huge misconception when it comes to Context. Whether what you're teleporting is a static object, function, state, or anything else – it doesn't change that fact that Context is a teleporter, not a manager of state.
Re-rendering data in context follows the common react model of , where if state in a context changes, then the entire component tree of the context provider and what ever subcomponents are nested inside it gets re-rendered.
This means it's extremely important to use context only when you don't have a lot of re-renders, otherwise you will have to constantly repaint your app. Because context providers often wrap your whole app, when the context provider must re-render due to a state change in the context, that re-renders the entire app.
To avoid a shit ton of re-renders when using context, here are some tips to keep in mind:
- pick the right tool for the job: only use react context when it makes sense to do so, like when any state in the context does not change often.
- use
useReffor persistent context values when it makes sense: If you need a persistent variable to export in the context but it's not used anywhere in your app to update the view, make it a ref instead of state as to avoid unnecessary re-renders.
useReducer
Intro
The reducer pattern is a functional programming pattern that takes a collection as input and returns a single value as output. The way you get to that single value is by invoking a reducer function for every element in the collection.
What if instead of our collection being an array, it was a collection of user actions that happened over time? Then, whenever a new user action occurred, we could invoke our reducer to get the new state of our application.

How to use useReducer
Because it's such a helpful pattern, React comes with a built-in Hook called useReducer that functionally behaves like useState, but allows you to manage your state using the reducer pattern.
The API for useReducer is similar to what we saw earlier with Array.reduce, but with one big difference. Instead of just returning the state, similar to useState, useReducer also returns a way to update that state.
What's different is instead of returning an updater function, it returns a function called dispatch that when called, will invoke the reducer function.
import * as React from "react"
function reducer(state, value) {
const nextState = state + value
console.log(
`Reducer invoked. state: ${state}, value: ${value}, nextState: ${nextState}`
)
return nextState
}
const initialState = 0
export default function Counter() {
const [count, dispatch] = React.useReducer(
reducer, initialState
)
const handleIncrement = () => {
dispatch(1) // invokes reducer with state, value=1
}
return (
<main>
<h1>{count}</h1>
<button onClick={handleIncrement}>+</button>
</main>
)
}
useReducer vs useState
Here is when to use useReducer vs useState:
- use
useStatewhen If different pieces of state update independently from one another - use
useReducerwhen if your state tends to be updated together or if updating one piece of state is based on another piece of state
When using a reducer, we decouple how the state updates from the action that triggered the update. This makes using useReducer preferable to using useState in the following scenarios:
- minimizing event listeners and interval teardown and setup: in a
useEffectwhere we want to setup event listeners or intervals and we depend on a piece of state- if we use
useState, then we're recreating and tearing down those event listeners and intervals on every state change. - If we use
useReducer, then we don't have dependecy on the state, instead we just dispatch actions, which means we can omit state from the dependency array and thus run theuseEffectblock less.
- if we use
- updating state where state updates are tightly coupled
- Form state: in forms, you may need lots of state variables, and clearing and submitting forms require an imperative way of state updates, while
useReducerupdates state in a declarative way.
use case 1: tightly coupled state updates
Here is an example where state updates rely on other pieces of state, so it's better to use useReducer rather than useState:
import * as React from "react"
import Slider from "./Slider"
function reducer(state, action) {
if (action.type === "increment") {
return {
count: state.count + state.step,
step: state.step,
}
} else if (action.type === "decrement") {
return {
count: state.count - state.step,
step: state.step,
}
} else if (action.type === "reset") {
return {
count: 0,
step: state.step,
}
} else if (action.type === "updateStep") {
return {
count: state.count,
step: action.step,
}
} else {
throw new Error("This action type isn't supported.")
}
}
export default function Counter() {
const [state, dispatch] = React.useReducer(reducer, {
count: 0,
step: 1
})
const handleIncrement = () => dispatch({type: "increment"})
const handleDecrement = () => dispatch({type: "decrement"})
const handleReset = () => dispatch({type: "reset"})
const handleUpdateStep = (step) => dispatch({type: "updateStep", step})
return (
<main>
<h1>{state.count}</h1>
<div>
<button onClick={handleDecrement}>-</button>
<button onClick={handleIncrement}>+</button>
<button onClick={handleReset}>0</button>
</div>
<Slider
min={1}
max={10}
onChange={handleUpdateStep}
/>
</main>
)
}
With that, we see another subtle but powerful benefit of useReducer you might have missed. Because the reducer function is passed the current state as the first argument, it's simple to update one piece of state based on another piece of state. In our example, we can see this in how we're updating count based on the value of step.
TIP
In fact, I'd go as far as to say whenever updating one piece of state depends on the value of another piece of state, reach for useReducer.
use case 2: imperative vs declarative state updates
The reason useReducer can be more declarative is because it allows us to map actions to state transitions. This means, instead of having a collection of setX invocations, we can simply dispatch the action type that occurred. Then our reducer can encapsulate the imperative, instructional like code.
const handleSubmit = async (e) => {
// e.preventDefault()
// setSubmitting(true)
// setError(null)
// setSuccess(false)
// try {
// await subscribe({ name, email, marketing })
// setSubmitting(false)
// setName("")
// setEmail("")
// setMarketing(true)
// setSuccess(true)
// } catch (e) {
// setSubmitting(false)
// setSuccess(false)
// setError(e.message)
// }
e.preventDefault()
dispatch({ type: "submit" })
try {
await subscribe({ name, email, marketing })
dispatch({ type: "success" })
} catch (e) {
dispatch({ type: "error", error: e.message })
}
}
Notice that we're describing what we want to do - submit. Then, based on that result, success or error. That's a lot cleaner and easier to reason about than our imperative solution.
import * as React from "react";
const initialFormData = {
name: "",
email: "",
address: "",
city: "",
zipcode: ""
};
const initialState = {
currentStep: 1,
formData: initialFormData
}
function reducer(state, action) {
if (action.type === "next") {
return {
...state,
currentStep: state.currentStep + 1
}
}
else if (action.type === "previous") {
return {
...state,
currentStep: state.currentStep - 1
}
}
else if (action.type === "change") {
if (!action.payload) throw new Error("empty payload for 'change' action")
const {name, value} = action.payload
return {
formData: {
...state.formData,
[name]: value
},
currentStep: state.currentStep
}
}
else if (action.type === "submit") {
return initialState
}
else {
throw new Error("unhandled action type " + action.type)
}
}
export default function MultistepFormReducer() {
// const [currentStep, setCurrentStep] = React.useState(1);
// const [formData, setFormData] = React.useState(initialFormData);
const [state, dispatch] = React.useReducer(reducer, initialState)
const {currentStep, formData} = state
const handleNextStep = () => {
// setCurrentStep(currentStep + 1);
dispatch({type: "next"})
};
const handlePrevStep = () => {
dispatch({type: "previous"})
};
const handleChange = (e) => {
dispatch({
type: "change",
payload: {
name: e.target.name,
value: e.target.value
}
})
// setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
alert("Thank you for your submission");
dispatch({type: "submit"})
// setCurrentStep(1);
// setFormData(initialFormData);
};
if (currentStep === 1) {
return (
<form onSubmit={handleSubmit}>
<h2>Personal Information</h2>
<div>
<label>Step {currentStep} of 3</label>
<progress value={currentStep} max={3} />
</div>
<div>
<label htmlFor="name">Name</label>
<input
required
name="name"
id="name"
placeholder="Enter your name"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
required
name="email"
id="email"
type="email"
placeholder="Enter your email"
value={formData.email}
onChange={handleChange}
/>
</div>
<button type="button" className="secondary" onClick={handleNextStep}>
Next
</button>
</form>
);
} else if (currentStep === 2) {
return (
<form onSubmit={handleSubmit}>
<h2>Address</h2>
<div>
<label>Step {currentStep} of 3</label>
<progress value={currentStep} max={3} />
</div>
<div>
<label htmlFor="address">Address</label>
<input
required
name="address"
id="address"
type="address"
placeholder="What is your address?"
value={formData.address}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="city">City</label>
<input
required
name="city"
id="city"
placeholder="What city do you live in?"
value={formData.city}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="zipcode">Zipcode</label>
<input
required
name="zipcode"
id="zipcode"
type="number"
placeholder="What is your zipcode?"
value={formData.zipcode}
onChange={handleChange}
/>
</div>
<div>
<button className="secondary" type="button" onClick={handleNextStep}>
Next
</button>
<button type="button" className="link" onClick={handlePrevStep}>
Previous
</button>
</div>
</form>
);
} else if (currentStep === 3) {
return (
<form onSubmit={handleSubmit}>
<h2>Confirm your information:</h2>
<div>
<label>Step {currentStep} of 3</label>
<progress value={currentStep} max={3} />
</div>
<table>
<tbody>
{Object.keys(formData).map((key) => {
return (
<tr key={key}>
<td>{key}</td>
<td>{formData[key]}</td>
</tr>
);
})}
</tbody>
</table>
<div>
<button className="primary" type="submit">
Submit
</button>
<button type="button" className="link" onClick={handlePrevStep}>
Previous
</button>
</div>
</form>
);
} else {
return null;
}
}
use case 3: decoupling state updates from state values, leading to less setup and tear down
This is the way where if we use state, then we create an interval and tear it down each time step changes, which is not ideal:
import * as React from "react"
import Slider from "./Slider"
export default function Counter() {
const [count, setCount] = React.useState(0)
const [step, setStep] = React.useState(1)
const handleIncrement = () => setCount(count + step)
const handleDecrement = () => setCount(count - step)
const handleReset = () => setCount(0)
const handleUpdateStep = (step) => setStep(step)
React.useEffect(() => {
console.log('useEffect called')
const id = window.setInterval(() => {
setCount((c) => c + step)
}, 1000)
return () => window.clearInterval(id)
}, [step])
return (
<main>
<h1>{count}</h1>
<div>
<button onClick={handleDecrement}>-</button>
<button onClick={handleIncrement}>+</button>
<button onClick={handleReset}>0</button>
</div>
<Slider
min={1}
max={10}
onChange={handleUpdateStep}
/>
</main>
)
}
But if we use useReducer, we solve this issue since we decouple state updates from their current values, thus meaning we can have an empty dependency array:
import * as React from "react"
import Slider from "./Slider"
function reducer(state, action) {
if (action.type === "increment") {
return {
count: state.count + state.step,
step: state.step,
}
} else if (action.type === "decrement") {
return {
count: state.count - state.step,
step: state.step,
}
} else if (action.type === "reset") {
return {
count: 0,
step: state.step,
}
} else if (action.type === "updateStep") {
return {
count: state.count,
step: action.step,
}
} else {
throw new Error("This action type isn't supported.")
}
}
export default function Counter() {
const [state, dispatch] = React.useReducer(reducer, {
count: 0,
step: 1
})
const handleIncrement = () => dispatch({ type: "increment" })
const handleDecrement = () => dispatch({ type: "decrement" })
const handleReset = () => dispatch({ type: "reset" })
const handleUpdateStep = (step) => dispatch({ type: "updateStep", step })
React.useEffect(() => {
console.log("useEffect called")
const id = window.setInterval(() => {
dispatch({ type: "increment" })
}, 1000)
return () => window.clearInterval(id)
}, [])
return (
<main>
<h1>{state.count}</h1>
<div>
<button onClick={handleDecrement}>-</button>
<button onClick={handleIncrement}>+</button>
<button onClick={handleReset}>0</button>
</div>
<Slider
min={1}
max={10}
onChange={handleUpdateStep}
/>
</main>
)
}
NOTE
useReducer also offers a bit more flexibility than useState since it allows you to decouple how the state is updated from the action that triggered the update - typically leading to more declarative state updates.
useLayoutEffect
The useLayoutEffect runs side effects the same way as useEffect except that the side effects (including state changes) runs synchronously BEFORE the browser gets painted.
Here is the different between useLayoutEffect and useEffect:
useLayoutEffect: runs synchronously after the rendering stage but before browser paintuseEffect: runs asynchronously after the rendering stage and after browser paint
React guarantees that the code inside useLayoutEffect and any state updates scheduled inside it will be processed before the browser repaints the screen. This lets your component use layout information for rendering – as we're doing in our example with the browser's dimensions.
NOTE
Use useLayoutEffect if you need to synchronize state with the DOM or layout, basically for some use case where the side effect needs to run before the browser paints the screen.
WARNING
Don't use useLayoutEffect unless you're synchronizing externally with the layout. The only thing to keep in mind with useLayoutEffect is you don't want to use it unnecessarily. If you're not synchronizing layout information with your component, you don't need it. Unless you absolutely have to, intentionally blocking React from updating the UI is not a great idea.
useSyncExternalStore
Use the useSyncExternalStore hook to subscribe to an external store or variable that can change values throughout the lifetime of the app, like navigator.userAgent or navigator.onLine.
This is an example where useState + useEffect is subpar, and useSyncExternalStore would work much better
import * as React from "react"
export default function App () {
const [networkStatus, setNetworkStatus] = React.useState("online")
React.useEffect(() => {
const handleChange = () => {
setNetworkStatus(navigator.onLine ? "online" : "offline")
}
window.addEventListener("online", handleChange)
window.addEventListener("offline", handleChange)
return () => {
window.removeEventListener("online", handleChange)
window.removeEventListener("offline", handleChange)
}
}, [])
return (
<div>
<span className={networkStatus} />
<label>{networkStatus}</label>
</div>
)
}
In scenarios where a piece of non-React state is already being managed by some outside system – whether that's the browser, another library, or even your own custom data store – you can use React's built-in useSyncExternalStore hook to subscribe a component to that state without needing to redundantly duplicate it as React state in your component.
This hook requires two arguments:
getSnapshot(): a function that returns the external statesubscribe(): a subscription function that returns a callback function that gets invoked when removing the subscription.
This hook returns whatever getSnapshot() returns as a state variable, but the subscribe() function determines the subscription and unsubscription process and manages when to call getSnapshot() behind the scenes, updating the state automatically through the subscription.
const getSnapshot = () => {
return snapshot
}
const subscribe = () => {
// subscription code here
}
const snapshot = React.useSyncExternalStore(subscribe, getSnapshot)
This is the complete example:
import * as React from "react"
const getSnapshot = () => {
return navigator.onLine ? "online" : "offline"
}
const subscribe = (callback) => {
window.addEventListener("online", callback)
window.addEventListener("offline", callback)
return () => {
window.removeEventListener("online", callback)
window.removeEventListener("offline", callback)
}
}
export default function App () {
const networkStatus = React.useSyncExternalStore(
subscribe,
getSnapshot
)
return (
<div>
<span className={networkStatus} />
<label>{networkStatus}</label>
</div>
)
}
Performance
The way useSyncExternalStore decides if it should resubscribe to the store or re-render the UI is through value equality:
`
- resubscription logic: If the
subscribefunction you pass in has a different reference value than the previous snapshot, it runs the subscription cleanup function and re-subscribes. - re-render logic: If the return value from the
getSnapshotvalue changes, then that is equivalent to a state change and then a re-render gets triggered.
NOTE
If the subscribe function changes between renders, then useSyncExternalStore will re-subscribe to the store. Similarly, if the value returned from getSnapshot change between renders, then React will trigger a re-render of the component because the state changed.
WARNING
If the value you return from getSnapshot is a reference value, like the object literal we're returning, and that reference value isn't memoized, then you'll throw React into an infinite loop scenario since useSyncExternalStore will re-render every time the reference value changes, and every re-render will call getSnapshot again.
There are two ways to mitigate this:
- return only primitive values from
getSnapshot(): this way value- equality checking works - cache any object values from
getSnapshot(): this way value-equality works by returning the same object reference across renders.
Here's an example where by returning object values and instantiating the getSnapshot() and subscribe() functions inside the component, we have to use useMemo() and useCallback() to avoid constantly recreating those functions and thus creating infinite loops:
import * as React from "react"
export default function NetworkIndicator () {
// 1. create "cache" of snapshot state so that reference stays consistent.
const cache = React.useRef({
networkStatus: navigator.onLine ? "online" : "offline",
language: navigator.language
})
// 2. update "cache" to preserve object creation across re-renders
const getSnapshot = React.useCallback(() => {
const prevState = cache.current
const nextState = {
networkStatus: navigator.onLine ? "online" : "offline",
language: navigator.language
}
if (
prevState.networkStatus === nextState.networkStatus &&
prevState.language === nextState.language
) {
return prevState
}
cache.current = nextState
return nextState
}, [])
// avoid recreating function
const subscribe = React.useCallback((callback) => {
console.log("Subscribing to network status")
window.addEventListener("online", callback)
window.addEventListener("offline", callback)
return () => {
console.log("Unsubscribing to network status")
window.removeEventListener("online", callback)
window.removeEventListener("offline", callback)
}
}, [])
const networkStatus = React.useSyncExternalStore(
subscribe,
getSnapshot
)
return (
<div className="network">
<span className={networkStatus} />
<label>{networkStatus}</label>
</div>
)
}
Example
Here's an example where we naively use useState + useEffect for matching media:
import * as React from "react";
import { phone, desktop } from "./icons";
const query = "only screen and (max-width : 768px)";
export default function MatchMedia() {
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const handleChange = () => {
setIsMobile(window.matchMedia(query).matches);
};
const matchMedia = window.matchMedia(query);
matchMedia.addEventListener("change", handleChange);
return () => {
matchMedia.removeEventListener("change", handleChange);
};
}, []);
return (
<section>
Resize your browser's window to see changes.
<article>
<figure className={isMobile ? "active" : ""}>
{phone}
<figcaption>Is mobile: {`${isMobile}`}</figcaption>
</figure>
<figure className={!isMobile ? "active" : ""}>
{desktop}
<figcaption>Is larger device: {`${!isMobile}`}</figcaption>
</figure>
</article>
</section>
);
}
Here is the imporved version using useSyncExternalStore:
import * as React from "react";
import { phone, desktop } from "./icons";
const query = "only screen and (max-width : 768px)";
const getSnapshot = () => {
return window.matchMedia(query).matches
}
const subscribe = (callback) => {
const matchMedia = window.matchMedia(query);
matchMedia.addEventListener("change", callback);
return () => {
matchMedia.removeEventListener("change", callback);
};
}
export default function MatchMedia() {
const isMobile = React.useSyncExternalStore(subscribe, getSnapshot)
return (
<section>
Resize your browser's window to see changes.
<article>
<figure className={isMobile ? "active" : ""}>
{phone}
<figcaption>Is mobile: {`${isMobile}`}</figcaption>
</figure>
<figure className={!isMobile ? "active" : ""}>
{desktop}
<figcaption>Is larger device: {`${!isMobile}`}</figcaption>
</figure>
</article>
</section>
);
}
useEffectEvent
useEffect recap
useEffect encapsulates side effect code by removing it from the rendering flow and then asynchronously executing that side effect code after rendering, when it's safe to do so.
Here is how the useEffect execution lifecycle works:
- after initial render: executes side effects after the first render of the component has completed, and queues up the cleanup function to execute before the first re-render.
- subsequent re-renders
- without dependency array: executes the side effect code again after every render.
- with dependency array: When one of the values in its dependency array has changed,
useEffectwill re-execute after the render, and then when one of its dependencies changes again, it queues up the cleanup function to execute before the next re-render.
useEffectEvent usage
Whenever you need to access a reactive but non-synchronizing value inside of useEffect, look into abstracting that logic into useEffectEvent.
// url is the only synchronizing value, we just use state.
const onPageView = React.useEffectEvent((url) => {
pageview(url, state)
})
// synchronize only to url, not state
React.useEffect(() => {
onPageView(url)
}, [url])
NOTE
The main use case for useEffectEvent is that is allows you to abstract reactive but non-synchronizing values out of useEffect, which eliminates unnecessary useEffect executions.
The main use case for using this hook is when you run into a problem like this:
React.useEffect(() => {
const id = window.setInterval(() => {
setCount((c) => c + step)
}, delay)
return () => window.clearInterval(id)
}, [delay, step])
// setting the interval doesn't really count on step,
// so we have unnecessary creation and teardown
We need to access the reactive step value from inside of useEffect, but we don't want to include it in the dependency array because it has nothing to do with setting and removing our interval.
This is the perfect use case for useEffectEvent. All we have to do is abstract our reactive but non-synchronizing logic into useEffectEvent, then we can use that event handler inside of useEffect without needing to include it as a dependency.
import * as React from "react"
import Slider from "./Slider"
React.useEffectEvent = React.experimental_useEffectEvent;
export default function App() {
const [count, setCount] = React.useState(0)
const [delay, setDelay] = React.useState(100)
const [step, setStep] = React.useState(1)
const handleDelayChange = (d) => setDelay(d)
const handleStepChange = (s) => setStep(s)
// step and count are reactive, but not used for synchronization for interval
const onInterval = React.useEffectEvent(() => {
setCount(count + step)
})
// in effect, only synchronizing state is delay
React.useEffect(() => {
const id = window.setInterval(onInterval, delay)
return () => window.clearInterval(id)
}, [delay])
return (
<main>
<h1>{count}</h1>
<Slider
min={100}
max={2000}
step={100}
onChange={handleDelayChange}
label="ms delay"
/>
<Slider
min={1}
max={10}
step={1}
onChange={handleStepChange}
label="increment by"
/>
</main>
)
}
Concurrent React
When React is busy doing computationally expensive renders, it blocks the main thread and prevents other renders from happening, no matter how lightweight they might be. A big performance problem in React is that it treats all renders and computations with the same priority.
NOTE
The problem with old react
if React was rendering, and a high-priority event like a user input occurred, because all of React's work happened on the main thread, React would have no choice but to finish rendering before it could process that event.
Concurrent React is a way to schedule and prioritize different component renders as to prioritize high-priority renders and avoid blocking the main thread by offloading expensive yet low-priority rendering work to a separate background thread.
However, we as developers have to manually tell React which rendering work is high priority and which is low priority and can be deferred, which is where these two hooks come into play:
useDeferredValue
The useDeferredValue hook is a way to activate concurrent rendering in React by telling React to defer updating a value until all of its high priority work has finished.
NOTE
You can think of useDeferredValue as a smarter, more dynamic debouncing functionality for executing high-priority work, deferring updating state until all of the high priority rendering work has finished.
You can think of useDeferredValue as a way to tell React to defer updating a value until all its high-priority rendering work has finished.
For example, a common problem this hook was made to fix is a search items in a list functionality, where the query changes the list state, often performing an API call and then rendering all search results in the list. Here are the two rendering things in this problem:
- high priority, low computation: rendering the search term in the
<input>tag - low priority, expensive computation: rendering the list of results from search
Historically, React would treat updating the input field with the exact same priority as updating the expensive component. Often times, this meant the value, and therefore the text in the input field, would lag behind what the user had actually typed since React was too busy rendering the expensive list component to notice.
Instead, what we want is for React to always prioritize updating the input field, and then, only when it's finished, re-render the Employees component with the final value.
This is exactly how useDeferredValue works.
You pass it the value you want to defer, and React will automatically defer updating that value until all its high-priority rendering work has finished.
import * as React from "react"
import Employees from "./Employees"
export default function App() {
// high priority user-facing search term
const [searchQuery, setSearchQuery] = React.useState("");
// low priority search term used to render list (expensive API call)
const deferredSearchQuery = React.useDeferredValue(searchQuery)
return (
<div className="wrapper">
<div className="search-container">
<label>Filter Employees</label>
<input
type="text"
placeholder="Laurie Lowe"
className="search-input"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Employees query={deferredSearchQuery} />
</div>
);
}
By passing the deferredSearchQuery to the Employees component, and wrapping Employees inside of React.memo, Employees will only re-render when deferredSearchQuery has changed, which is when React has already finished all of it's high-priority work.
useTransition
The useTransition hook also is another way to hook into concurrent rendering in React, by wrapping state updates that lead to computationally expensive operations (re-render may be expensive or state update may kick off expensive side effect) in a "transition".
Whenever you wrap a state update inside of React.startTransition, that tells React that that update, which we'll call a transition, may trigger a computationally expensive render, and as such, to not block any other higher-priority events that may occur during that render.
NOTE
A transition is a non-urgent, interruptible state update that allows React to defer updating the UI until all the work and the transition is complete.
NOTE
startTransition allows developers to mark specific state updates as lower priority. Any state updates wrapped in startTransition will be treated as non-blocking updates that can be interrupted if a higher-priority update occurs.
The way it works is while React is busy working on the transition, it will continue to show the user what they were already seeing, while at the same time, checking every 5ms to see if there are any other higher-priority events that it should prioritize.
If there are, it will pause the work it's doing and shift its priority.
If not, or once all the high-priority work has finished, it'll go back to working on the transition, committing the work all at once when it's finished – updating the UI.
This is how it works:
- React queues state updates wrapped in a
startTransition()call to take place, offloading its rendering work caused by that state update to another thread while showing the UI rendered by the previous snapshot (the snapshot before the transition executes) - While the transition is executing in the background, React checks every 5ms for high priority rendering work to run in the main thread
- Once all high priority rendering work is finished, React finishes the transition and updates the UI with the state update snapshot the transition wrapped.
const [isPending, startTransition] = React.useTransition()
And here is a complete example:
import * as React from 'react';
import useConversations from './useConversations';
import Chat from './Chat';
import { MessageSquareIcon } from './icons';
export default function App() {
const conversations = useConversations();
const [activeChat, setActiveChat] = React.useState(0);
const handleSelectChat = (index) => {
React.startTransition(() => {
setActiveChat(index);
})
};
return (
<div>
<aside>
<div>
{conversations.map(({ id, messages, title }, i) => {
let className = activeChat === i ? 'selected' : '';
return (
<button
key={id}
className={className}
onClick={() => handleSelectChat(i)}
>
<MessageSquareIcon length={messages.length} />
<span>{title}</span>
</button>
)
})}
</div>
</aside>
<Chat conversation={conversations[activeChat]} />
</div>
);
}
async startTransition
In concurrent react, we can also mark any type of computational work as low-priority, not just rendering work. For example, since going to the network is the main bottleneck in most web apps, we can mark that as low priority work to free up other high priority async work.
So in React v19, that's exactly what they did by giving transitions the ability to support async functions – and they even gave them a fun name, "actions".
We can use startTransition and pass in an async callback - also called action - to mark any code inside that callback as low-transition code.
const action = async () => {}
startTransition(action)
NOTE
The main benefit of using async transitions and actions over managing loading state manually is that all currently queued async transition calls are batched together and only update the UI when every transition has finished. Think of it as a really good debounce.
However, there's one major caveat: any state update code inside an async startTransition callback will NOT be treated as a low-priority state update and thus will not be considered a "transition". To fix this, just wrap that state update in a nested startTransition() call.
WARNING
When you use await inside a startTransition function, the state updates that happen after the await are not marked as Transitions.
import * as React from "react"
import { fetchPrice } from "./api"
export default function App() {
const [quantity, setQuantity] = React.useState(1);
const [price, setPrice] = React.useState(null)
const [isPending, startTransition] = React.useTransition()
React.useEffect(() => {
const handleFetchPrice = async () => {
const data = await fetchPrice(quantity)
setPrice(data)
}
handleFetchPrice()
}, [])
const handleChange = async (e) => {
const nextQty = Number(e.target.value)
setQuantity(nextQty);
startTransition(async () => {
const data = await fetchPrice(nextQty)
startTransition(() => {
setPrice(data)
})
})
}
if (!price) {
return null
}
const baseUrl = `https://ui.dev/api/checkout?product_id=subscription`
return (
<main>
<img
src="https://ui.dev/images/subscribe/tier-annual.svg"
alt="ui.dev courses banner"
/>
<div>
<em>
{price.discountedPrice || price.basePrice} <span>/yr</span>
</em>
{price.discountedPrice && <s>{price.basePrice}</s>}
</div>
<div>
Team Size
<input type="number" value={quantity} onChange={handleChange} />
</div>
<p>
{price.discount?.percentOff > 0
? <>Enjoy your <strong>{price.discount.percentOff}% off</strong> bulk discount <br /></>
: null}
{price.message}
</p>
<a
href={`${baseUrl}&quantity=${quantity}&code=${price.discount?.code || ""}`}
className={isPending ? "pending" : ""}
>
Subscribe to ui.dev
</a>
</main>
);
}
useOptimistic
The useOptimistic hook is built on top of React's transition architecture, and it gives you access to the intermediate state updates during a transition, whereas useTransition does not. It only gives you the final state after batching all transitions
here is the basic structure of using it:
const [optimisticState, addOptimistic] = useOptimistic(
state,
(currentState, optimisticValue) => {
return getNextOptimisticState(currentState, optimisticValue);
}
)
Let's break it down:
optimisticState: set to the initialstateinitially, but will be whatever is returned from the callback you passed in, when you execute that callback usingaddOptimistic.addOptimistic(): executes the callback you passed in.
Here is an example of using this hook:
import * as React from "react";
import useNotes from "./useNotes";
export default function FieldNotes() {
const { notes, addNote } = useNotes();
const [optimisticNotes, addOptimisticNote] = React.useOptimistic(
notes,
(state, newNote) => [
...state,
{
text: newNote,
status: "pending"
},
]
);
const formAction = async (formData) => {
const newNote = formData.get("note");
if (newNote.trim()) {
addOptimisticNote(newNote);
await addNote(newNote);
}
}
return (
<article>
<h1>Field Notes</h1>
<div>
<ul>
{optimisticNotes.map((note, index) => (
<li key={index}>
<p>{note.text}</p>
{note.status === "pending" && <i>saving...</i>}
</li>
))}
</ul>
<form action={formAction}>
<input
required
type="text"
name="note"
placeholder="Type your note..."
/>
<button className="link" type="submit">
Submit
</button>
</form>
</div>
</article>
);
}
Here's a more advanced way of using this hook, where we account for all CRUD ways to optimistically update a list of objects.
function TodoList() {
const [todos, setTodos] = useState([]);
const [optimisticTodos, setOptimisticTodos] = React.useOptimistic(
todos,
(currentTodos, action) => {
switch (action.type) {
case "add":
return [...currentTodos, action.todo];
case "delete":
return currentTodos.filter((todo) => todo !== action.todo);
case "update":
return currentTodos.map((todo) =>
todo === action.oldTodo ? action.newTodo : todo
);
default:
return currentTodos;
}
}
);
}
use
The use() function in React is not a hook but rather it is a way to await promises but with two important behaviors:
- transitions with
use(promise)will be considered pending: if you calluse(promise)in a transition of any kind, that transition will be considered pending until the promise resolves. - React rerenders when
use(promise)resolves: If you resolve a promise withuse(), React re-renders even though it's not a state update.
To avoid infinite re-rendering when using use(promise) (since a promise is an object, each re-render leads to a different in-memory address for that promise), it's important to cache promises somehow:
WARNING
use lets you unwrap the value of a promise, but you need to make sure that promise is referentially consistent across renders. This is usually done by caching the promise yourself.
import * as React from "react"
import { fetchPokemon } from "./api"
let cache = new Map()
// use cache for stable references instead of constantly recreating promises
const getPokemon = (id) => {
if (!cache.has(id)) {
cache.set(id, fetchPokemon(id))
}
return cache.get(id)
}
export default function PokemonCard({ id }) {
// blocks component from rendering until promise resolves
const pokemon = React.use(getPokemon(id))
return (
<div className="card">
<figure>
<img
width="475px"
height="475px"
src={pokemon?.sprites?.front_default}
alt={pokemon.name}
/>
<figcaption>
<h4>{pokemon.name}</h4>
<h6>No. {pokemon.id}</h6>
</figcaption>
</figure>
</div>
)
}
Whenever we use use(promise) in a component, the component itself is considered "pending" and thus we can wrap it in a <Suspense> to show some loading state until the promise resolves and thus the component is ready to be rendered.
import * as React from "react"
import PokemonCard from "./PokemonCard"
import Carousel from "./Carousel"
import LoadingCard from "./LoadingCard"
export default function App () {
const [id, setId] = React.useState(1)
const [isPending, startTransition] = React.useTransition()
const handlePrevious = () => {
if (id > 1) {
startTransition(() => {
setId((currentId) => currentId - 1)
})
}
}
const handleNext = () => startTransition(() => {
setId((currentId) => currentId + 1)
})
return (
<Carousel onPrevious={handlePrevious} onNext={handleNext}>
<React.Suspense fallback={<LoadingCard />}>
{isPending ? <LoadingCard /> : <PokemonCard id={id} />}
</React.Suspense>
</Carousel>
)
}
Form actions
As of react 19, instead of passing an onSubmit= handler to a <form> element, we can pass an action instead, which has several benefits:
- cleaner code
- no need for controlled form inputs: we automatically get the
FormDatainstance that contains the values of all the fields in the form at the time of submission.
export default function Search() {
const searchAction = async (formData) => {
const query = formData.get("query");
const results = await submitSearch(query);
redirectTo("/results", { results });
}
return (
<form action={searchAction}>
<input name="query" />
<button type="submit">Search</button>
</form>
);
}
Server components
Server components enable cool shit like this:

NOTE
Components with the use client directive can (and do with certain frameworks) render on a server. A better way to think about it is Server Components generate serializable templates and use client generates a JavaScript bundle.
whenever you add the use client directive to a module, React will treat that module and all of its transitive dependencies as Client modules (and include them in the bundle it generates).
101 Tips
1. HOC
A higher order component transforms a component into a better version of itself, adding additional functionality to it. It is a function that takes in a component and returns a new component
HOC are named with the with convention, as prefixed function started with “with” as a way of saying that HOCs enhance components with some funcitonality.
Here are some rules for creating a good higher order component
- Don’t mutate the passed-in component
- Don’t destroy the structure of props for the passed-in component
- Spread across all the props, and then you can add new ones.
- Don’t instantiate HOCs inside other components
- Instantiating an enhanced component inside another component makes it vulnerable to re-renders.
- Only instantiate HOCs outside components.
const Component = (props) => {// This is wrong. Never do thisconst EnhancedComponent = HOC(WrappedComponent);return <EnhancedComponent />;};// This is the correct wayconst EnhancedComponent = HOC(WrappedComponent);const Component = (props) => {return <EnhancedComponent />;};
Here is an example of an HOC:
function withMousePosition(WrappedComponent) {
return (props) => {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
useEffect(() => {
const handleMouseMove = (e) => {
setMouseX(e.clientX)
setMouseY(e.clientY)
}
window.addEventListener("mousemove", handleMouseMove)
return () => window.removeEventListener("mousemove", handleMouseMove)
}, [])
return <WrappedComponent {...props} mouseX={mouseX}, mouseY={mouseY} />
}
}
2. Dealing with Form state
The best way to deal with form state is to use object state and then create handler that sets state using event.target.value.
function Form() {
const [{ name, email }, setFormState] = useState({
name: "",
email: "",
});
const createFormValueChangeHandler = (field) => {
return (event) => {
setFormState((formState) => ({
...formState,
[field]: event.target.value,
}));
};
};
return (
<>
<h1>Class Registration Form</h1>
<form>
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={createFormValueChangeHandler("name")}
/>
</label>
<label>
Email:{" "}
<input
type="email"
value={email}
onChange={createFormValueChangeHandler("email")}
/>
</label>
</form>
</>
);
}
And here is the custom hook version of that:
import React, { useState } from "react";
export function useFormObjectState<T extends Record<string, any>>(
initialData: T
) {
const [formState, setFormState] = useState(initialData);
const createFormValueChangeHandler = (field: keyof T) => {
return (event: React.ChangeEvent<HTMLInputElement>) => {
setFormState((formState) => ({
...formState,
[field]: event.target.value,
}));
};
};
return { formState, createFormValueChangeHandler };
}
3. Master useRef
Did you know you can pass a function to the ref attribute instead of a ref object?
Here's how it works:
- When the DOM node is added to the screen, React calls the function with the DOM node as the argument.
- When the DOM node is removed, React calls the function with
null.
Here is how we can use refs with a function call, where we pass in a callback to the ref= prop on a JSX element, where this callback has one argument, which is the DOM element we're attaching the ref to.
This pattern is useful to prevent us from doing a useEffect to access and do stuff on the ref - instead the function is called whenever the ref is available or null.
❌ Before: