You Might Not Need Redux
Redux is for many considered the industry standard way to manage state in React applications. But much like using a cannon to kill a mosquito, more often than not Redux is overkill for many scenarios. From my personal experience, it's very easy for a Redux-based application to grow out of control in complexity really fast if you are not diligent in you code reviews.
I'll list here a few scenarios where Redux isn't necessary, and a few where it can be a good call.
Local state is king
In the same way you wouldn't store all your variables inside the window
object, you shouldn't store all your state inside a global store. A global store is for, you guessed it, for state that should be globally available. There's no need to save the current color of an button in said store.
Avoiding prop drilling
If all you need is a way to avoid prop drilling, then Context is your friend. Since version 16.3
, React ships with this API that allows you to declare some data in a Provider
component, and have this data available anywhere down the tree from a useContext
hook:
ThemeContext.jsx
import { createContext } from 'react'
export const ThemeContext = createContext('light')
index.jsx
import { MyComponent } from './MyComponent.jsx'
import { ThemeContext } from './ThemeContext.jsx'
import { render } from 'react-dom'
render(
<ThemeContext.Provider value="dark">
<MyComponent />
</ThemeContext.Provider>,
document.getElementById('root'),
)
Component.jsx
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext.jsx'
export const MyComponent = () => {
const currentTheme = useContext(ThemeContext)
return <p>This app is using the {currentTheme} theme.</p>
}
Result
<p>This app is using the dark theme.</p>
On the above example, we didn't need to pass the theme value as props, instead we recovered it directly from our Context.
Updating data on distant components
You have two components that are distant from each other in the render tree, but still need to be able to share and update the same data for both. This problem can also be solved with Context, in combination with useState
hooks:
CounterProvider.jsx
import { createContext, useState, useContext } from 'react'
const CounterContext = createContext({
value: 0,
setValue: () => {}
})
export const useCounter = () => useContext(CounterContext)
export const CounterProvider = ({ children }) => {
const [value, setValue] = useState(0)
return (
<CounterContext.Provider value={{ value, setValue }}>
{children}
</ThemeContext.Provider>
)
}
In the example above, all components under CounterProvider
that use useCounter
will receive a { value, setValue }
object. Calling setValue()
with a new number will update the value
for all other components using useCounter
.
You can also make a version with useReducer if you need more control on how the value must be updated.
Saving the fetch result of an endpoint
It's a common pattern in Redux-heavy applications to have all you application logic living inside reducers and action creators, including async operations like fetching data from a server, with the help of middleware like redux-thunk .
If you see yourself repeating those patterns to have this external data available in your application, manually creating some isLoading
and error
states in your reducers, then a much simpler solution is to have a data fetching/caching library that was specifically built to handle this situation.
React Query and SWR are very popular ones, and Apollo Client uses a similar logic for GraphQL endpoints.
Here's how it would look like fetching data from a REST API using React Query:
index.jsx
import { Example } from './Example.jsx'
import { QueryClient, QueryClientProvider } from 'react-query'
import { render } from 'react-dom'
const queryClient = new QueryClient()
render(
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>,
document.getElementById('root'),
)
Example.jsx
import { useQuery } from 'react-query'
const fetchData = async () => {
const response = await fetch('https://api.test.com/v1/endpoint')
const data = await response.json()
return data
}
export const Example = () => {
const { isLoading, error, data, isFetching } = useQuery('myData', fetchData)
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<p>{data.content}</p>
<div>{isFetching ? 'Updating...' : ''}</div>
</div>
)
}
Notice how it automatically handles first load, error and update states for you. The data is also cached, so multiple calls to useQuery
using the same query keys will make use of the local cache.
Managing form state
If you're using any Redux tooling to manage state and validation of a form, I've got good news for you! We now have react-hook-form , and as the name suggests, it's a form library built around React Hooks, and it's very simple to use.
Here's a quick start example:
import { useForm } from 'react-hook-form'
const MyForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} />
<input {...register('lastName', { required: true })} />
{errors.lastName && <p>Last name is required.</p>}
<input {...register('age', { pattern: /\d+/ })} />
{errors.age && <p>Please enter number for age.</p>}
<input type="submit" />
</form>
)
}
Notice how all the validation is described inside register()
calls, and you get the updated form state from the useForm()
hook. The data
sent to onSubmit
will be an object with all the form data, with the input names as keys. No actions, action creators, dispatchers, reducers or selectors in sight.
OK, when should I use Redux then?
The Redux team itself answered this question very eloquently in their FAQ . But to quote the most important bit:
Redux is most useful when in cases when:
- You have large amounts of application state that are needed in many places in the app
- The app state is updated frequently
- The logic to update that state may be complex
- The app has a medium or large-sized codebase, and might be worked on by many people
- You need to see how that state is being updated over time
Dan Abramov also created an equally titled post , where here describes a few more situations where Redux shines:
Back to home page
- Persist state to a local storage and then boot up from it, out of the box.
- Pre-fill state on the server, send it to the client in HTML, and boot up from it, out of the box.
- Serialize user actions and attach them, together with a state snapshot, to automated bug reports, so that the product developers can replay them to reproduce the errors.
- Pass action objects over the network to implement collaborative environments without dramatic changes to how the code is written.
- Maintain an undo history or implement optimistic mutations without dramatic changes to how the code is written.
- Travel between the state history in development, and re-evaluate the current state from the action history when the code changes, a la TDD.
- Provide full inspection and control capabilities to the development tooling so that product developers can build custom tools for their apps.
- Provide alternative UIs while reusing most of the business logic.