[REACT](https://www.kirupa.com/react/index.htm) [BOOK](https://www.amazon.com/exec/obidos/ASIN/0134546318/kirupacom) # Keeping Your UI Responsive by [ kirupa](https://www.kirupa.com/me/index.htm) | filed under [Learning React](https://www.kirupa.com/react/index.htm) There is a particular kind of slow that has nothing to do with how long something takes. You type a letter into a search box, and the letter doesn't show up. You click a tab, and nothing seems to happen for a moment. Your app eventually does the right thing, but it felt broken while it was doing it. Users judge an app by that feeling far more than they judge it by a stopwatch. In the last tutorial, we looked at [avoiding unnecessary renders](https://www.kirupa.com/react/avoiding_unnecessary_renders.htm), which is about doing less work. In this tutorial, we'll look at the bigger picture. We'll see why a React app stops responding to people, how to measure it, and what to reach for when it does. We'll build a deliberately slow list and fix it with `useDeferredValue` and `useTransition`. Then we'll look at two other tools: splitting your code so that less of it loads up front, and virtualizing a huge list so that only the rows you can see exist. Onwards! ## Why a UI Feels Janky Your browser has one main thread. That single thread runs your JavaScript, calculates styles, works out the layout, handles clicks and key presses, and paints pixels to the screen. It can only do one of those things at a time. While it is busy with one task, everything else waits in line. A screen refreshes about 60 times per second, which gives the browser roughly 16 milliseconds to produce each frame. If a task takes longer than that, the browser can't draw the next frame on time. If the task takes a few hundred milliseconds, you can feel it. If the user pressed a key during that time, the key press sits in line, and the letter doesn't appear until the task finishes. That's the lag you feel as a person. React makes this easy to run into because of how rendering works. When you update state, React calls your components to figure out what the screen should look like, compares the result to what it had before, and then updates the page. All of that happens on the main thread. If rendering one update takes 800 milliseconds, then for 800 milliseconds nothing else can happen. Not even typing. React 18 and later give us a way out of this. They let us tell React that some updates are **urgent** (like typing into a field) and some are not (like re-rendering a big list based on what was typed). React can then work on the non-urgent update in small slices, and it can pause or even throw away that work if something urgent comes along. The diagram below shows the difference: ![Two timelines. In the first, every update is urgent and the whole list render blocks typing. In the second, typing is urgent and the list render is a transition that runs in small slices between key presses.](https://www.kirupa.com/react/images/responsive_main_thread_144.png) In the top lane, every update is treated as urgent. The key press has to wait for the whole list to render, and the next key press has to wait for the next render after that. In the bottom lane, typing gets to go first, and the list is rendered in the gaps. The list takes about the same total amount of work, but the input never feels stuck. Keep this picture in mind, because everything in this tutorial is a variation on it. ## Measure Before You Fix Before we fix anything, a word of caution. Most React apps are fast enough, and the ones that aren't are usually slow in one or two specific places. If you start sprinkling the tools from this tutorial around your code without knowing where the slowness is, you'll make your code harder to read and you may not make your app any faster. So measure first. You have two main tools for this. The first is the **Profiler** tab in the React Developer Tools extension, which we used in the [previous tutorial](https://www.kirupa.com/react/avoiding_unnecessary_renders.htm). It tells you which components rendered during a recording, how long each one took, and why it rendered. When the question is "which of my components is expensive?", this is where you look. The second is the **Performance** panel built into your browser's developer tools. Press record, do the thing that feels slow, and stop. You'll see a timeline of everything the main thread did. Tasks that take longer than 50 milliseconds are flagged as long tasks, and those are the ones standing between your user and a responsive page. When the question is "why did this interaction feel slow?", this is where you look. If you want a number to aim for, Google's guidance for a good *Interaction to Next Paint* score (a measure of how long it takes your page to respond after a click or a key press) is 200 milliseconds or less. One more thing to keep in mind. Your app runs slower in development than it does in production, partly because of checks like `StrictMode` rendering your components twice. Measure with `npm run build` followed by `npm run preview` when you can. The slow example we are about to build is slow on purpose, so you will feel it either way. ## Building a Slow List Our example is an app called Animal Finder. It has a text field, and below it is a list of 1,600 animals that gets filtered as you type. Nothing about the list is difficult to render, but we are going to make every row cost half a millisecond. That's a stand-in for the kind of real-world row that has some formatting, a few icons, and maybe a calculation or two. Create a new project with Vite. If you need a refresher, the [Setting Up Your React Dev Environment Easily](https://www.kirupa.com/react/setting_up_react_environment.htm) tutorial walks through it: ```bash npm create vite@latest animalfinder -- --template react cd animalfinder npm install ``` Delete the **src/App.css** file and the **src/assets** folder, as we won't need them. Next, we need some data. Create a file called **items.js** inside the **src** folder and add the following: ```js const adjectives = [ "red", "quick", "lazy", "bright", "quiet", "brave", "calm", "dizzy", "eager", "fancy", "gentle", "happy", "jolly", "kind", "lucky", "merry", "nimble", "odd", "proud", "rusty", "shiny", "tiny", "unique", "witty", "zany", "sleepy", "fuzzy", "mighty", "cosmic", "salty", "velvet", "wobbly", "crispy", "sunny", "stormy", "dusty", "golden", "breezy", "cheeky", "snappy", ]; const nouns = [ "otter", "badger", "falcon", "panda", "walrus", "gecko", "heron", "lynx", "marmot", "newt", "osprey", "puffin", "quokka", "raven", "sloth", "toucan", "urchin", "vole", "wombat", "yak", "zebra", "beetle", "cobra", "dingo", "ferret", "gopher", "hedgehog", "iguana", "jackal", "koala", "llama", "moose", "narwhal", "ocelot", "parrot", "quail", "robin", "stoat", "tapir", "viper", ]; export const items = adjectives.flatMap((adjective) => nouns.map((noun) => `${adjective} ${noun}`) ); ``` We have 40 adjectives and 40 nouns, and the `flatMap` pairs every adjective with every noun. That gives us 1,600 strings like `"red otter"` and `"lazy walrus"`. Now replace the contents of **index.css** with some basic styles. The Vite template's **main.jsx** already imports this file, so there is nothing to wire up: ```css body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; color: #333; } .app { max-width: 420px; margin: 0 auto; } input { width: 100%; box-sizing: border-box; padding: 10px 12px; font-size: 18px; border: 2px solid #ccc; border-radius: 6px; } input:focus { border-color: #0099ff; outline: none; } ul { padding-left: 20px; } .list { transition: opacity 0.2s; } .list.pending { opacity: 0.5; } ``` The last two rules aren't used yet. They'll come in handy in a few minutes. Finally, replace the contents of **App.jsx** with the following: ```jsx import { useState } from "react"; import { items } from "./items"; function wasteTime(ms) { const start = performance.now(); while (performance.now() - start < ms) { // do nothing, on purpose } } function SlowItem({ text }) { wasteTime(0.5); return
  • {text}
  • ; } function App() { const [query, setQuery] = useState(""); const matches = items.filter((item) => item.includes(query.toLowerCase())); return (

    Animal Finder

    setQuery(e.target.value)} placeholder="Type to filter..." aria-label="Filter animals" />

    {matches.length} matches

    ); } export default App; ``` The highlighted lines are the slow part. The `wasteTime` function sits in a loop and does nothing until the number of milliseconds we gave it has passed. The `SlowItem` component calls it, which means that every row in our list takes at least half a millisecond to render. We keep the timing code in a helper function outside the component because a component is supposed to be predictable, and reading the clock inside one is the sort of thing that the linter in the Vite template will warn you about. Everything else is the kind of code you have written many times by now: a piece of state for the text, a filtered array, and a list. Run `npm run dev` and open the page. Click in the text field and type the word **red**. Here is what you'll see after typing a few letters: ![The Animal Finder app with the word lazy typed into the text field, showing 40 matches and the start of a list of lazy animals.](https://www.kirupa.com/react/images/responsive_animal_finder_144.png) It works, but typing in it is miserable. Each letter you type takes a noticeable moment to show up, and if you type quickly, the letters arrive in clumps. On my machine, in development mode, a single key press that matches most of the list took close to a second to finish. Your numbers will differ, because they depend on your computer, but you'll feel the lag. This is the top lane of our diagram. The reason is the one we talked about earlier. When you type a letter, `setQuery` runs, React re-renders `App`, and that renders every matching `SlowItem`. The text field's new value can't be put on the screen until all of that is done. The text field and the list are tied to the same update, so the cheap thing (showing a letter) gets held hostage by the expensive thing (rendering the rows). ## Deferring the Expensive Part What we want is for the text field to update right away, and for the list to catch up when it can. The first tool for this is `useDeferredValue`. You hand it a value, and it gives you back a copy of that value that is allowed to lag behind. When the original value changes, React first re-renders with the old copy, which is cheap, so the urgent part of the screen updates immediately. Then React tries again in the background with the new copy. If the user types another letter before that background render finishes, React abandons it and starts over with the latest value. Here is **App.jsx** with that change. Pay attention to the highlighted lines, because there are more changes than you might expect: ```jsx import { memo, useDeferredValue, useState } from "react"; import { items } from "./items"; function wasteTime(ms) { const start = performance.now(); while (performance.now() - start < ms) { // do nothing, on purpose } } function SlowItem({ text }) { wasteTime(0.5); return
  • {text}
  • ; } const ItemList = memo(function ItemList({ query }) { const matches = items.filter((item) => item.includes(query.toLowerCase())); return ( <>

    {matches.length} matches

    ); }); function App() { const [query, setQuery] = useState(""); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return (

    Animal Finder

    setQuery(e.target.value)} placeholder="Type to filter..." aria-label="Filter animals" />
    ); } export default App; ``` Let's go through what changed. We moved the filtering and the list into their own component called `ItemList`, which takes the query as a prop. In `App`, we call `useDeferredValue(query)` to get `deferredQuery`, and we pass the deferred one to the list. The text field still uses the real `query`, so it always shows exactly what you typed. We also compare the two values. When they differ, the list is showing results for an older query, and we use that to fade the list with the `pending` class we added to the stylesheet earlier. That small bit of feedback tells people that something is on its way. Now type **red** again. The letters show up as fast as you can type them, the list fades while React catches up, and then the results snap in. That's the bottom lane of our diagram. ### The memo Gotcha You probably noticed that we wrapped `ItemList` in `memo`. That isn't decoration, and it's the part of this technique that catches people off guard. To see why, think about the order of events. When you type a letter, React first re-renders `App` with the new `query` and the *old* `deferredQuery`. That's the urgent render. Without `memo`, `ItemList` re-renders as part of it, because its parent rendered. It receives the same props as before, but React doesn't know that it can skip it. So the slow list renders on the urgent pass, and then renders a second time on the background pass with the new value. I tried this with `memo` removed to see how bad it would be. Typing the same word was *worse* than the version we started with. With `memo` in place, the same typing finished in a fraction of the time, and the longest delay for any key press stayed within about a frame on my machine. The first version was close to a second. That's a very big difference from one function call. What `memo` does is tell React to skip rendering a component when its props are the same as last time. We covered it in [Avoiding Unnecessary Renders in React](https://www.kirupa.com/react/avoiding_unnecessary_renders.htm). During the urgent pass, `deferredQuery` hasn't changed yet, so `ItemList` gets identical props and React skips it. Only on the background pass, when the deferred value catches up, does the list re-render. So the rule is simple: if you defer a value, make sure the component that receives it is memoized. If you are using the React Compiler, it can add this kind of memoization for you, but it's still worth knowing why it needs to be there. ## Marking Updates with useTransition `useDeferredValue` is nice when you receive a value and want a lagging copy of it. There's a second tool for when you control the state update yourself. It's called `useTransition`, and it lets you wrap a state update to say, "This one isn't urgent." The hook returns two things: a function named `startTransition`, and a boolean named `isPending` that is `true` while the transition is still working. To use it for our example, we need to split our one piece of state into two. One piece, `query`, drives the text field and is always updated right away. The other, `listQuery`, drives the list and is updated inside a transition. Change the import at the top of **App.jsx** so that it reads `import { memo, useState, useTransition } from "react";` and then replace the `App` function with this one. The `SlowItem` and `ItemList` components stay exactly the same: ```jsx function App() { const [query, setQuery] = useState(""); const [listQuery, setListQuery] = useState(""); const [isPending, startTransition] = useTransition(); function handleChange(e) { const value = e.target.value; setQuery(value); startTransition(() => { setListQuery(value); }); } return (

    Animal Finder

    ); } ``` In `handleChange`, the first `setQuery` call is a normal update, so the text field stays in sync with the keyboard. The second update, `setListQuery`, sits inside `startTransition`, so React treats it as low priority, renders it in the background, and interrupts it if another key press comes in. We use `isPending` to fade the list in the meantime. The behavior is almost identical to the `useDeferredValue` version, and in my testing, the numbers were too. Notice that `memo` is still doing the same job. If `ItemList` weren't memoized, it would re-render on the urgent update because `App` re-rendered, even though `listQuery` hasn't changed yet. This tripped me up the first time, so it's worth repeating: transitions and deferred values decide *when* a render happens. They don't decide *whether* a component renders. That's still memo's job. As for which one to use, here is my rule of thumb. If you own the line of code that calls the state setter, as we do in `handleChange`, use `useTransition`. If the value comes from somewhere you can't wrap, like a prop handed down from a parent or the result of a hook, use `useDeferredValue`. Both end up in the same place. ## What Transitions Don't Do It's easy to read the last two sections and think that transitions make slow things fast. They don't. The list took the same amount of work to render before and after. We changed *when* that work happens and who has to wait for it. The text field no longer has to wait, but the list still does. A few other things are worth knowing: - **If an update is slow, it's still slow.** On a very slow device, the list will take longer to appear than it did before, because React keeps yielding to the user's typing. If the list is expensive enough that people are staring at a faded screen for seconds, you need to make the work smaller (with a technique like virtualization, which we'll get to) and not just reorder it. - **Transitions are for state updates.** You can't wrap a text input's own `value` in one. A controlled input has to update immediately, or you'll get a field that ignores your keystrokes. That's why we split our state into two pieces. - **Don't wrap everything.** A transition makes updates interruptible, and that has a cost. Use it where there's a real difference between urgent and non-urgent work. - **Use `startTransition` without the hook when you don't need `isPending`.** React also exports a plain `startTransition` function that you can import and call from anywhere, such as inside a library or an event handler that doesn't need to show a pending state. ## Transitions and Suspense Transitions have a second job, and it's a very useful one. In the [Suspense and Error Boundaries](https://www.kirupa.com/react/suspense_and_error_boundaries.htm) tutorial, we saw that a component can suspend while it waits for code or data, and that the nearest `Suspense` boundary shows a fallback in its place. That's great the first time a page loads. It's less great when the user clicks a tab, and the content they were reading is swapped for a spinner. I built a small version of that to try it. It has two tabs, About and Reports. The Reports component is loaded with `lazy`, and I delayed it by one second to simulate a slow network. When the tab change was a regular state update, clicking Reports replaced the About content with the "Loading..." fallback right away. When I wrapped the same `setTab` call in `startTransition`, the About content stayed on screen, the Reports button changed to read "Reports (loading...)" using `isPending`, and the new tab appeared after the second was up. No spinner, no flash of empty content. This is the same idea as the text field. React is being told that the tab change isn't urgent, so it keeps showing what you have while the new screen gets ready in the background. The Suspense tutorial has the full code for this, so we won't repeat it here. When you build navigation in your own apps, remember to reach for a transition so that your users don't see a loading state that they didn't need to see. ## Loading Less Code So far, we have been dealing with slowness at the moment of interaction. There's another kind of slowness that hits earlier, and that's how long it takes for your app to show up at all. Every component, library, and helper function that you import ends up in a bundle of JavaScript that the browser has to download, parse, and run before your app does anything. The more code you ship, the longer the wait. The trick is that your users don't need all of your code at the same time. If your app has a Reports page that 5% of users visit, then there's no reason to make the other 95% download it on their first visit. This is called **code splitting**, and React already gave us the tool for it in the Suspense tutorial: `lazy`. It takes a function that calls the dynamic `import()`, and your bundler (Vite, in our case) puts everything in that imported file into a separate chunk that is only downloaded when it's needed. The most natural place to split an app is at the route level. Each page is a self-contained chunk of the UI, and nobody sees it until they navigate to it. Let's see what this looks like with [React Router](https://www.kirupa.com/react/creating_single_page_app_react_using_react_router.htm). After installing it with `npm install react-router`, the **main.jsx** file wraps our app in a router: ```jsx import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App.jsx"; createRoot(document.getElementById("root")).render( ); ``` We'll have three pages. Create a **pages** folder inside **src**, and in it, make a file for each page. They are all tiny. For example, here's **Reports.jsx**: ```jsx export default function Reports() { return

    Reports

    ; } ``` Do the same for **Home.jsx** and **Settings.jsx**, changing the name and the heading. Each file needs to use a default export, because `lazy` expects one. Now for the interesting part, **App.jsx**: ```jsx import { lazy, Suspense } from "react"; import { NavLink, Route, Routes } from "react-router"; import Home from "./pages/Home.jsx"; const Reports = lazy(() => import("./pages/Reports.jsx")); const Settings = lazy(() => import("./pages/Settings.jsx")); function App() { return (
    Loading...

    }> } /> } /> } />
    ); } export default App; ``` The `Home` page is imported normally, because it's the first thing people see and we want it in the main bundle. `Reports` and `Settings` are loaded with `lazy`. A single `Suspense` boundary wraps the `Routes`, so that anything that's still loading shows the fallback in the content area, while the navigation links stay put. To see if it worked, run `npm run build`. Vite prints each file that it produced. This is what I got: ```bash dist/index.html 0.38 kB dist/assets/Reports-qGiE9Ah4.js 0.12 kB dist/assets/Settings-BCRbpIj5.js 0.12 kB dist/assets/index-D24_2194.js 258.25 kB | gzip: 82.15 kB ``` There it is. `Reports` and `Settings` each became their own file, separate from the main bundle. The letters and numbers in the file names are a hash that changes with the content, so yours will be different. Our pages are tiny, so the chunks are tiny too. In a real app, a page with charts or a rich text editor can be hundreds of kilobytes, and this is where you save the most. I also ran `npm run preview` and watched the Network tab. When the page first loaded, only the main bundle was requested. The **Reports** chunk showed up in the list the moment I clicked the Reports link, and not before. A couple of tips on where to split. Route-level splitting is the best first move because the boundaries are obvious. After that, look for big components that aren't visible on first load, such as modals, charts, editors, or anything behind a tab. Don't split small components. Each chunk costs a network request, and you'll make things slower if you split into dozens of tiny pieces. And remember what we learned in the last section: if a lazy component is something the user navigates to, wrap that navigation in a transition so they don't see a flash of fallback. ## Rendering Less with Virtualization Our slow list had 1,600 rows, and we've been trying to make rendering them feel better. A different way to look at the problem is to ask why we're rendering 1,600 rows at all. Nobody can see more than about a dozen of them at a time. That's the idea behind **virtualization** (also called windowing). Instead of rendering every row, you render only the ones that are inside the visible area, plus a few extra above and below. As the user scrolls, you swap in the rows that scrolled into view and remove the ones that left. A list with 10,000 items ends up with only a dozen or so DOM elements at any moment. The browser has far fewer elements to lay out and paint, and React has far fewer components to render. ![A tall list of 10,000 rows where only a small window of four rows, numbered 5000 to 5003, exists in the DOM. Rows before and after the window are not rendered.](https://www.kirupa.com/react/images/responsive_virtual_list_144.png) Building this by hand involves a fair amount of math, such as the position of each row, the total height of the scroll area, and which rows to show. This is a great job for a library. [TanStack Virtual](https://tanstack.com/virtual) is a popular choice that works with React and gives you a hook. Install it in a project: ```bash npm install @tanstack/react-virtual ``` Then replace the contents of **App.jsx** with this: ```jsx import { useRef } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; const rows = Array.from({ length: 10000 }, (_, i) => `Row number ${i + 1}`); function App() { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 36, }); return (
    {virtualizer.getVirtualItems().map((virtualRow) => (
    {rows[virtualRow.index]}
    ))}
    ); } export default App; ``` Let's break this down. We have an array of 10,000 strings. The `useVirtualizer` hook needs to know how many items there are (`count`), which element does the scrolling (`getScrollElement`), and a guess at how tall each row is (`estimateSize`). It gives us back a `virtualizer` object. The markup has three layers. The outer `div` is the scroll container, with a fixed height of 400 pixels. Inside it is a tall, empty `div` whose height is set to `virtualizer.getTotalSize()`, which is the combined height of all 10,000 rows. That's what gives the scrollbar the right size. Finally, `getVirtualItems()` returns only the rows that should currently exist, and we position each one absolutely using the `start` value that the library calculated. Run the app and scroll around. It's smooth, even though we have 10,000 rows. When I looked at the page with the browser's element inspector, only 13 or 14 row elements were in the DOM at once, and dragging the scrollbar to the middle landed on "Row number 5000" as expected. Compare that to rendering 10,000 elements in a plain list, and you can see why this works. Virtualization isn't free, though. A few things to keep in mind: - **Rows that don't exist can't be found.** The browser's find-in-page feature only sees what's in the DOM, so searching for a row you haven't scrolled to won't work. For something like a data table, you may want your own search box. - **Accessibility needs attention.** Screen readers only see the rendered rows, so test with one and add the appropriate ARIA attributes if the list is an important part of your app. - **Row heights matter.** Fixed-height rows are the easiest to get right. Variable-height rows work, but you have to let the library measure them. - **It's more code.** Don't reach for this until you have a list that's big enough to need it. A few hundred simple rows are usually fine without any of it. One more note. If you run the linter that comes with the Vite template, it will print an informational message that `useVirtualizer` returns functions that the React Compiler can't safely memoize. That's expected for this particular library. It means the compiler will skip optimizing that one component, and that's fine here since we are managing performance ourselves. TanStack Virtual isn't the only game in town. [react-window](https://github.com/bvaughn/react-window) is another well-liked library that gives you ready-made list and grid components. Its approach is a bit different, because you hand it a row component and it handles the rest, which is easier to start with but gives you less control over the markup. Either one will get the job done. ## What About the React Compiler? In the previous tutorial, we saw that the React Compiler can add the equivalent of `memo`, `useMemo`, and `useCallback` to your components automatically. That makes it a good fit for this topic, because a lot of "keeping the UI responsive" comes down to not doing work that you didn't need to do. The compiler helps with exactly that category of problem. It won't turn a slow list into a fast one, though. It can stop a component from re-rendering when nothing it depends on has changed. It can't make a component that has to render cheaper, it can't split your bundle, and it can't decide which of your updates are urgent. All of the techniques in this tutorial still apply. I ran the examples here without the compiler turned on, so the `memo` calls you see are written out by hand. If you are using the compiler, you may find that some of that memoization is handled for you, but I would still keep an explicit `memo` on a component like `ItemList`, so that the intent is clear to the next person reading the code. ## Where to Start We've covered a handful of tools. Here is a rough order for how I'd approach a sluggish app: 1. **Measure.** Use the Profiler to find the components that are expensive, and the Performance panel to find the long tasks. Fix the thing that is actually slow. 1. **Do less work.** Look for components that render when they don't need to, and for calculations that can be skipped. This is where `memo`, or the React Compiler, comes in. 1. **Prioritize.** If the slow work is real and can't be skipped, such as filtering a big list, mark it as not urgent with `useTransition` or `useDeferredValue` so that typing and clicking stay snappy. 1. **Render less.** For very long lists, use virtualization so that you don't render rows nobody can see. 1. **Load less.** Split your bundle with `lazy`, starting with routes, so that the first visit is quick. Most of the time, you will only need one or two of these. And you won't know which ones until you measure. ## Conclusion A UI feels responsive when it responds to the user quickly, and that has less to do with how fast your code is than with what it does first. React gives us tools for choosing. `useTransition` and `useDeferredValue` let typing and clicking go first while the slow work happens in the background, as long as the slow component is wrapped in `memo`. Code splitting with `lazy` keeps your first load small, and virtualization keeps a huge list from ever becoming a huge pile of DOM elements. The most important tool, though, is the habit of measuring before you change anything. A fast-feeling app is usually the result of a few well-placed fixes and not a pile of optimizations everywhere. Next, we're going to switch gears and look at a different kind of problem. As your app grows, you'll have state that lots of components need to read and change, and passing it around gets tiresome. We'll start with Redux, one of the best-known answers to that problem. **Next tutorial:** [Introduction to Redux](https://www.kirupa.com/react/introduction_to_redux.htm)