Ever since React Hooks feature was announced and the API went public, I have been thinking about what this means for Redux — a well known state management library. For quite some time there have been voices shouting that we don’t need Redux, we can achieve the same with React Context. Well, I think of Redux as more of a pattern than a library. After a day playing with React Hooks, I was able to implement this pattern using context and hooks. I will show you how to make your own Provider wrapper and useRedux hook. Oh, and it’s fully typed in TypeScript.
First, we create a Provider for our state and actions. Let’s create a function called initRedux
that will have 2 parameters: root reducer and initial state of application, so we can pass them to useReducer hook.
const [state, dispatch] = useReducer(rootReducer, initialState)
This hook will give us state object and dispatch function. Those two should be accessible in the entire application. The best way to achieve this is by passing them down the component tree with React Context, one context for each.
const StateContext = React.createContext(null)const DispatchContext = React.createContext(null)
Our initRedux
method creates a Provider
that already contains providers for these two contexts.
With this Provider
you can wrap your entire application and have your application state and dispatch function available in every component. But how can we use them in our components?
One of the great advantages of hooks is their ability to compose. You can make your own hook which use another hooks. As long as you obey the Rules of Hooks, you can play with them in any way you want.
As I said, we need to have access to the application state and be able to dispatch our actions. To make things a little more convenient, I decided to use an interface very similar to the one used by connect
function from React-Redux. If you are not familiar with it, definitely check it, it’s very useful.
So, our useRedux
hook will take 2 arguments called extractState
and actionMap
(as parallel to mapStateToProps
and mapDispatchToProps
).
First, we retrieve both contexts.
const appState = React.useContext(StateContext)const dispatch = React.useContext(DispatchContext)
Of course we don’t want to return the entire state of application but instead, we use extractState
function from arguments to… well, extract the desired portion of state.
const stateExtract = extractState(appState)
That part was easy. A consumer of useRedux
already provided a function that takes the application state as an argument and selects only relevant values. Now we need to take the actionMap
argument and do some mapping magic. Assuming that actionMap
is an object with a shape {key: myActionCreator}
, let’s bind a dispatch
function to those action creators.
The code iterates through keys in the actionMap
object and replaces it’s values (action creators) with functions that directly dispatch actions returned by those action creators. With one simple modification, we can dispatch also thunks. Just add one condition:
const fn = (...args) => {const action = actionCreator(...args)if (typeof action === 'function') {action(dispatch, () => appState)} else {dispatch(action)}}
Now all we need to do is to return stateExtract
and actions
from our custom hook. A complete initRedux
function looks like this.
In about 40 lines of code you can create your own Provider and useRedux custom hook. I did so, typed it all with TypeScript and published it as redoox
package to npm. You can try and play with it, if you want, but be aware that this is just an experiment and it’s definitely not suitable for production!
There is already quite a number of packages trying to achieve the same. There’s one even from Facebook incubator. I think it would be nice to not have gazillion packages trying to solve the same problem with slightly different API. If you know about one that has a fair chance to become “the official one”, let me know in the comments. I will gladly help implementing it.
Now, let’s take a look at how good a developer experience can be thanks to typed interface.
The usage is very simple. As an example, I created simple application that loads users from REST API, displays them and you can increment/decrement their age.
2. Implement action creators
3. Implement reducer. I’m using the immer library to ensure immutability. Notice, how you can hardly make a mistake while accessing the action payload.
4. Implement selectors. I’m using the reselect library.
5. Create a store.
6. And finally, use the hook in a component. If all interfaces are typed properly, you will always know what part of state you extracted and what actions are available.
You can find a full example here. Just clone a repository, then yarn
,yarn build
and yarn start
.
I came across many different state management patterns throughout my career as a software developer and not just those for React /JavaScript. So far, I like Redux the most. It’s predictable, maintainable and easily testable. Those are the attributes that really matter to me with regard to application’s data layer. It would be great if we could make it work with React Hooks on larger scale.
Thanks for reading. If you a have a question or an interesting idea, please, share it in the comments section.