React Docs: Learn React
Table of Contents
Describing the UI
Your First Component
It is possible to use React for only ‘sprinkles of interactivity’.
Writing Markup with JSX
For historical reasons, aria-*
and data-*
attributes are written as in HTML
with dashes, and not in camelCase.
Rendering Lists
Arrow functions implicitly return the expression right after =>
, so you don’t
need a return
statement. However, you must write return
explicitly if your
=>
is followed by a {
curly brace. Arrow functions containing => {
are
said to have a ‘block body’.
For creating keys: If your data is generated and persisted locally (e.g. notes
in a note-taking app), use an incrementing counter, crypto.randomUUID()
or a
package like uuid
when creating items.
Rules of keys
Keys must be unique among siblings. However, it’s okay to use the same keys for JSX nodes in different arrays.
Keys must not change or that defeats their purpose! Don’t generate them while rendering.
React will use the item’s index if you don’t specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered.
Do not generate keys on the fly, e.g. with key={Math.random()}
.
This will cause keys to never match up between renders, leading to all your
components and DOM being recreated every time. Not only is this slow, but it
will also lose any user input inside the list items.
Note that your components won’t receive key
as a prop. It’s only used as a
hint by React itself.
Adding Interactivity
Responding to Events
Can stop event propogation with e.stopPropagation();
. Can also stop default
browser behaviour (i.e. stopping refresh after submitting forms) with
e.preventDefault()
.
Render and Commit
The default behavior of rendering all components nested within the updated component is not optimal for performance if the updated component is very high in the tree. See the Performance section for more details.
Updating Objects in State
Note that the … spread syntax is ‘shallow’ – it only copies things one level deep. This makes it fast, but it also means that if you want to update a nested property, you’ll have to use it more than once.
Square brackets inside your object definition specify a property with dynamic name. This can be used to create a single event handler for multiple fields:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleChange(e) { setPerson({ ...person, [e.target.name]: e.target.value }); } return ( <> <label> First name: <input name="firstName" value={person.firstName} onChange={handleChange} /> </label> <label> Last name: <input name="lastName" value={person.lastName} onChange={handleChange} /> </label> <label> Email: <input name="email" value={person.email} onChange={handleChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
For nested objects:
setPerson({ ...person, // Copy other fields artwork: { // but replace the artwork ...person.artwork, // with the same one city: 'New Delhi' // but in New Delhi! } });
Although objects are not technically nested, they are separate objects which point at each other.
Immer allows you to write convenient but mutating syntax which takes care of producing copies for you. With Immer, this is valid functioning code:
updatePerson(draft => { draft.artwork.city = 'Lagos'; });
To try Immer:
Run
npm install use-immer
to add Immer as a dependencyThen replace
import { useState } from 'react'
withimport { useImmer } from 'use-immer'
Updating Arrays in State
In Javascript, slice
is how to select a part of an array.
import { useState } from 'react'; let nextId = 3; const initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState( initialArtists ); function handleClick() { const insertAt = 1; // Could be any index const nextArtists = [ // Items before the insertion point: ...artists.slice(0, insertAt), // New item: { id: nextId++, name: name }, // Items after the insertion point: ...artists.slice(insertAt) ]; setArtists(nextArtists); setName(''); } return ( <> <h1>Inspiring sculptors:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={handleClick}> Insert </button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Managing State
Choosing the State Structure
Group related state.
Avoid contradictions in state.
Avoid redundant state.
Avoid duplication in state.
Avoid deeply nested state.
Preserving and Resetting State
If you want to preserve the state between re-renders, the structure of your tree needs to ‘match up’ from one render to another. Hence you should never nest component function definitions, because otherwise each time the parent is re-rendered, so are any nested components. Always declare component functions at the top level, and don’t nest their definitions.
Keys are not globally unique. They only specify the position within the parent.
Can use keys to reset state.
To preserve state for removed apps, could:
render everything but hide everything aside from the current one with CSS. This is slow and stupid.
lift the state up.
save to (and load from)
localStorage
. This could be clever.
Extracting State Logic into a Reducer
A reducer takes two arguments, the current state and the action object, and it returns the next state.
Example:
const tasksReducer = (tasks, action) => { if (action.type === "added") { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } else if (action.type === "changed"){ return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } else if (action.type === "deleted") { return tasks.filter((t) => t.id !== action.id); } else { throw Error("Unknown action: " + action.type); } }
By convention, switch statements are used in reducers:
const tasksReducer = (tasks, action) => { switch (action.type) { case "added": { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } case "changed": { return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case "deleted": { return tasks.filter((t) => t.id !== action.id); } default: { throw Error("Unknown action: " + action.type); } } }
Reducers are named after reduce
which takes the result so far and the
current item.
const arr = [1, 2, 3, 4, 5]; const sum = arr.reduce( (result, number) => result + number ); return sum
To use a reducer:
import {useReducer} from "react"; const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
When reducers are used, event handlers only specify what happened by dispatching actions, and the reducer function determines how the state updates in reponse to them.
The Immer reducer is called useImmerReducer
. Immer provides a special draft
object which is safe to mutate.
const tasksReducer = (draft, action) => { switch (action.type) { case "added": { draft.push({ id: action.id, text: action.text, done: false, }), break; } case "changed": { const index = draft.findIndex((t) => t.id === action.task.id); draft[index] = action.task; break; } case "deleted": { return tasks.filter((t) => t.id !== action.id); } default: { throw Error("Unknown action: " + action.type); } } }
Passing Data Deeply with Context
Context allows a parent to make information available to any components in the tree below it, no matter how deep, without passing it explicitly through props.
For context, on top of useContext
, a context provider is also required.
import { createContext } from "react"; const LevelContext = createContext(1); const Heading = ({ children }) => { const level = useContext(LevelContext); // .... } const Section = ({ children }) { return ( <section className="section"> <LevelContext.Provider value={level + 1}> {children} </LevelContext.Provider> </section> ); }
Contexts are especially useful for:
theming,
making the current account available,
routing,
managing state. It is common to use a reducer together with context.
Scaling Up with Reducer and Context
Need to separate contexts for state and the dispatch:
import { createContext } from "react"; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Then to provide them for the entire tree below:
import { TasksContext, TasksDispatchContext } from "./TasksContext.jsx"; export default TaskApp = () => { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); //... return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> ... </TasksDispatchContext.Provider> </TasksContext.Provider> ); }
Can move all of the wiring into one file:
export TasksProvider = ({ children }) => { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> {children} </TasksDispatchContext.Provider> </TasksContext.Provider> ); }
Then TaskApp
looks like:
import { TasksProvider } from "./TasksContext.jsx"; export default TaskApp = () => { return ( <TasksProvider> ... </TasksProvider> ); }
Can create custom hooks:
export useTasks = () => { return useContext(TasksContext); } export useTasksDispatch = () => { return useContext(TasksContextDispatch); }
Then when a component needs to read context, it can do it through these functions:
const tasks = useTasks(); const dispatch = useTasksDispatch();
Escape Hatches
When you want a component to ‘remember’ some information, but you don’t want that information to trigger new renders, you can use a ref:
const ref = useRef(0);
There are two common cases where you do not need effects:
transforming data for rendering,
handling user events.
Manipulating the DOM with Refs
React does not let a component access the DOM nodes of other components, not even for its own children; components that want to expose their DOM nodes have to opt in to that behaviour.
const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; });
Synchronizing with Effects
useEffect
without a dependency array with run after every re-render.
useEffect
with an empty dependency array only runs on mount.
To clean up, use return
.
useEffect(() => { const connection = createConnection(); connection.connect(); return () => { connection.disconnect(); }; }, []);
When React runs in development mode, it seeks out bugs, so in the above case will connect, then disconnect, the re-connect again. This does not occur in production.
If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result:
useEffect(() => { let ignore = false; async function startFetching() { const json = await fetchTodos(userId); if (!ignore) { setTodos(json); } } startFetching(); return () => { ignore = true; }; }, [userId]);
The React documentation states that data fetching through React isn’t the quickest, and even suggests using a more fully featured framework such as Next.js to deal with data fetching. Should I look into Next.js?
Some logic should only run once when the application starts. You can put it outside your components:
if (typeof window !== 'undefined') { // Check if we're running in the browser. checkAuthToken(); loadDataFromLocalStorage(); } const App = () => { // ... }
You Might Not Need An Effect
Cache expensive calculations with useMemo
:
const TodoList = ({ todos, filter }) => { const [newTodo, setNewTodo] = useState(''); const visibleTodos = useMemo(() => { // Does not re-run unless todos or filter change return getFilteredTodos(todos, filter); }, [todos, filter]); // ... }
To find out how expensive a calculations is, use console.time
and
console.timeEnd
:
console.time('filter array'); const visibleTodos = getFilteredTodos(todos, filter); console.timeEnd('filter array');
Do not chain useEffect
as it is incredibly inefficient, and a sign of bad
code.
To subscribe to an external store, use useSyncExternalStore
.
To fix race conditions, use ignore.
function SearchResults({ query }) { const [results, setResults] = useState([]); const [page, setPage] = useState(1); useEffect(() => { let ignore = false; fetchResults(query, page).then(json => { if (!ignore) { setResults(json); } }); return () => { ignore = true; }; }, [query, page]); function handleNextPageClick() { setPage(page + 1); } // ... }
Consider building own Effect to make data fetching better:
function SearchResults({ query }) { const [page, setPage] = useState(1); const params = new URLSearchParams({ query, page }); const results = useData(`/api/search?${params}`); function handleNextPageClick() { setPage(page + 1); } // ... } function useData(url) { const [data, setData] = useState(null); useEffect(() => { let ignore = false; fetch(url) .then(response => response.json()) .then(json => { if (!ignore) { setData(json); } }); return () => { ignore = true; }; }, [url]); return data; }
Lifecycle of Reactive Effects
All values inside the component (including props, state, and variables in your component’s body) are reactive. Any reactive value can change on a re-render, so you need to include reactive values as Effect’s dependencies.
Removing Effect Dependencies
Object and function dependencies create a risk that your Effect will re-synchronize more often than you need. Whenever possible, avoid objects and functions as your Effect’s dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them.