DEV Community

loading...

Revisiting Redux with Context and Hooks

Brendan B
Yo escribir código
・6 min read

bind actions to dispatch

I develop a small, internal facing UI, and it's been using Redux for awhile now. The store provides functionality for certain global concerns like API fetching and notifications, but it's a bit unwieldy with all of the connecting and mapStateToProps'ing that has to happen just to inject a basic data flow. The best alternative to using Redux as a global state manager is React Context (from a purely React perspective) but until recently had some issues to overcome.


React Context, introduced in early 2018, is a way to share data deep into a component tree, by wrapping that tree in a Provider, giving it an initial store / values, and then accessing / updating those values in the child components by accessing the context 'Consumer.' The original implementation involved calling that Consumer, and rendering its children as a function with props, the props being the original store/value object from the parent node. But keeping track of all that Provider/Consumer/render propping gets clunky, and results in false hierarchies inside consuming components.

Updating data received from context, too, is tricky. Most people solved this by passing callback functions down with the context values, and using those to pass changes back up. But pairing data with callbacks like that is a little ugly, and it means that every time your data tree updates, it re-instantiates those callbacks with it. Redux's reducers provide a much cleaner way to update state, listening for event triggers that get fired by actions in the component, and updating the part of state relevant to that action. Until hooks, however, integrating reducers and context was a bulky marriage of technologies.


When hooks were introduced at the React Conf I attended in 2018, I saw their usefulness, but didn't understand why people were saying it was a Redux killer (it's not, necessarily, but that's a topic for another day). But when I discovered hooks like useContext and useReducer, things started to click into place. With the useContext hook, you can extract the context values without a consumer or having to use render props, and with useReducer you can extract both state and dispatch without much of the overhead needed by Redux.

Armed with these new tools, I decided to create my own global store/state management system, to rid myself of Redux once and for all (until I discover down the road that I actually do need it, but we'll let future problems live in the future for now). After about four or five iterations, I finally came on a pattern that made the most sense to me, and happened to eliminate hundreds of lines of code, as a nice side effect.


Before we get into the details, I want to give credit where credit is due - this article by Eduardo Marcondes Rabelo and this one by Tanner Linsley were foundational to my understanding of how to put these pieces together, and I borrow heavily from their ideas. I've also seen similar implementations here and elsewhere. The takeaway here is that there's more than one way to peel an orange, and you should choose the way that's most… appealing to you.


For an example, we'll make a very simple React application that lets the user view and refresh data from a 'stocks' API, using both state and actions from a global store. The folder structure will look something like this:

File structure

Notice the 'store' folder contains a folder for the stocks' API reducer and actions, similar to how a typical Redux project might be structured.

Our entire application will be wrapped in a StoreProvider to give every child element access to the actions and state, so let's create our index.js to start:

Again, this is a similar construct to how a Redux store would be placed at the top of an application:

The types, reducer, and actions also look very similar to Redux:

This sets up the state for stocks - an 'isLoading' boolean is available to display a loading state, data is populated on a successful fetch, and an error message is delivered if the fetch ran into a problem

This particular action is provided a 'dispatch' (similar to how thunk works) to conditionally dispatch different states based on the response from the API.

Next, let's create a helper function called 'combineStores' that will combine all reducers, combine all initial states, and return an object with both:

Note the comments in this code - this is just a simple example of a combineReducers function that I pulled from an online example - you may want to use something different, even the full blown original from Redux

We'll create two other files in our store folder - a rootReducer to give us a structured object with all the reducers and initial states (namespaced according to their respective folder names), and a rootActions to provide a similarly namespaced object for all actions in the store:

The root reducer uses the 'combineStores' function from the previous example to get the combined state object and root reducer


 

This delivers all the actions for the store with the dispatch attached to each. If the action is a Promise (as in our API example) then it gives dispatch as a parameter, otherwise it wraps the return value (the action for that case would just return an object with a type and optional payload)

To bring it all together, we'll create the StoreProvider to wrap our application in, which will provide access to all components with the global state, actions, and dispatch:

There's a few things going on here - first, if you're not familiar with hooks like useReducer, useMemo, and useContext, the React hooks API docs are a great place to start. There are three important features - the useStore function (which is actually a custom hook) returns the values from the global State context, and the useActions hook returns the namespaced actions object (more on that in a bit). The store provider is actually three nested contexts, State at the top to provide actions and dispatches access to the global state values, Dispatch, then Actions, so actions will have access to the dispatch. I'm keeping them as separate contexts here, because when the state updates (as it will do when an action is fired off) it won't reinitialize the actions and dispatch. Dispatch doesn't necessarily have to be its own context - it could just be a value passed into the actions getter, but I like to keep it available in case there arises a need for a child component to directly dispatch something.


Before we look at the store being used inside of a component, let's first understand what useStore and useActions are actually delivering. When we call useStore and useActions, they give back objects something like this:

The namespacing is important here to differentiate different parts of the store. I'm keeping names on actions and store the same for consistency (actions.stocks, store.stocks, etc)

Let's go ahead and create our App.js which will hold our Stocks component:

Now let's create that Stocks component:

You can see we're pulling in the useStore and useActions hooks from the store, getting the state values under 'stocks' from useStore and the global actions object from useActions. The useEffect hook runs every time the component updates, but because we pass in an empty array as its second parameter it only runs on mount. So when the component loads, a call to the 'fetchStocks' action will be made, and then again any time the user clicks the 'Refresh stocks' button. For a comparison, let's see what that component would look like if we used Redux:

Things would get even more complex if we allowed the user to modify the existing state (another article for another time).


The choice to use a large state management library like Redux vs some kind of custom variant like this is at least partly subjective, and will depend on the different needs and scale of your application. Bear in mind, too, that tools like context and hooks are brand new, and 'best practices' is still in the eye of the beholder. That being said, feedback is strongly encouraged - this implementation is really just a first effort for something that will hopefully be much more robust in the future.

Discussion (14)

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
visarts profile image
Brendan B Author

Do you have any public code I could look at? It's hard to tell exactly without seeing the context :)

Collapse
trickydisco78 profile image
trickydisco78

Got it working now. I took out params = {} thinking it wasn't required

Thread Thread
visarts profile image
Brendan B Author

That's great! Glad it worked

Collapse
jsina profile image
Sina Maleki

Thanks about your articles, do have a repo for this article?

Collapse
visarts profile image
Brendan B Author

I never made one, sorry! I may one day and if I do I'll post it here, but I'm more likely to write an update to this that's a little less heavy handed with context, I'll have a repo with that one if I do.

Collapse
nielsdom profile image
Niels

Thanks for the article. It seems really a pain to implement redux... You have any repo with best practices architecture redux/hooks to recommend ? a simple one if possible, thanks :)

