1.) Does useReducer
come as a replacement of Redux?
Let's clarify what Redux is, so we can compare its main parts to a vanilla React solution with useReducer
:
(inspired by this article)
How to replace all parts with vanilla React
Action Dispatcher / State Selector
This is a replacement for what React Redux does:
import { useReducer, useContext, useMemo } from "react"
import { rootReducer } from "./reducers"
const GlobalContext = React.createContext()
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, { count: 0 });
const store = useMemo(() => [state, dispatch], [state]);
// You can also separate dispatch and state context providers
return (
<GlobalContext.Provider value={store}>{children}</GlobalContext.Provider>
);
};
// Provider is placed at top-level to emulate a global state store
ReactDOM.render(<Provider> <App /> </Provider>, document.getElementById('root'))
const Comp = () => {
// extract this UI logic in its own custom hook for better encapsulation
const [state, dispatch] = useContext(GlobalContext);
// ...
};
Data Store
Redux store can be instantiated (createStore
) and accessed quite flexibly. With vanilla React, the store is bound to a single useReducer
in the UI. We can pass its state down via context or props.
Store Reducer
The exact same pure rootReducer
function is used for vanilla as with Redux.
Middleware API
redux-thunk
and redux-saga
are two of the most popular middlewares for async operations and side effects in Redux. With useReducer
, we do not have any built-in middleware API. Instead we do the async processing first and afterwards forward the result to dispatch
:
const [state, dispatch] = useContext(GlobalContext);
// ...
<button onClick={() => dispatchAsync(dispatch)}> Process </button>
const dispatchAsync = dispatch => {
fetchData().then(data => dispatch({type: "increment"}, data))
};
It is still possible to integrate existing Redux middlewares with useReducer
, as long as their common API is covered - you can take a look at this answer for more infos.
Redux DevTools (Time traveling, debug support)
There is no direct integration for useReducer
available, so you may miss an important workflow tool here. The reinspect library uses Redux DevTools to also inspect useState
and useReducer
(haven't tested though).
2.) Does it suits particular projects better? Where would it fit?
useReducer
usage starts with local state and in the component scope. As you have seen, it can also be lifted to the global scope to take over most of Redux' roles.
In some situations, useReducer
can even provide more flexibility, as the global state can be divided between multiple contexts. Example: Separate low priority and high priority state changes in different state trees/contexts.
With vanilla React, you will mainly miss out on Redux DevTools and popular middleware libraries. On the other hand functions like combineReducers
can be easily re-implemented for useReducer
, as seen here.
General rule of thumb: Start using vanilla React for your app and add functionality like a global Redux store incrementally, as soon as you feel, this becomes mandatory (also depends on app size).
const GlobalContext = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, { count: 0 });
const store = useMemo(() => [state, dispatch], [state]);
// You can also separate dispatch and state context providers
return (
<GlobalContext.Provider value={store}>{children}</GlobalContext.Provider>
);
};
const rootReducer = (state, action) =>
action === "increment" ? { count: state.count + 1 } : state;
const dispatchAsync = dispatch => {
// just do the async operation before and invoke dispatch afterwards
setTimeout(() => dispatch("increment"), 1000);
};
const Comp = () => {
// You can extract this UI logic in its own custom hook for better encapsulation
const [state, dispatch] = useContext(GlobalContext);
return (
<div>
<div>Counter: {state.count}</div>
<button onClick={() => dispatchAsync(dispatch)}>
Increment async (1sec delay)
</button>
</div>
);
};
ReactDOM.render(<Provider><Comp /></Provider>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useContext, useMemo } = React</script>