The key to implementing web apps -- web pages that behave like applications -- is dynamic HTML, i.e., HTML on a web page that changes as the user interacts with the page. For very simple cases, all you need is basic JavaScript, but it became obvious years ago that more complicated applications need a more structured approach to manage complexity. Most approaches use some variant of model-view-controller (MVC). The model is a data object describing the state of the application: what data has been entered, retrieved, or calculated. The view is the HTML that displays that data. The controller is the collection of event handlers and HTML objects that a user can interact with.
React's solution is a specific set of concepts for rendering the view and managing the model. Understanding these concepts is critical for effective React programming.
An app is collection of components not pages
Traditional web sites have multiple pages and links that connect them. In classic 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.
To do this, the standard setup for a React app starts with a boilerplate index.html file that loads a boilerplate JavaScript file that in turn runs your React code to get the HTML to install in the web page, for example:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html>
import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode>, )
The details of this code will vary depending on what React libraries you are using and whether you are using TypeScript.
Components are just functions
A component is just a function that returns HTML. That means you can keep components simple by defining subcomponents. React implements a special syntax called JSX to let you write HTML directly in JavaScript. Here's a toy example:
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. Here's an example.
const Banner = ({title}) => (
<h1>
{ title }
</h1>
);
const App = () => (
<div>
<Banner title="Welcome to React" />
</div>
);
When App calls Banner, Banner is passed the props object
{
title: "Welcome to React"
}
The parameter list for Banner uses
destructuring assignment
to set the variable
title to the argument "Welcome to React"
.
The Rules of React
Components should be "pure". In functional programming, a pure function has no side effects and will return the same result given the same arguments, no matter how often it is called. In React, a component is pure if
- There are no side effects in the code called to generate HTML
- The same HTML is returned for the same props
Any changes in the state of the app and any side effects involving systems outside the app should be done via hooks such as useState() and useEffect(), described below.
Modularization
Because components are code, they can be refactored into separate files and imported, for more modular development.
In React files, you do not load scripts with <script src="...." />. You use import. For example, the Hello component can be in the file /src/Hello.tsx and imported into App.tsx, like this:
const Hello = () => ( <div> Hello, World! It's { new Date().toLocaleTimeString() }! <div> ); export default Hello;
import Hello from 'Hello'; const App = () => ( <div> <Hello /> </div> ); export default App;
Use .tsx for files with JSX in TypeScript, or .jsx for JSX in JavaScript.
Importing assets
React apps can also use import to manage static assets such as CSS and image files. This line:
import './index.css';
includes a stylesheet. This line
import dogImage from './images/rintintin.jpg';
includes the image file ./images/rintintin.jpg and sets dogImage to the correct URL to use to load that image in code 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> ) );
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.
Change state, not HTML
In React you do not directly modify the HTML on a web page. Instead, when something needs to change, you create a state variable to hold the relevant data, and update that state. This causes React to re-run, i.e., re-render, your components.
const App = () => { const [on, setOn] = useState(false); return ( <div className="App"> <button onClick={() => setOn(!on)}> {on ? 'On': 'Off'} </button> </div> ); };
In model-view-controller terms:
- The state variable on is the model.
- The code that displays "On" or "Off" depending on on is the view.
- The button event handler that calls setOn() is the controller.
State: what you use to cause re-rendering
Definitions of React state range from vague to confusing, e.g., "State represents the value of a dynamic properties of a React component at a given instance." .
Here's a practical definition of what state means in React:
State variables hold changeable data that affects what is on-screen.
For example, the list of items in a shopping cart is data that appears on screen, so that list would be stored in a state variable. If the shopping cart can be showing or hidden, that's another bit of data to store in a state variable. The items a user can shop for that has been downloaded to display would be another state variable,
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
Components create state variables
Each component in React manages its own bits of state. You create state data for a component by calling the React function useState() directly, or some custom hook function that internally calls useState(). A simple example is a counter:
import { useState } from 'react'; const Counter = () => { const [ count, setCount ] = useState(0); return ( <button onClick={() => setCount(count => count + 1)} className="m-32 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> Click count: {count} </button> ); }; export default Counter;
useState() returns a tuple of two elements: a state variable reference and a function to call to reset the state. This syntax lets you name the variable and setter any way you want.
There are two ways to call the setter function. You can pass it a function that returns the new value, based on the previous value. That's what is used here. If the new value is unrelated to the old value, then you can instead just pass the expression for the new value.
When React state persists, when it doesn't
The most important property of
const [counter, setCounter] = useState(0);
is that re-rendering the component do not reset the state variable. An internal table of state variables for each component is kept. 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 disappears on reload or unmount
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. If the component is unmounted (removed from the page HTML) for any reason and later remounted, a new state is created. Unmounting and remounting happens when the user reloads the page or navigates to another screen and back in a router-based app.
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.
Never update state in rendering code
Consider the following code intended to display a button with a random number between 1 and 100:
import { useState } from 'react'; const RandomCounter = () => { const [ count, setCount ] = useState(0); setCount(Math.floor(1 + Math.random() * 100)); return ( <button onClick={() => setCount(Math.floor(1 + Math.random() * 100)} className="m-32 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> Your Lucky Number is {count} -- click for a new one! </button> ); }; export default RandomCounter;
When RandomCounter is rendered:
- count is set to 0
- setCount() is called with a random number, e.g., 62
- the button is rendered with the value of count, i.e., 1
- React sees that state has changed so it re-runs RandomCounter
- count is set to 62
- setCount() is called with a new random number
- the button is rendered with 62
- React sees the state has changed so it re-runs RandomCounter
- ...
When something like this happens in a component that fetches data from an external source like Firebase or Spotify, 1000s of network calls happen immediately and your account is disabled!
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 => likes + 1); // just testing what would be stored in a database console.log(`storing likes = ${likes}`); } return ( <div> <div>We have {likes} likes!</div> <button onClick={like} /> </div> ); };
Clicking the button calls like(), which in turn calls setLikes(). You might expect that this means 1 will be printed 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. See Why React doesn't update state immedately.
setLikes() does not change the variable likes! There's no way a JavaScript function call can do that. It just stores likes + 1 in the internal table.
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. Returning objects makes it easy to ignore returned values, but a little trickier to use a different local variable names. E.g., if useQuery() returns an object with the key data, but you want a better name, e.g., players, then rename when destructuring like this:
const { players: data, ... } = useQuery(...);
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
Here are some common side-effects to avoid in component rendering:
- 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 do this. Here is a minimal example with no error-checking that fetches a list of quotes from DummyJson.
import { useEffect, useState } from "react"; interface Quote { id: number; author: string; quote: string; } const Quotes = ( ) => { !! const [quotes, setQuotes] = useState<Quote[]>([]); !! useEffect(() => { const getQuotes = async () => { const response = await fetch('https://dummyjson.com/quotes'); if (!response.ok) return; const json = await response.json(); if (json?.quotes) setQuotes(json.quotes); } getQuotes(); !! }, []); return ( <ul className="m-8"> { quotes.map(quote => ( <li key={quote?.id}>{quote?.quote}<br />—{quote?.author}</li> )) } </ul> ) }; export default Quotes;
When Quotes is first rendered, a local state variable is created with a empty list of quotes, the setup function passed to useEffect() is stored but not run, and the HTML is rendered. When all rendering as finished, React runs the setup function. In this case, the setup is asynchronous code that gets a list of quotes. When and if a list is fetched, the setup code sets the state variable, triggering a re-render, displaying the quotes.
useEffect() takes an optional second argument: a list of variables called the dependency array. React will call the setup function whenever any variable in the dependency array changes. If the dependency array is empty, as it is here, then the function is called just once when the component is mounted.
The above is not good code. For better handling of errors and loading, see Fetching JSON data. More complex, but even better, is to use a library like Tanstack Query.
useEffect() mistakes
There are three common pitfalls with useEffect().
- Setting a state variable in the setup code that modifies a variable in the dependency array. This will cause an endless loop!
- Omitting the dependency array. This is not the same as an empty array. Instead it means "run the setup code on every render". This is almost never what you want.
- Omitting a variable whose value affects the setup code. This means that the setup code won't run when it should. ESLint will normally warn you about missing dependencies.
Don't call hooks conditionally
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.