Editor’s note: This React state patterns guide was updated on 3 August 2022 to include information on why React uses state and to include a section differentiating state and props.
Since its inception in 2013, React has rolled out a robust set of tools to help relieve developers of some of the minutiae of creating web applications and allow them to focus on what matters.
Despite React’s many features and consistent popularity among developers, however, I have found time and again that many of us ask the same question: how do we handle complex state using React?
In this article, we’ll investigate what React state is, how we can organize it, and different React patterns to employ as the complexity of our applications grow. Let’s get started!
- What is a React state?
- Why does React use state?
- State vs. props
- Managing state with Redux
- Contemporary React state patterns
What is React state?
In its purest form, React can be considered a blueprint. Given some state, your application will look a certain way. React favors the declarative over the imperative, which is a fancy way of saying that you write what you want to happen instead of writing the steps to make it happen. Because of this, managing state correctly becomes supremely important because state controls how your application will behave.

State in action
Before we get started, it will be useful to briefly discuss what state is. Personally, I think of state as a collection of mutable values that change over time and directly influence component behavior.
State is very similar to props, but the difference is that state can be changed within the context of where it is defined, whereas props received cannot be changed without passing a callback function.
Let’s have a look:
const UserList = () => { const [users, setUsers] = useState([]) useEffect(() => { const getUsers = async () => { const response = await fetch("https://myuserapi.com/users") const users = await response.json() setUsers(users) } getUsers() }, []) if (users.length < 1) return null; return - {users.map(user => - {user.name} - )} - }
In this example, we are fetching users from an API when the component mounts and updating the users
array once we have received a response. We naively assume that the call will always be successful to reduce the complexity of the example.
We can see that the state is being used to render list items with the user’s name, and it returns null
if there are no users in the array. The state changes over time and is used to influence component behavior directly.
It’s also worth noting that we’re utilizing React’s state management method via the useState
hook. Depending on the complexity of your application and state management, you may only need to manage your state using React’s inbuilt hook.
However, as is clear by the abundance of state management solutions for React, the inbuilt state management method sometimes is not enough. Let’s take a look at some of the reasons why.
Understanding prop drilling
Let’s consider a slightly more complex app. As your application grows, you’re forced to create multiple layers of components in order to separate concerns and/or increase readability. The problem occurs when you have state that is needed in multiple components that have different places in the component tree.

If we wanted to supply both the UserMenu
and the Profile
components with user data, we must place the state in App
because that’s the only place that can propagate the data down to each component that requires it. That means we’ll pass it through components that may not require the data — Dashboard
and Settings
, for example — polluting them with unnecessary data.
Now, what if you need to manipulate the data in another component? Well, you’ll need to supply the updater function (the setUsers
function from the last example) to the component that needs to do the updating, adding yet another property to propagate down — all this for one piece of state. Now imagine compounding it by adding five more properties. It can quickly get out of hand.
Have you ever heard someone say, “You’ll know when you need a state library”?
For me, that means how comfortable I feel with drilling the properties and updater functions through multiple layers of components. Personally, I have a hard limit on three layers; after that, I reach for another solution. But until that point, I’m adamant about using the inbuilt functionality in React.
State libraries come with a cost as well, and there is no reason to add unnecessary complexity until you are sure that it’s absolutely needed.
The re-rendering issue
Since React automatically triggers a re-render once state is updated, the internal state handling can become problematic once the application grows. Different branches of the component tree might need the same data, and the only way to provide these components with the same data is to lift the state up to the closest common ancestor.
As the application grows, a lot of state will need to be lifted upwards in the component tree, which will increase the level of prop drilling and cause unnecessary re-renders as the state is updated.
The testing issue
Another problem with keeping all of your state in the components is that your state handling becomes cumbersome to test. Stateful components require you to set up complex test scenarios where you invoke actions that trigger state and match on the result. Testing the state in this way can quickly become complex, and changing how state works in your application will often require a full rewrite of your component tests.
Why does React use state?
All of the code we write in React is defined within a component.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
A React state is an inbuilt object that stores data or information about a component. A component’s state can change over time, and when it does, the component re-renders. Changes in state can occur as a result of user activity, network response, or system-generated events, and these changes have an impact on how the component behaves and renders. When you know that the variable value will affect the view, use React state.
State is used by React to control how a component behaves. Because state is dynamic, a component can keep track of changing information between renders, thereby keeping it dynamic and interactive.
State vs. props
As we alluded to before, props are objects that contain data used to control how a specific component behaves. Although this sounds a lot like state, props and state are nowhere near the same. Let’s distinguish between the two.
First and foremost, props cannot be changed after they have been set, while states are observable objects that can be used to hold data that may change over time. State then controls the components’ behavior after each change.
Additionally, props are usually set by the parent component, whereas state is usually updated internally by the component itself.
Finally, while props are read-only for the component they belong to, state is contained within a single component and can be updated.
Managing state with Redux
As far as state libraries go, one of the most prominent and widely used libraries for managing state is Redux. Launched in 2015, Redux is a state container that helps you write maintainable, testable state. It’s based upon principles from Flux, which is an open source architecture pattern from Facebook.

