使用useReducer Hook时如何实现类似mapStateToProps的选择性响应式渲染?
嘿,我刚上手React Hooks研究useReducer的时候,也遇到过这个困惑——默认情况下只要state有变化,整个组件就会重新渲染,哪怕组件只用到了state里没变化的属性。其实我们可以通过几个小技巧实现类似Redux里mapStateToProps的效果,让组件只对关心的state属性变化做出响应,下面一步步来给你演示:
问题场景:默认的全量重渲染
先看一个基础示例,当state里的任意属性更新时,组件都会无条件重渲染:
import { useReducer } from 'react'; const initialState = { count: 0, username: 'kartik' }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'UPDATE_USERNAME': return { ...state, username: action.payload }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); console.log('Counter组件重渲染了'); // 哪怕只更新username,这里也会打印 return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'INCREMENT' })}>加1</button> </div> ); }
这里哪怕我们只修改username,Counter组件也会触发重渲染,因为useReducer的state是整体更新的,组件会默认重新执行。
解决方案1:拆分组件 + React.memo(最推荐)
把只关心特定state属性的部分拆成独立子组件,用React.memo包裹——它会对组件的props做浅比较,只有当props变化时才会重渲染子组件:
import { useReducer, memo } from 'react'; const initialState = { count: 0, username: 'kartik' }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'UPDATE_USERNAME': return { ...state, username: action.payload }; default: throw new Error(); } } // 只关心count的子组件,用memo包裹 const CounterDisplay = memo(({ count }) => { console.log('CounterDisplay组件重渲染了'); return <p>Count: {count}</p>; }); // 只关心username的子组件,用memo包裹 const UsernameDisplay = memo(({ username }) => { console.log('UsernameDisplay组件重渲染了'); return <p>Username: {username}</p>; }); function App() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <CounterDisplay count={state.count} /> <button onClick={() => dispatch({ type: 'INCREMENT' })}>加1</button> <UsernameDisplay username={state.username} /> <button onClick={() => dispatch({ type: 'UPDATE_USERNAME', payload: 'newKartik' })}> 修改用户名 </button> </div> ); }
现在点击“加1”时,只有CounterDisplay会重渲染;点击“修改用户名”时,只有UsernameDisplay会重渲染,完美实现了类似mapStateToProps的精准响应。
解决方案2:自定义useSelector风格Hook(无需拆分组件)
如果不想拆分组件,我们可以自己实现一个类似Redux useSelector的Hook,用来筛选需要的state属性并做比较,只有当筛选结果变化时才返回新值:
import { useReducer, useMemo, useRef } from 'react'; // 自定义useSelector:筛选state并做浅比较 function useSelector(state, selector) { const selected = selector(state); const prevSelected = useRef(selected); // 浅比较当前和历史选中值,更新缓存 if (JSON.stringify(selected) !== JSON.stringify(prevSelected.current)) { prevSelected.current = selected; } // 用useMemo缓存结果,只有选中值变化时才更新 return useMemo(() => prevSelected.current, [selected]); } const initialState = { count: 0, username: 'kartik' }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'UPDATE_USERNAME': return { ...state, username: action.payload }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); // 只选择count属性,只有count变化时,这个值才会更新 const count = useSelector(state, s => s.count); // 用useMemo缓存渲染内容,只有count变化时才重新生成 const counterContent = useMemo(() => { console.log('Counter内容更新了'); return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch({ type: 'INCREMENT' })}>加1</button> </div> ); }, [count]); return counterContent; }
注意:这种方案下组件本身还是会触发重渲染,但我们可以用useMemo把依赖特定state的渲染内容缓存起来,避免不必要的DOM更新。如果想要完全阻止组件重渲染,还是推荐拆分组件+React.memo的方案。
补充:全局状态场景下的优化(配合Context)
如果是用Context全局共享useReducer的state和dispatch,同样可以结合上面的技巧实现精准响应:
import { useReducer, createContext, useContext, memo } from 'react'; const StateContext = createContext(); const DispatchContext = createContext(); const initialState = { count: 0, username: 'kartik' }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'UPDATE_USERNAME': return { ...state, username: action.payload }; default: throw new Error(); } } function AppProvider({ children }) { const [state, dispatch] = useReducer(reducer, initialState); return ( <StateContext.Provider value={state}> <DispatchContext.Provider value={dispatch}> {children} </DispatchContext.Provider> </StateContext.Provider> ); } // 全局useSelector function useSelector(selector) { const state = useContext(StateContext); return useMemo(() => selector(state), [state, selector]); } function useDispatch() { return useContext(DispatchContext); } // 用memo包裹的Counter组件 const Counter = memo(() => { const count = useSelector(s => s.count); const dispatch = useDispatch(); console.log('Counter组件重渲染了'); return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch({ type: 'INCREMENT' })}>加1</button> </div> ); }); // 用memo包裹的Username组件 const Username = memo(() => { const username = useSelector(s => s.username); const dispatch = useDispatch(); console.log('Username组件重渲染了'); return ( <div> <p>Username: {username}</p> <button onClick={() => dispatch({ type: 'UPDATE_USERNAME', payload: 'newKartik' })}> 修改用户名 </button> </div> ); }); function App() { return ( <AppProvider> <Counter /> <Username /> </AppProvider> ); }
这里Counter和Username组件只会在各自关心的state属性变化时重渲染,完全符合我们想要的响应式效果。
总的来说,核心思路就是拆分单一职责组件+React.memo,或者用useMemo缓存筛选后的state/渲染内容,这两种方式都能帮我们实现类似mapStateToProps的精准更新,避免不必要的性能损耗。
内容的提问来源于stack exchange,提问作者kartik shah