Collapse
visarts profile image
Brendan B Author

I don't have a public one, yet! But I'll post something here when I get to working on a big refactor of a public project I have. I've learned a few things since I wrote this and think there are some cleaner ways to do stuff, especially around actions. I do agree that redux is a lot of implementation detail, and while this article cuts down on some of that it's still a lot of boilerplate that I'd like to reduce, without putting a lot of work on the consuming component.

Collapse
nielsdom profile image
Niels • Edited

Thanks, looking forward for a repo :D

By the way, I tried this redux dev tools package to trace hooks activity, really cool: github.com/troch/reinspect

Thread Thread
trickydisco78 profile image
trickydisco78

i couldn't seem to get this to work. Where did you add it? I tried around the main app or storecontext but it didn't show that it updated state in the devtools

Collapse
aceabdel profile image
aceAbdel

hit there is any way to access the state tree in the actions?
like store.getState() ?

Collapse
visarts profile image
Brendan B Author

Sorry for the late reply. One way would be in the 'getRootActions' method in the StoreProvider component, to pass another parameter after dispatch, 'state', and then pass that into bindDispatchToActions to be available to any of the actions it iterates over. It's kind of clunky, but quite honestly this whole approach here is a bit heavy. I think I might, especially for a smaller app, just leave actions context out entirely, export some basic action functions from a file and just import those anywhere they're needed, passing in dispatch at point of use.

Collapse
trickydisco78 profile image
trickydisco78

Using this architecture does it mean that any changes to the store will cause re-renders to all the components even if they aren't interested in a piece of state?

Collapse
visarts profile image
Brendan B Author

I tested this out on a project I used to work on and didn't see re-renders, although it may depend on the implementation - I believe (although I could be wrong) that the way context works here in conjunction with the reducer hook means that state changes don't cause re-renders in anything but the components using that piece of state.