In essence, Redux provides a global state object that supplies each component with the state it needs, re-rendering only the components that receive the state (and their children). Redux manages stated based on actions and reducers. Let’s quickly examine the components:

In this example, the component dispatches an action that goes to the reducer. The reducer updates the state, which in turn triggers a re-render.
State
State is the single source of truth; it represents your state at all times. Its job is to supply the components with state. Example:
{ users: [{ id: "1231", username: "Dale" }, { id: "1235", username: "Sarah"}] }
Actions
Actions are predefined objects that represent a change in state. They are plain text objects that follow a certain contract:
{ type: "ADD_USER", payload: { user: { id: "5123", username: "Kyle" } } }
Reducers
A reducer is a function that receives an action and is responsible for updating the state object:
const userReducer = (state, action) => { switch (action.type) { case "ADD_USER": return { ...state, users: [...state.users, action.payload.user ]} default: return state; } }
Contemporary React state patterns
While Redux still is a great tool, over time, React has evolved and given us access to new technology. In addition, new thoughts and ideas have been introduced into state management that have resulted in many different ways of handling state. Let’s investigate some more contemporary React state patterns in this section.
useReducer
and the Context API
React 16.8 introduced Hooks and gave us new ways to share functionality through our application. As a result, we now have access to a Hook that comes built into React called useReducer
, which allows us to create reducers out of the box. If we then pair this functionality with React’s Context API, we now have a lightweight Redux-like solution that we can use through our application.
Let’s take a look at an example with a reducer handling API calls:
const apiReducer = (state = {}, action) => { switch (action.type) { case "START_FETCH_USERS": return { ...state, users: { success: false, loading: true, error: false, data: [] } } case "FETCH_USERS_SUCCESS": return { ...state, users: { success: true, loading: true, error: false, data: action.payload.data} } case "FETCH_USERS_ERROR": return { ...state, users: { success: false, loading: false, error: true, data: [] } } case default: return state } }
Now that we have our reducer, let’s create our context:
const apiContext = createContext({}) export default apiContext;
With these two pieces, we can now create a highly flexible state management system by combining them:
import apiReducer from './apiReducer' import ApiContext from './ApiContext const initialState = { users: { success: false, loading: false, error: false, data: []}} const ApiProvider = ({ children }) => { const [state, dispatch] = useReducer(apiReducer, initialState) return {children} }
With that done, we now need to wrap this provider around the components in our application that need access to this state. For example, at the root of our application:
ReactDOM.render(document.getElementById("root"), )
Now, any component that is a child of App
will be able to access our ApiProviders
state and dispatcher in order to trigger actions and access the state in the following way:
import React, { useEffect } from 'react' import ApiContext from '../ApiProvider/ApiContext const UserList = () => { const { users, apiDispatcher } = useContext(ApiContext) useEffect(() => { const fetchUsers = () => { apiDispatcher({ type: "START_FETCH_USERS" }) fetch("https://myapi.com/users") .then(res => res.json()) .then(data => apiDispatcher({ type: "FETCH_USERS_SUCCCESS", users: data.users })) .catch((err) => apiDispatcher({ type: "START_FETCH_ERROR" })) } fetchUsers() }, []) const renderUserList = () => { // ...render the list } const { loading, error, data } = users; return 0} show={renderUserList} /> }
Managing state with state machines and XState
Another popular way of managing state is using state machines. Briefly explained, state machines are dedicated state containers that can hold a finite number of states any any time. This makes state machines extremely predictable. Since each state machine follows the same pattern, you can insert a state machine into a generator and receive a state chart with an overview of your data flow.

