Skip to content

Instantly share code, notes, and snippets.

@fnky
Last active January 7, 2024 12:32
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save fnky/7d044b94070a35e552f3c139cdf80213 to your computer and use it in GitHub Desktop.
Save fnky/7d044b94070a35e552f3c139cdf80213 to your computer and use it in GitHub Desktop.
React Hooks: useReducer with actions and selectors (Redux-like)
function useSelectors(reducer, mapStateToSelectors) {
const [state] = reducer;
const selectors = useMemo(() => mapStateToSelectors(state), [state]);
return selectors;
}
function useActions(reducer, mapDispatchToActions) {
const [, dispatch] = reducer;
const actions = useMemo(() => mapDispatchToActions(dispatch), [dispatch]);
return actions;
}
const initialState = { count: 0 };
function reducer(state = initialState, action) {
switch(action.type) {
case 'increment':
case 'decrement':
return { count: state.count + action.amount };
}
}
function Example() {
const counterReducer = useReducer(reducer, initialState);
const { increment, decrement } = useActions(counterReducer, (dispatch) => ({
increment: (amount) => dispatch({ type: 'increment', amount || 1 }),
decrement: (amount) => dispatch({ type: 'decrement', amount: -(amount || 1) })
}));
const { getCount, getDividedBy } = useSelectors(counterReducer, (state) => ({
getCount: () => state,
getDividedBy: (amount) => state.count / amount
}));
return (
<div>
<p>Current count is {getCount()} and divided by two: {getDividedBy(2)}</p>
<button onClick={() => increment(1)}>+</button>
<button onClick={() => decrement(1)}>-</button>
</div>
)
}
@josh-stevens
Copy link

This looks nice. I'm also trying to get redux-like state management out of hooks. For a truly global store I would put the reducer/action/selector hooks in a context, and relevant components would use the context(es) they need.

Question: how efficient is the useSelector's memoization here? It looks like this would recalculate all of the selectors every time the state is updated, which is fine for a simple state such as this, but a more complex state object would need to memoize different parts of the state, yes?

@josh-stevens
Copy link

Having spent a large part of the day working on this, I'm starting to wonder if I should just bring Redux into the project I'm working on. I think react-redux has hook integration now.

@fnky
Copy link
Author

fnky commented Jun 25, 2019

This is quite an old example when I first got introduced to hooks.

You're right that it would have to update all selectors when state changes, which isn't viable. Ideally you would wan't to memorize individual selectors and use their slice of the state as a dependency instead, rather than the whole state.

Now, you won't need redux to handle global state. Personally, I have had great success with just using Context and Hooks to manage state across components, also using GraphQL for data with Apollo.

If you're building a complex app a state management library like Redux may be useful to you.

I can recommend the following articles about the matter:

@josh-stevens
Copy link

Interesting, so are you still using a similar pattern to this for your Context/Hooks state?

I ended up using a useActions hook like you have here, but I got rid of the useSelectors. Instead I just access the reducer state directly, and for derived state I have a couple of useMemo hooks attached to those specific pieces of the reducer state.

Thanks for the articles. It seems a lot of people are not in favor of the action creators, but I like this method of action creators living in the context with the reducer. This way, components themselves just worry about using callback functions and don't even know the difference between a context that has a simple useState vs a context that has a reducer.

My context stuff is almost certainly causing unnecessary re-renders but I don't notice any performance degradation.

@fnky
Copy link
Author

fnky commented Jul 2, 2019

I rarely have a need for this kind of abstraction using Contexts and Hooks. What I do for state in a shared area is have two contexts, one that represents the state (e.g. TodosState), and another that can be used to update the state (e.g. TodosDispatch).

If I even run into performance issues I use a combination of React.memo/useMemo. I recommend reading on how you can use useMemo to optimize expensive renders.

@NikolaGrujicic
Copy link

if you really want to replace Redux using hooks I suggest using the useReducer and useContext hooks, they are easy to understand and quite flexible, here is an article that compares Redux and React hooks and detail:
https://www.framelessgrid.com/react-hooks-vs-redux-for-state-management-in-2021/

@fnky
Copy link
Author

fnky commented Jul 13, 2021

I have since moved on from this style of state management and gone to libraries like Recoil, jotai and XState. I found that reducers and contexts required a lot of boilerplate, and the fact that managing memoized components is cumbersome and can become hard to debug.

Another issue is that useContext currently has no way to select state to prevent unnecessary re-renders out of the box (although they are experimenting with an API to do this). For medium to large projects, which depends on a lot of state, having to implement this myself would be a waste, when most state management libraries already handles this for you.

I still use Context for things like dependency injection (e.g. in contrast to deep prop-drilling) and useReducer in small projects which are easy to debug and doesn't need to type of performance that state management libraries provide.

If I were to use Redux, I'd use Redux Toolkit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment