为什么Redux状态会影响点击处理函数触发的emit事件?
问题根因与解答
1. 为什么状态内容的不同会导致点击处理函数出现不一样的行为?
是两个代码逻辑缺陷叠加导致的:
- 第一,
pinFlow工厂函数违反了React Hook调用规则。所有React Hook(useState、useDispatch、useSelector、useEffect等)只能在React函数组件的顶层调用,不能在普通函数、工厂函数内部调用。你当前把useState、和注释块里的多个Hook都写在了pinFlow函数体中,而非它返回的组件内部,这些Hook实际会挂载到调用pinFlow的父组件Comp的Hook链上,状态和副作用的生命周期完全不符合预期的隔离要求。 - 第二,你在
Comp的渲染逻辑中动态生成ConfirmationModal组件:每次Comp重渲染时,只要protected为true,都会重新调用pinFlow生成一个全新的函数组件。React会判定这是不同的组件类型,直接销毁旧组件实例、重建新实例,之前保存的局部状态会直接丢失。
当/users请求成功时,Redux中pinAuth.users状态更新,所有订阅了该状态的组件都会触发重渲染,刚好你的匿名返回组件用useSelector读取了这个状态,就会触发上述销毁重建流程。此时你点击打开模态框的事件还在冒泡阶段,刚重建的模态框的点击外部关闭逻辑检测到了这个冒泡上来的点击事件,就直接触发了onDismiss,导致模态框刚打开就关闭。
而请求返回401时,pinAuth.users只会更新一次为失败状态,后续不会再触发重渲染,刚好避开了事件冒泡和组件重建的叠加问题,所以能正常工作。
2. 是不是我哪里考虑不周,混淆了Redux状态和实际运行逻辑?
和Redux本身无关,是你对React组件生命周期、Hook调用规则、组件类型判定的理解有疏漏,Redux的状态更新只是刚好触发了代码逻辑里的隐患。
3. 除了加event.stopPropagation()之外的解决方案
event.stopPropagation()只是临时规避方案,没有解决组件结构不合理的根本问题,最彻底的修复是改写pinFlow结构,把所有Hook移到返回的组件内部,同时避免每次渲染都生成新的组件:
// 修正后的pinFlow.tsx type States = | { state: 'Confirm' } | { state: 'SelectUser' } | { state: 'SubmitPin'; user: User }; export function pinFlow<T extends ConfirmationBoxProps>( ConfirmationBox: React.FC<T>, authorization: Authorization, ) { // pinFlow仅做参数闭包,所有逻辑移到返回的组件内部 return (props: T) => { const [state, setState] = React.useState<States>({ state: 'Confirm' }); const dispatch=useDispatch(); // 初始化用户数据 const users = useSelector((state: InitialState) => state.pinAuth.users); const filteredUsers = useSelector((state: InitialState) => mapRemoteData(state.pinAuth.users, users => users.filter(user => user.authorizations.includes(authorization)), ), ); const fetchUsers = useCallback(() => { dispatch(fetchUsersThunk()); }, [dispatch]); useEffect(() => { if (users.state === RemoteDataState.NotStarted) { fetchUsers(); } }, [fetchUsers, users.state]); switch (state.state) { case 'Confirm': { return ( <ConfirmationBox {...props} onSubmit={(_event: React.MouseEvent) => { setState({ state: 'SelectUser' }); }} /> ); } case 'SelectUser': { return ( <Modal title={'PIN Required'} canClickOutsideToDismiss={true} onDismiss={() => { setState({ state: 'Confirm' }); }} > <p className={style.selectProfileText}>Select your profile:</p> <pre> <code>{JSON.stringify(filteredUsers, null, 4)}</code> </pre> {/* <UserList users={filteredUsers.data} /> */} </Modal> ); } default: { return <Modal title="others">all others</Modal>; } } }; }
同时修改Comp组件的调用逻辑,用useMemo缓存生成的组件避免重复创建:
// 修正后的Comp调用 function Comp(){ const [selected, setSelected] = useState(); const [mode, setMode] = useState(); // 缓存pinFlow返回的组件,仅依赖变化时才重新生成 const ConfirmationModal = useMemo(() => { return protected ? pinFlow(MenuItemModal, permission) : MenuItemModal; }, [protected, permission]); return( <ConfirmationModal item={selected} mode={mode} disabled={availability.state === RemoteDataState.Loading} errorMessage={tryGetError(availability)} onCancel={() => { setMode(undefined); dispatch(resetAvailability()); }} onSubmit={(accessToken: string) => { dispatch(findAction(selected, mode, accessToken)); }} /> ) }
修改后组件的生命周期和Hook挂载完全符合React规则,不会出现不必要的销毁重建,自然不会触发意外的onDismiss事件。
内容的提问来源于stack exchange,提问作者StephenWeiss
相关产品推荐
相关产品推荐

