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:

  1. Run npm install use-immer to add Immer as a dependency

  2. Then replace import { useState } from 'react' with import { 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

  1. Group related state.

  2. Avoid contradictions in state.

  3. Avoid redundant state.

  4. Avoid duplication in state.

  5. 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:

  1. render everything but hide everything aside from the current one with CSS. This is slow and stupid.

  2. lift the state up.

  3. 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:

  1. theming,

  2. making the current account available,

  3. routing,

  4. 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:

  1. transforming data for rendering,

  2. 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.