React minus Redux — the steps for removal
A brief history of React state management, and an escape plan for moving off Redux with no new frameworks.
Redux was a pivotal step forward for managing state in React applications. It was 2015, Trap Queen was on the Billboard top 10, a time when “unidirectional dataflow” made developers blush with excitement as they wrestled to bring conformity to their React application’s modeling layer.
And then it happened. Dan Abramov descended from the heavens, preaching the gospel of Redux + purity + immutability. At last, an elegant way to model data and issue global updates to your application when data changed. All of a sudden, Redux was in most React applications. And then there was the middleware; redux-thunk, redux-saga, react-router-redux, redux-logger…it was a renaissance, every paradigm being built now atop this new sexy data flow. Data was coursing through applications more deterministically, as developers rapidly built new tools and tests into their newfound state machine.
Sounds great…so what happened?
Well, the alternatives to Redux are just so much better now. When React hooks were announced in 2018, the conversation slowly shifted to “How do I move off Redux or MobX?” or “Is Redux dead?”, as more applications employed hooks.
One of the biggest issues with Redux is applications tend to build their state around a single store. This can be helpful to standardize development flows, as actions and reducers are consolidated around a universal source of truth for navigation states, server data, and user input from the UI (remember redux-form)? When you control the observable universe of state, any given state of the UI may be mocked and tested, and you can even “time travel” to different application states easily via recording reducer states or action sequences.
“If you wish to make an application from scratch, you must first invent the universe” — Redux
Yet, there is a serious downside to all of this consolidation — it comes at the cost of composability. Redux’s presence in apps has a tendency to pull the business logic out of the React component, into a black hole of thunks, sagas, and redux reducers.
This can work if you want to completely separate your data model from your interface, but it can also make the application harder to understand. In Redux apps, components are dependent (directly or indirectly) on the Redux provider‘s presence in the app, connect(mapStateToProps, mapDispatchToProps)(MyComponent)
a lifeline to components hoping to represent significant features. Any global state updates may trigger a re-render of most of the components in the app, if developers aren’t careful representing immutability, selectors and component purity correctly. It’s a lot of overhead if your needs don’t require it.
When Abramov finally announced React hooks in 2018, there was a new promised land, where apps could contain all of this modeling and logic to just the parts of the application that needed it. With hooks, the logic is also portable, and may work in any react app without the need for frameworks and middleware.
Enough with the history lesson, how do I remove Redux?
Your company probably doesn’t want to refactor out the Redux layer all at once, so with that in mind, here are some steps to safely move your dependence off of Redux, without requiring a complete rebuild.
The Rules
- For new code, only functional components are allowed. Disallow contributions of any new class components in your app, so hooks are easily accessible going forward.
- Model any new data with hooks, and separate out any new shared state into
React.useContext
hooks. If you do need to access Redux, leverageuseSelector
anduseDispatch
fromreact-redux
to peek into the store or change it. These later can be replaced with new hooks when you deprecate redux completely and represent state elsewhere. - When you do have time to tackle tech debt, follow step 1 and 2 for your old components: move your old class components to functional components, and redux reducers + actions to a separate context provider, one reducer at a time, as to not shake the boat too much.
Here’s an example approach. Imagine we have a simple UserReducer
in our app for managing users, and a UserList
component for rendering that all of the users.
Step 1— Remove Class Component
Let’s first start by updating UsersList.tsx
to be a functional component. Moving to a functional component is fairly simple, leveraging useSelector
and useDispatch
instead of connect
, so we can leverage more hooks in the component’s body:
Step 2— Replace Redux reducer with useUserStore hook
Finally, the biggest step is removing the redux pieces altogether. To do this, we will model users via a React context that can be accessed via a hook. First, we create the context, and then expose the provider (for providing the user state as a source of truth to other components) and export a custom hook for accessing this shared state.
Now accessing user data is as easy as const { users } = useUserStore()
. Anytime users
changes, it will update the value in context, due to the useMemo
dependencies. Just don’t forget to add UserStoreProvider
in your component hierarchy, above whatever components need access:
// Root.tsx
import * as React from "react"
import { UserStoreProvider } from "./user-hooks.ts"
import MyApp from "./my-app";
const Root = () => {
return (
<UserStoreProvider>
<MyApp />
</UserStoreProvider>
)
};
export default Root;
Caveats
Redux still may have value in key use cases. One major case is if you’re not sure that you want to use React, or if you have many different application implementations, with a similar data model. Redux can operate independently of the ui framework you choose. My personal experience is that Redux often continues to exist in modern applications due to precedent, and moving to hooks simplifies logic and removes boilerplate code in the app. This generally makes it clearer regarding what’s happening within the body of components.
If your reducer is more complicated than the UserReducer
, you may want to consider React.useReducer
to model your data in the useUserStore
hook, instead of React.useState
, and React.useCallback
.
React.memo + Performance
If you start using context for your data model, you will want to consider what components are selecting from context, and triggering updates. Whenever a context update is triggered, it will re-render all pure components that use that context. Even React.memo
items are re-rendered if they call useContext
within their body, and that context has changed.
Frameworks to consider
react-query
react-query
provides a nice context abstraction for requesting and caching data from any API using useQuery
hooks. It has great Typescript support, and handles most of the API layer use cases that redux supports in apps with a lot less boilerplate.
GraphQL
If you find you have an opportunity to re-architect things more deeply, you may consider building a GraphQL layer. @apollo/client
or other GraphQL frameworks will do so much of the state management for you, when fetching, caching, and updating data based on your GraphQL schema and caching config. This means less hooks for you to write!
If you want to avoid some of the useContext
performance challenges, https://recoiljs.org/ claims to solve some of these challenges with observables, but I haven’t had a chance to try it yet.
Have any other tips for migrating of Redux or why it’s still valuable to your app? Leave a comment!