This page is a primary resource for the Quick, React! tutorial. Unlike many examples of React code online, this page takes advantage of modern JavaScript coding style, functional components, and React Hooks.

React starting points

Modern React defines a lot of code under the hood so that you, the developer, only need to understand a few basic concepts to get started. Don't worry about the details of the JavaScript examples for now.

Build components not pages

Traditional web sites have multiple pages and links.In React, you build a Single Page App (SPA) by defining one page as a set of nested HTML components. As users interact with the page, JavaScript adds (mounts) and removes (unmounts) components.

A component looks like this:

const App = () => (
  <div>
    Hello, World! It's { new Date().toLocaleTimeString() }!
  </div>
);

React implements a special syntax called JSX to let you write HTML directly in JavaScript.

The root component is normally named App. The pre-defined index.html file loads the pre-defined index.jsx file:

<body>
  <div id="root"></div>
  <script type="module" src="/src/index.jsx"></script>
</body>

index.jsx loads and renders the App component:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
  <App />
  </React.StrictMode>
);

Components are just functions

A component is just a function that returns HTML, using JSX. That means you can keep components simple by defining subcomponents. For example, you could refactor the HTML for "Hello, World!" into its own component.

const Hello = () => (
<div>
  Hello, World! It's { new Date().toLocaleTimeString() }!
<div>

const App = () => (
  <div>
    <Hello />
  </div>
);

Components must be capitalized to be properly recognized by React.

All HTML tags in JSX must be closed. <Hello /> is shorthand for <Hello></Hello>.

Do not use the old-style React.Component class.

Components are called with properties

Data is passed to components with HTML attributes. The attributes are collected into a props object. For example, in this code

const Banner = ({title}) => (
<h1>
  { title }
</h1>

const App = () => (
  <div>
    <Banner title="Welcome to React" />
  </div>
);

calling App will cause Banner to be called with the object

{
  title: "Welcome to React"
}

The parameter list for Banner uses destructuring assignment to set the variable title to the argument "Welcome to React".

Change state, not HTML

In React you do not change a web page by changing its HTML. Instead, when something needs to change, you update the value of a state variable you have created. This causes React to re-run your components. This is called rendering.

const App = () => {
  const [on, setOn] = useState(false);
  return (
    <div className="App">
      <button onClick={() => setOn(!on)}>
        {on ? 'On': 'Off'}
      </button>
    </div>
  );
};

Modularization

In React files, you do not load your scripts with <script src="...." />. You use import. At the very least, you must import React itself. If you moved the Hello code into a file called Hello.js, then you could write App.js like this:

import React from 'react';
import Hello from 'Hello';

const App = () => (
  <div>
    <Hello />
  </div>
);

export default App;

Importing assets

React apps can also use import to manage static assets such as CSS and image files. This line:

import 'rbx/index.css';

includes the stylesheet node_modules/rbx/index.css in the app npm run build creates. The line

import dogImage from 'images/rintintin.jpg';

includes the image file src/images/rintintin.jpg in your build. It also sets dogImage to the correct URL to use to load that image, like this

<p><img src={ dogImage } float="left" />One famous dog in cinema was Rin Tin Tin.<p>

More on static assets.


JSX Syntax

React lets you embed HTML inside JavaScript. This is called JSX syntax. The scripts for npm run start and npm run build replace all JSX with regular JavaScript code that builds the desired HTML.

More on JSX.

There are a few rules to keep in mind when writing JSX.

Expressions in JSX are limited

Curly braces in JSX can contain a single expression. a statement, such as for or if statement, are not allowed.

The expression must return a primitive, a JSX component, or a list of components. Anything else will cause a build-time error.

Some HTML attribute names change

HTML event handling attributes attributes must be camelCased, e.g., onclick must be written onClick.

The class attribute is changed to className and the for attribute is htmlFor, because class and for are reserved words in JavaScript.

JSX expressions should be simple

Avoid putting very complex expressions inside JSX. Do complex logic outside the JSX, store results in const variables, and use the variables in the JSX. That's easier to read and debug.

Put parentheses around multi-line JSX forms.

All elements must be closed

The following is not legal JSX:

const App = () =>  (
  <Banner title={ schedule.title }>
);

You need to write it like this:

const App = () =>  (
  <Banner title={ schedule.title } />
);

Lists of components need keys

This code to generate a list of buttons runs but gets a warning:

const SizePicker = ({ sizes, selectSize }) => (
  sizes.map(size => (
    <button>{() => selectSize(size)}</button>
  ))
);

In order to efficiently update just the changed items in dynamic lists of components, React requires each component have a unique and stable key attribute, like this:

const SizePicker = ({ selectSize }) => (
  sizes.map(size => (
    <button key={size}>{() => selectSize(size)}</button>
  )
);

Unlike HTML id attributes, keys only need to be unique within the list, not over the whole page. Different lists can have the same keys as long as those keys are distinct within the list and fixed for each list item.

For more on list keys, see the React documentation and this discussion.

Don't nest component definitions

It is legal to define a component inside another component. Unfortunately, this means the inner component is re-defined and rendered every time the outer component is rendered, even when it doesn't need to be. This can cause flickering and odd effects with input elements.

For a good example of this pitfall, see Avoid Declaring React Components inside Parent Components.


React State

React was designed and refined to avoid common errors in managing the state of a user interface. Most of the challenges in learning React come from not understanding what state is and how it works. Here are some key concepts often misunderstood.

State: what you use to cause re-rendering

Most definitions of React state are surprisingly vague, such as "State is the place where the data comes from" . A bit closer is "'state' is an object that represents the parts of the app that can change" .

Here's a functional definition of what state means in React:

State variables hold data that can be changed to affect what is on-screen.

For example, to implement a shopping cart that a user can show and hide, you need a state variable, e.g., cartIsVisible. To implement a list of items that is loaded from an external source, you need a state variable, e.g., items.

Common things that need state

Visual feedback for user interface actions: a clicked checkbox, a selected menu item, text typed into a field, an open modal dialog box

Retrieved data: data read from a file, data retrieved from a database, data fetched from a third-party service

Temporal events: a timer update, a network event

State is created with useState()

You create state data for a component by calling the React function useState() directly, or some custom hook function that calls useState(). To call useState, inport it first:

import { useState } from 'react';

Call useState() at the top level of the component that needs the state, like this

const App = () =>{
  const [counter, setCounter] = useState(0);
  ...

This code assigns counter to the state's current value, and setCounter to that state's update function.

React state persists over renders

The most important property of

const [counter, setCounter] = useState(0);

is that calling it repeatedly creates a new state object only if it does not already exist.

The first time useState(0) is called, it creates a state object in an internal table, and initializes it to zero. Calling setCounter() modifies the stored state. When re-rendering calls useState(0) again, the stored state is returned.

React state is temporary

Although a state variable is not reset when a component is re-rendered, it is still just data stored in a local JavaScript data structure in the browser. Reloading the page with the code will reset all of JavaScript, including all React state.

If you have data you want to persist over page reloads, you need to (1) store that data somewhere else, and (2) load that data into a state variable using useEffect().

Pick initial state values carefully

Make sure your component function will run with initial value you pass to useState(). This code will break.

const App = () => {
  const [user, setUser] = useState(null);
  return (
    <h1>Hello, { user.name }!</h1>
};

user.name will blow up because user is null at the start. One possible solution is

const App = () => {
  const [user, setUser] = useState({null);
  const name = user ? user.name : 'guest';
  return (
    user ? <h1>Hello, { name }!</h1> : 
};

Create separate states for separate purposes

Don't put unrelated state in one object, like this:

const App = () => {
  const [data, setData] = useState({ name: '', score: 0 });
  ...
}

This complicates updating just the score, especiallly since date.score = 10; won't work.

Create separate states instead:

const App = () => {
  const [name, setName] = useState('');
  const [score, setScore] = useState(0);
  ...
}

Update a state with its update function

Example: here is a reusable button component that shows how many times it's been clicked.

Since the component needs to re-render whenever the number of clicks changes, you create a state variable clicks. The button's onClick handler calls setClicks to change the count.

State updates occur after rendering

Consider a page with a button to track clicks on a Like button.

const App = () => {
  const [likes, setLikes] = useState(0);
  const like = () => {
    setLikes(likes + 1);
    // just testing what would be stored in a database
    console.log(`storing likes = ${likes}`);
  }

  return (
    <React.Fragment>
      <div>We have {likes} likes!</div>
      <button onClick={like} />
    </React.Fragment>
  );
};

Clicking the button calls like(), which in turn calls setLikes(likes + 1). You might expect that this means 1 will be printed (or stored) as the value of likes the first time the button is clicked, but in fact 0 is printed. Every subsequent click on the button will print one less than that actual count.

setLikes(likes + 1) does not change the variable likes! There's no way a JavaScript function could do that. It just stores likes + 1 in the internal table. likes still has whatever value useState(0) returned for it.

After React has rendered all components, it checks the internal table to see if any states have changed. If so, then React re-renders. That is when the new value of likes will be returned and diplayed.

Don't mutate states directly

Do not modify state values directly, e.g., with push(). This small CodeSandbox shows that this doesn't work.

items.push() fails because it never calls setItems(), so React doesn't know the state has changed.

Calling push() then setItems(items) fails because it just saves the array object that is already stored. There's no way React can tell that something has changed.

Only the third method, passing setItems() a copy of the old array plus a new item works. Now React has a new value that is different than the old value.

Use custom hooks from libraries

When faced with a problem that involves complex state, such as fetching data asynchronously, displaying a modal dialog box, or managing a user input form, look for a library that defines a custom hook to do the job. A custom hook. is a function that calls other hook functions and, usually, returns some state variables and/or state setting functions. Custom hooks have names that begin with use, e.g., useForm or useDatabase. This is so that code checking utilities like esLint can verify the rules of hooks, e.g., that all hook functions are only called only inside a component or another hook function.

Though custom hooks can return anything, but most return either an array of values or an object. Look at the documentation carefully to see which.

Destructure hook return values

Use array-destructuring for hooks that return arrays. For example, the useDebounce() hook takes a state variable, and returns an array of another state variable that will have the value of the first variable, but only after that value has remained unchanged for some number of milliseconds.

const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);

Use object-destructuring for hooks that return objects. For example, useQuery in React Query returns an object with over 20 fields, describing many possible situations an asynchronous attempt to fetch data might be in, but a typical call uses just a few:

const { data, isLoading, error  } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchToDoList
});

Returning objects makes it easy to ignore returned values, but requires additional code to specify different local variable names, e.g.,

const { todos: data, isLoading, error  } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchToDoList
});

Side-effects in React

Side effects are a common source of hard-to-debug problems. They are even worse in web applications with multiple components and asynchronous user and network events. React Hooks were designed to manage side effects better.

Side-effect code is any code that interacts with objects created outside the scope of a function, such as database updates, network calls, mutations of arrays and structures passed into the function, and so on.

Examples of side effects

Changing the value of a state variable is a side effect, such as updating a counter created with useState().

Interacting with the user is a side effect, such as showing a modal dialog.

Changing something outside a component is a side effect, such as setting document.title, or saving something in localStorage.

Calling a network service is a side effect, such as getting data from Google Maps or Yelp, or subscribing to data from Firebase.

Avoid side effects during rendering

React renders your user interface by running your app component functions to generate HTML. It does this many times, whenever any state changes.

You only want to do such side effects when relevant state data has changed, not every time a component is re-rendered. Most side effects should be done in event handlers, e.g., when the user clicks a button.

Side effects inside user event handlers are fine

A side effect inside code that responds to user actions such as onClick is fine. The side effect will not occur during rendering, so it does not need to be put inside a useEffect().

For example, to have a button that turns on and off:

const App = () => {
  const [on, setOn] = useState(false);
  return (
    <div>
      <p>The light is { on ? 'On' : 'Off' }.</p>
      <button onClick={() => setOn(prev => !prev)}>
        { on ? 'Off' : 'On' }
      </button>
    </div>
  );
};

Put setup actions inside useEffect()

Sometimes there are actions that need to be taken when an application is started, or a component is mounted on a page. The React hook function useEffect() lets you specify code to run only at certain times, like when a component is mounted, unmounted, or when specific variables change value. useEffect() is called like this:

useEffect(() => { code }, [var1, var2, ... ])

This tells React to do several things:

  • Run the code whenever the component in which this useEffect() appears is mounted.
  • Run the code whenever any of the variables in the second argument changes value.
  • If the code returns a function of no arguments, run that function when the component is unmounted.

React runs the code passed to useEffect() after all rendering has finished . This is to avoid side effects from happening during page rendering. After React has run all useEffect() functions, if any state variables have changed, it will do another round of rendering.

Here is a minimal made-up example that fetches a list of books from some URL, using useEffect(). There is no error checking, in order to focus on useEffect().

const Library = ({url}) => {
  const [ books, setBooks ] = useState([]);
  useEffect(() => {
    const getBooks = async () => {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
    }
    getBooks();
  }, [url]);
  return <BookList books={books />}
}

When Library is first rendered, the function passed to useEffect() would be stored but not run, until after BookList was rendered with an empty list. Then the asynchronous code to fetch data would be called, eventually setting the state variable and triggering a re-render. The function passed to useEffect() does not return anything to do when Library is unmounted. If Library is re-rendered, the useEffect() function will only be stored and called again if the URL has changed.

The best way to fetch data is to use a library like react-query. They use useEffect() internally but handle many details such as error checking and caching results to reduce network traffic.

Specify dependencies to avoid repeated useEffect() calls

If you do not provide a second argument to useEffect(), React will call the function passed on every render. This is rarely what you want. If that function calls a cloud database, it can trigger so many calls that your app gets banned!

The second argument to useEffect() should list every variable declared by the component that is used in the function. This tells React to call the function only when at least one of those variables has changed value.

If the second argument is an empty array, React will call the function just once, when the component is first used.

The ESLint plugin for React Hooks will warn you if you don't include the correct variables.

Don't put useEffect() inside a conditional

React keeps track of states and effects based on the order in which useState() and useEffect() are called. Therefore those functions need to be called consistently on every rendering cycle.

For example, the following code is bad because sometimes the useEffect() is not called.

const App = () => {
  const [user, setUser] = useState(null);
  if (user) {
    useEffect(() => {
      document.title = user.name;
    });
  }
  return (
    <h1>Hello, { name }!</h1>
  )
};

The ESLint plugin for React Hooks will catch this error.

Nest async function calls in useEffect()

await can only be used in async functions, but async functions can not be passed directly to useEffect(). Instead define a normal function that calls your async/await code.

Example: here's a basic version of code to asynchronously load JSON from a URL and store it in a state variable.

const [data, setData] = useState();
useEffect(() => {
  const loadData = async () => {
    const response = await fetch(url);
    const json = await response.json();
    setData(json);
  }

  loadData();
}, [url]);

This code works but doesn't handle errors, delays, or cancelations.

Capture errors in state

The basic code to fetch data doesn't handle delays or errors in fetching data. If you want the app to display information about such issues, create state variables for them:

const [loading, setLoading] = useState(false);
const [errorText, setErrorText] = useState(null);
const [data, setData] = useState(null);

useEffect(() => {
  const loadData = async () => {
    setError(null);
    setLoading(true);
    try {
      const response = await fetch(url);
      if (!response.ok) {
        setErrorText(response.statusText);
        return;
      }
      const json = await response.json();
      setLoading(false);
      setData(json);
    }
    catch (e) {
      setErrorText(error.message);
    }
  };

  loadData();
}, [url]);

Use a cleanup function to cancel actions

The basic code to fetch data doesn't handle the fairly common case where a request for data should be canceled because the user has chosen to do something else.

You can cancel asynchronous state updates by having a local variable that says whether the component is still active. You set this variable by returning a cleanup function from useEffect(). React runs functions returned by useEffect() when the component is removed from the current page. Anything can go in these cleanup functions.

The cleanup function sets a flag that can be checked to see if the component is still active. The cleanup function is only called when the component is removed.

const [data, setData] = useState({});
useEffect(() => {
  const active = true;
  const loadData = async () => {
    const response = await fetch(url);
    const json = await response.json();
    if (active) setData(json);
  }

  loadData();
  return () => { active = false; };
}, [url]);

Use cleanup functions to cancel subscriptions

The Firebase RealTime Database lets you subscribe to changes in a cloud-based JSON object. A subscription is a bit of JavaScript code that listens for changes to the database. It's something you want to stop doing if the relevant component is no longer active. You can do this by canceling the listener in a cleanup function:

const [data, setData] = useState(null);
const [errorText, setErrorText] = useState(null)
useEffect(() => {
  const handleData = (snap) => {
    if (snap.val()) 
      setData(snap.val());
  }
  db.on('value', handleData, error => setErrorText(error.message));
  return () => { db.off('value', handleData); };
}, []);

© 2024 Chris Riesbeck
Template design by Andreas Viklund