如何无Hack实现带依赖、内置逻辑与事件触发的自定义useReducer
无hack实现方案
核心思路两点:
- 用
useRef存储最新的props,不需要将props塞入状态,也不需要直接修改state - 把reducer改造为无外部依赖的纯函数,副作用(
onTextChanged回调)放到useEffect中触发,规避reducer多次执行导致的回调重复调用问题
完整实现代码如下:
import { useReducer, useRef, useEffect } from 'react'; interface TextEditorProps { disabled?: boolean onTextChanged?: (text: string) => void } interface TextState { text: string } interface TextAction { type: 'APPEND'|'UPPERCASE' payload?: string } // 纯函数reducer,无任何外部依赖,仅负责计算新状态 const textReducerFunc = (state: TextState, action: TextAction): TextState => { switch(action.type) { case 'APPEND': return { text: state.text + (action.payload ?? '') }; case 'UPPERCASE': return { text: state.text.toUpperCase() }; default: return state; } } const useTextProcessor = (initialText: string, props: TextEditorProps) => { // 用ref存储最新的props,更新ref不会触发重渲染 const propsRef = useRef(props); // 每次渲染更新ref,保证永远拿到最新的props引用 propsRef.current = props; const [state, originalDispatch] = useReducer(textReducerFunc, { text: initialText }); // 封装原生dispatch,加入disabled判断逻辑 const dispatch = (action: TextAction) => { if (propsRef.current.disabled) return; originalDispatch(action); } // 监听text变化,触发变更回调,仅在text实际修改时执行 useEffect(() => { propsRef.current.onTextChanged?.(state.text); }, [state.text]); return [ state.text, dispatch ] as const; } // 组件使用方式和原来完全一致,无需修改 export default function TxtEditor(props: TextEditorProps) { const [text, dispatch] = useTextProcessor('hello', props); return ( <div> <div>{text}</div> <button onClick={() => dispatch({type: 'APPEND', payload: ' world'})}>append 'world'</button> <button onClick={() => dispatch({type: 'UPPERCASE'})}>uppercase</button> </div> ) }
方案说明
- 用
useRef存props的方式完美解决了依赖传递问题,既不需要把props塞到state里,也不需要把reducer声明在hook内部导致重复创建 - 副作用从reducer中剥离后,reducer符合纯函数要求,即使React出于内部逻辑多次调用reducer也不会产生额外副作用,彻底解决回调触发两次的问题
- 无论传入的
onTextChanged是静态函数还是内联生成的函数,propsRef都能拿到最新的引用,不会出现回调过期的问题
内容的提问来源于stack exchange,提问作者Heyyy Marco
相关产品推荐
相关产品推荐

