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.