Enhancing useReducer.
Tags: react, redux, webdev
Published: 2020-07-25 11:41:16

I really like React built-in `useReducer` and `useContext` hooks. They made an app state management handy and using Redux lost any sense for me.

When I used them first time I realized that there is a shortage of some pretty useful Redux features:
- **useSelector**. You can't optimize re-renders just with `memo` while using `useContext` inside.
- **Global dispatch**. You have to use multiple dispatches because every `useReducer` has its own dispatch.
- **Cache**. You have to have a special place for caching reducers data.

So I decided just to add this 3 features around these hooks.
And this idea turns to a new tiny library I called [Flex Reducer](https://github.com/IpShot/flex-reducer) which seems pretty handy (at least for me).

_**Interesting fact!**_
Flex Reducer uses neither `useReducer` nor `useContext` in its implementation.

Let's write a typical Todo app in 2 versions - one with built-in useReducer + useContext and another with Flex Reducer to demonstrate its handy.

First create a root file where we render our React tree to DOM. It will be the same for both versions.
```js
// index.js
import TodoApp from "./TodoApp";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <TodoApp />,
  rootElement
);
```
_Note_: We don't have to combine reducers, create store and use Provider anymore. Yippee! :)

Now let's create a main Todo app component using built-in useReducer.
```js
// TodoApp.js
import { useReducer, createContext, useMemo } from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';

export const AppContext = createContext(null);
const cache = {};

export default function TodoApp() {
  const [state, dispatch] = useReducer(reducer, cache.state || initialState);
  cache.state = state;
  const actions = useMemo(() => ({
    setInput: (value) => {
      dispatch({
        type: 'SET_INPUT', 
        payload: value
      })
    },
    addTodo: ({ id, content }) => {
      dispatch({
        type: 'ADD_TODO',
        payload: { id, content }
      })
    }
  }), []);
  return (
    <AppContext.Provider value=[state, actions]>
      <div className="todo-app">
        <h1>{state.title}</h1>
        <input value={state.input} onChange={e => actions.setInput(e.target.value)} />
        <AddTodo />
        <TodoList />
      </div>
    </AppContext>
  );
}
```
Good enough. Let's see how it looks if we use Flex Reducer.
```js
// TodoApp.js
import { useFlexReducer, dispatch } from 'flex-reducer';
import AddTodo from './AddTodo';
import TodoList from './TodoList';

export const setInput = (value) => dispatch({
  type: SET_INPUT,
  payload: value
});
export const addTodo = ({ id, content }) => dispatch({
  type: ADD_TODO,
  payload: { id, content }
});

export default function TodoApp() {
  const [state] = useFlexReducer('app', reducer, initialState);
  return (
    <div className="todo-app">
      <h1>{state.title}</h1>
      <input value={state.input} onChange={e => setInput(e.target.value)} />
      <AddTodo />
      <TodoList />
    </div>
  );
}
```
Looks better and feel readability was improved.
We got next improvements: 
- No need to use React Context. 
- We don't have to care about cache.
- We are able to move actions anywhere as we have a global dispatch.

Now let's compare a re-render optimization for Add Todo button.
With React Hooks.
```js
// AddTodo.js
import { useContext, memo } from 'react';
import { appContext } from './TodoApp';

const genId = () => Math.rand();

const AddTodo = memo(({ input, actions }) => {
  function handleAddTodo() {
    if (content) {
      actions.addTodo({ id: genId(), content: input });
      actions.setInput('');
    }
  }
  return (
    <button onClick={handleAddTodo}>
      Add Todo
    </button>
  );
})

export default const MemoizedAddTodo = () => {
  const [state, actions] = useContext(appContext);
  return (
    <AddTodo input={state.input} actions={actions} />
  );
}
```
We can't use `useContext` right in AddTodo because it will call re-render on context update whether `memo` has used or not. So we have to wrap it and use props instead.

Let's try Flex Reducer.
```js
// AddTodo.js
import { useSelector } from 'flex-reducer';
import { addTodo, setInput } from "./TodoApp";

const genId = () => Math.rand();

export default const AddTodo = React.memo(() => {
  const content = useSelector(state => state.app.input);
  function handleAddTodo() {
    if (content) {
      addTodo({ id: genId(), content });
      setInput('');
    }
  }
  return (
    <button onClick={handleAddTodo}>
      Add Todo
    </button>
  );
})
```
Nice. No need in additional wrapper. Thanks to `useSelector` which call re-render only if `input` changed.

But everything in the world have its pros and cons.
Let's compare how it works with remote data when use a declarative way, for example [react-query](https://github.com/tannerlinsley/react-query/).
In case of built-in useReducer.
```js
// TodoApp.js
import { useReducer, createContext, useMemo } from 'react';
import { useQuery } from 'react-query';
import AddTodo from './AddTodo';
import TodoList from './TodoList';

export const AppContext = createContext(null);
const cache = {};

export default function TodoApp() {
  const [reducerState, dispatch] = useReducer(reducer, cache.state || initialState);
  cache.state = reducerState;
  const actions = useMemo(() => ({
    setInput: (value) => {
      dispatch({
        type: 'SET_INPUT', 
        payload: value
      })
    },
    addTodo: ({ id, content }) => {
      dispatch({
        type: 'ADD_TODO',
        payload: { id, content }
      })
    }
  }), []);

  const todos = useQuery('todos', fetchTodoList);
  const state = { ...reducerState, todos };
  
  return (
    <AppContext.Provider value=[state, actions]>
      <div className="todo-app">
        <h1>{state.title}</h1>
        <input value={state.input} onChange={e => actions.setInput(e.target.value)} />
        <AddTodo />
        <TodoList />
      </div>
    </AppContext>
  );
}
```
Perfect. Simple and readable.

Let's try the same with Flex Reducer.
```js
// TodoApp.js
import { useFlexReducer, dispatch } from 'flex-reducer';
import { useQuery } from 'react-query';
import AddTodo from './AddTodo';
import TodoList from './TodoList';

export const setInput = (value) => dispatch({
  type: SET_INPUT,
  payload: value
});
export const addTodo = ({ id, content }) => dispatch({
  type: ADD_TODO,
  payload: { id, content }
});
export const setTodos = (todos) => dispatch({
  type: SET_TODOS,
  payload: todos
});

export default function TodoApp() {
  const [state] = useFlexReducer('app', reducer, initialState);
  const todos = useQuery('todos', fetchTodoList);
  React.useEffect(() => {
    setTodos(todos);
  }, [todos]);
  
  return (
    <div className="todo-app">
      <h1>{state.title}</h1>
      <input value={state.input} onChange={e => setInput(e.target.value)} />
      <AddTodo />
      <TodoList />
    </div>
  );
}
```
We got a problem with additional render when we update our reducer state on every todos query update.

**Conclusion**
Using useReducer + useContext for state management are pretty good. But it requires to be careful about context and cache.
[Flex Reducer](https://github.com/IpShot/flex-reducer) takes this work, improves readability, memo optimization and reduces code base. But it's worse when you work with remote data in a declarative way (e.g. react-query).

**Warning!**
Flex Reducer is an experiment and hasn't been used in production yet.

Thanks for reading. Appreciate any thoughts.

Full code of the Todo app you can find [here](https://github.com/IpShot/flex-reducer/tree/master/examples/todos).
[Link to Flex Reducer repository](https://github.com/IpShot/flex-reducer)