DEV Community

terrierscript
terrierscript

Posted on

Emulate Redux with React hooks

Notice: React Hooks is RFC. This article is experimental

1. use combineReducers with useReducer

We can create nested reducer in redux combineReducer and I try to combine nested reducer and useReducer hook.

import { combineReducers } from "redux" const counter = (state = 0, action) => { switch (action.type) { case "INCREMENT": return state + 1 case "DECREMENT": return state - 1 } return state } const inputValue = (state = "foo", action) => { switch (action.type) { case "UPDATE_VALUE": return action.value } return state } export const rootReducer = combineReducers({ counter, // nest someNested: combineReducers({ inputValue }) }) 

And create components

import React, { useReducer } from "react" const App = () => { const [state, dispatch] = useReducer(rootReducer, undefined, { type: "DUMMY_INIT" }) return ( <div className="App"> <div> <h1>counter</h1> <div>count: {state.counter}</div> <button onClick={(e) => dispatch({ type: "INCREMENT" })}>+</button> <button onClick={(e) => dispatch({ type: "DECREMENT" })}>-</button> </div> <div> <h1>Input value</h1> <div>value: {state.someNested.inputValue}</div> <input value={state.someNested.inputValue} onChange={(e) => dispatch({ type: "UPDATE_VALUE", value: e.target.value }) } /> </div> </div> ) } 

I can got good result when pass dummy initialState(=undefined) and any dummy action.

const [state, dispatch] = useReducer(rootReducer, undefined, { type: "DUMMY_INIT" }) 

2: Create Provider with createContext and useContext

We can avoid passing props with context.

https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down

const ReducerContext = createContext() // Wrap Context.Provider const Provider = ({ children }) => { const [state, dispatch] = useReducer(rootReducer, undefined, { type: "DUMMY_INIT" }) return ( <ReducerContext.Provider value={{ state, dispatch }}> {children} </ReducerContext.Provider> ) } const App = () => { return ( <Provider> <div className="App"> <Counter /> <InputValue /> </div> </Provider> ) } 

For consumer, we can use useContext

const Counter = () => { const { state, dispatch } = useContext(ReducerContext) return ( <div> <h1>counter</h1> <div>count: {state.counter}</div> <button onClick={(e) => dispatch({ type: "INCREMENT" })}>+</button> <button onClick={(e) => dispatch({ type: "DECREMENT" })}>-</button> </div> ) } const InputValue = () => { const { state, dispatch } = useContext(ReducerContext) return ( <div> <h1>Input value</h1> <div>value: {state.someNested.inputValue}</div> <input value={state.someNested.inputValue} onChange={(e) => dispatch({ type: "UPDATE_VALUE", value: e.target.value }) } /> </div> ) } 

If you want use <Consumer>, like this.

 const Counter = () => { return ( <ReducerContext.Consumer> {({ state, dispatch }) => { return ( <div> <h1>counter</h1> <div>count: {state.counter}</div> <button onClick={(e) => dispatch({ type: "INCREMENT" })}>+</button> <button onClick={(e) => dispatch({ type: "DECREMENT" })}>-</button> </div> ) }} </ReducerContext.Consumer> ) } 

3. Emulate bindActionCreactors with useCallback

If we want bind action, we can use useCallback

 const increment = useCallback((e) => dispatch({ type: "INCREMENT" }), [ dispatch ]) const decrement = useCallback((e) => dispatch({ type: "DECREMENT" }), [ dispatch ]) const updateValue = useCallback( (e) => dispatch({ type: "UPDATE_VALUE", value: e.target.value }), [dispatch] ) return <div> : <button onClick={increment}>+</button> <button onClick={decrement}>-</button> : </div> 

4. Emulate mapStateToProps and reselect with useMemo

const InputValue = () => { const { state, dispatch } = useContext(ReducerContext) // memolized. revoke if change state.someNested.inputValue  const inputValue = useMemo(() => state.someNested.inputValue, [ state.someNested.inputValue ]) return ( <div> <h1>Input foo</h1> <div>foo: {inputValue}</div> <input value={inputValue} onChange={(e) => dispatch({ type: "UPDATE_VALUE", value: e.target.value }) } /> </div> ) } 

5. Emulate Container

 const useCounterContext = () => { const { state, dispatch } = useContext(ReducerContext) const counter = useMemo(() => state.counter, [state.counter]) const increment = useCallback( (e) => setTimeout(() => dispatch({ type: "INCREMENT" }), 500), [dispatch] ) const decrement = useCallback((e) => dispatch({ type: "DECREMENT" }), [ dispatch ]) return { counter, increment, decrement } } const Counter = () => { const { counter, increment, decrement } = useCounterContext() return ( <div> <h1>counter</h1> <div>count: {counter}</div> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> ) } //  const useInputContainer = () => { const { state, dispatch } = useContext(ReducerContext) // memolized action dispatcher const updateValue = useCallback( (e) => dispatch({ type: "UPDATE_VALUE", value: e.target.value }), [dispatch] ) // memolized value const inputValue = useMemo(() => state.someNested.inputValue, [ state.someNested.inputValue ]) return { updateValue, inputValue } } const InputValue = () => { const { updateValue, inputValue } = useInputContainer() return ( <div> <h1>Input foo</h1> <div>value: {inputValue}</div> <input value={inputValue} onChange={updateValue} /> </div> ) } 

Example code

https://stackblitz.com/edit/github-hgrund?file=src/App.js

Extra: middleware

Extra-1: Async fetch

We can emulate middleware with useEffect,But this may not recommended and we wait for Suspence


Reducer

const fetchedData = (state = {}, action) => { switch (action.type) { case "FETCH_DATA": return action.value } return state } 

We create async function return random value.

const fetchData = (dispatch) => { return new Promise((resolve) => { setTimeout(() => { resolve({ random: Math.random() }) }, 100) }) // Really: // return fetch("./async.json") // .then((res) => res.json()) // .then((data) => { // return data // }) } 

Container:
We want pass useEffect to empty array([]).
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

const useFetchDataContainer = () => { const { state, dispatch } = useContext(ReducerContext) // call on mount only useEffect(() => { fetchData().then((data) => { dispatch({ type: "FETCH_DATA", value: data }) }) }, []) const reload = useCallback(() => { fetchData().then((data) => { dispatch({ type: "FETCH_DATA", value: data }) }) }) const data = useMemo( () => { return JSON.stringify(state.fetchedData, null, 2) }, [state.fetchedData] ) return { data, reload } } const FetchData = () => { const { data, reload } = useFetchDataContainer() return ( <div> <h1>Fetch Data</h1> <pre> <code>{data}</code> </pre> <button onClick={reload}>Reload</button> </div> ) } 

Extra-2: Emulate custom middleware (like applyMiddleware)

If we need reducer middleware, we can wrap reducer dispatch

// my custom middleware const myMiddleware = (state, dispatch) => { return (action) => { if (action.type == "OOPS") { // fire action when `OOPS` action. dispatch({ type: "SET_COUNT", value: state.counter + 100 }) } } } const useEnhancedReducer = (reducer, enhancer) => { const [state, baseDispatch] = useReducer(reducer, undefined, { type: "DUMMY_INIT" }) const next = useMemo(() => enhancer(state, baseDispatch), [ state, baseDispatch ]) // wrapped dispatch const dispatch = useCallback((action) => { baseDispatch(action) next(action) }) return { state, dispatch } } const Provider = ({ children }) => { const { state, dispatch } = useEnhancedReducer(rootReducer, myMiddleware) const value = { state, dispatch } return ( <ReducerContext.Provider value={value}>{children}</ReducerContext.Provider>  ) } 

Top comments (2)

Collapse
 
budarin profile image
Вадим Бударин • Edited

for the emulating reselect

const { state, dispatch } = useContext(ReducerContext); const counter = useMemo(() => state.counter, [state.counter]) 

how to avoid re rendering every time the state is changed only when the particular slice of the state is changed?

Collapse
 
terrierscript profile image
terrierscript

Oh, I did not know about that behavior.I will try to find out about it.