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)
for the emulating reselect
how to avoid re rendering every time the state is changed only when the particular slice of the state is changed?
Oh, I did not know about that behavior.I will try to find out about it.