State machines generally follow stricter rules than Redux with respect to their format to maintain predictability. In the world of React state management, XState is the most popular library for creating, interpreting, and working with state machines.
Let’s take a look at the example from the XState docs:
import { createMachine, interpret, assign } from 'xstate'; const fetchMachine = createMachine({ id: 'Dog API', initial: 'idle', context: { dog: null }, states: { idle: { on: { FETCH: 'loading' } }, loading: { invoke: { id: 'fetchDog', src: (context, event) => fetch('https://dog.ceo/api/breeds/image/random').then((data) => data.json() ), onDone: { target: 'resolved', actions: assign({ dog: (_, event) => event.data }) }, onError: 'rejected' }, on: { CANCEL: 'idle' } }, resolved: { type: 'final' }, rejected: { on: { FETCH: 'loading' } } } }); const dogService = interpret(fetchMachine) .onTransition((state) => console.log(state.value)) .start(); dogService.send('FETCH');
useSWR
Over the years, state management has grown increasingly complex. While proper state management coupled with view libraries like React allows us to do amazing things, there is no doubt that we are moving a lot of complexity to the frontend. And with increased complexity, we are also inviting more cognitive load, more indirection, more potential for bugs, and more code that needs to be thoroughly tested.
useSWR
has been a breath of fresh air in this regard. Pairing this React state library with the native capabilities of React Hooks produces a level of simplicity that is hard not to love. This library uses the HTTP cache technique stale-while-revalidate
, which means it keeps a local cache of the previous dataset and syncs with the API in the background to get fresh data.
This keeps the app highly performant and user-friendly because the UI can respond with stale date while waiting for updates to be fetched. Let’s take a look at how we can utilize this library and do away with some of the complexities of state management.
// Data fetching hook import useSWR from 'swr' const useUser(userId) { const fetcher = (...args) => fetch(...args).then(res => res.json()) const { data, error } = useSWR(`/api/user/${userId}`, fetcher) return { user: data, error, loading: !data && !error } } export default useUser
Now we have a reusable Hook that we can utilize in order to get data into our component views. No need to create reducers, actions, or connecting components to state in order to get your data — just import and use the Hook in the components that need the data:
import Loader from '../components/Loader' import UserError from '../components/UserError' import useUser from '../hooks/useUser'; const UserProfile = ({ id }) => { const { user, error, loading } = useUser(id); if (loading) return if (error) return return {user.name} ... }
And in another component:
import Loader from '../components/Loader' import UserError from '../components/UserError' import useUser from '../hooks/useUser'; const Header = ({ id }) => { const { user, error, loading } = useUser(id); if (loading) return if (error) return return ... }
This method allows you to easily pass around Hooks that can access a shared data object because the first argument to useSWR
is a key:
const { data, error } = useSWR(`/api/user/${userId}`, fetcher)
Based on this key, our requests are deduped, cached, and shared across all our components that use the useUser
Hook. This also means that only one request is sent to the API as long as the key matches. Even if we have 10 components using the useUser
Hook, only one request will be sent as long as the useSWR
key matches.
Conclusion
If React is a canvas that at any time represents your application state, then state is really important to get right. In this article, we’ve looked at various ways to handle React state in applications, and in truth, we could have included more.
Recoil and Jotai, not to mention React Query and MobX, are certainly relevant in a discussion like this, and the fact that we have a lot of different state libraries is a great thing. It pushes us to try out different things, and pushes library authors to constantly do better. And such is the way forward.
Now, which solution should you choose for your project? This is a question I cannot answer, but I will give my own opinion.
Personally, I tend to side with the library that introduces the least amount of complexity. It’s fantastic to have tools such as Redux at our disposal, and there are times when they are needed, but until you feel the pain, I would go for the simplest possible solution.
For me, using useSWR
has been a revelation and has significantly reduced the indirection and level of complexity in the apps I’ve recently authored.
If you liked this article, please give me a shout on Twitter. And if you want to follow more of my content, follow my YouTube channel.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Thanks for this article. Great coverage and you introduced me to some projects that I wasn’t aware of prior.
Very interesting. UseSWR looks really cool but how well does it work across all CRUD actions? How do you invalidate the cache? And what about saving the cache locally instead of in memory as an iPhone has very small memory that gets wiped often.
Thanks!
useSWR will only handle the read operations from the API. Create, Update and Delete you need to handle yourself, but you could easily extract this into a hook of your own or use the native fetch API.
By default useSWR will set up a refreshInterval to refresh the local cache. This property is also configurable, so you can choose your own refreshInterval when you use the useSWR hook. In addition it will revalidate the cache on page focus and network connection by default, which means that you get a lot for free in terms of cache revalidation. In the case that you need to force a cache invalidation you’re provided with a mutate method that you can extract from the useSWR hook that will let useSWR know that something changed and you need to refetch immediately. As for the memory, I believe useSWR utilizes the browser cache instead of actually keeping it in memory.
I’m sorry for being that guy, but claiming that state machines are inspired by Redux is such a backward thing to say. State machines have been around for decades, long before Dan Abramov and Andrew Clark were even born…
If anything Redux is probably inspired from state machines, like the concept of Actions to move from one state to the other.
Why on earth would you be sorry for pointing that out? Thanks for clarifying that obvious error, it was clumsily formulated. In fact, what I meant was that the library XState is reminiscent to redux to the point where you can actually plug in a reducer in xstate and have a state machine with createMachine. I did not mean to imply redux was the source of inspiration for state machines.
Great catch!
The way I see it, React has too many inconsistent ways to deal with state to begin with – hooks added even more new ways to deal with state, and still, new libraries and patterns keep cropping up. There are well over 50 state management libraries, any of which build around or on top of state management in React itself.
In my view, this is all symptomatic of React having incomplete/inconsistent state management to begin with – something that does not seem to get fixed by inventing another pattern or yet another state management library; why else would there still be another idea cropping up every two weeks? It’s because it doesn’t work well, or doesn’t do what developers need or want.
So why don’t they fix it? Well, they can’t, because ultimately that would be a breaking change – and probably a big one, since the problem is not just implementation details, but rather the fundamental concepts; if it were just implementation details, a library would have solved it by now.
A fundamental design change would mean existing component libraries no longer work, at which point React is no longer really React, and the community would likely split into two. This is where programming languages and frameworks get stuck, time and again. They can adjust and adapt – but they can’t truly change.
For an example of state management that actually works, see for example Sinuous – it has a single, consistent, very simple state mechanism that works equally well for local component state and global application state. Refactoring from one to the other literally is a matter of moving lines of code from one place to another. I’ve used it quite a bit, and never felt the need for any state management library or clever patterns – it just works.
Just an example, but I don’t believe the problem is which library or pattern you choose. I believe the problem is inherent to React itself and the state concepts it embodies.
One concept i can’t really wrap my brain around is state vs database storage.
When you want to save the state in a database or a service like firebase, you have to deal with both fetching the data from the database as well as managing state client side.
Do you still need a state manager in that case? Or can you do everything by fetching the required data within the component that needs it? Or only fetch the complete state at loadtime and then handle it all client side?