React.js集成Redux及解决subscribe重复执行问题
React.js集成Redux及subscribe重复执行问题解决
问题根源
你遇到的store.subscribe执行次数累加的问题,核心原因是每次App组件重新渲染时,都会调用一次store.subscribe添加新的订阅函数。状态更新会触发所有已注册的订阅,导致执行次数越来越多(第一次更新1次,第二次2次,以此类推)。
解决方案一:修复手动订阅逻辑
利用React的useEffect钩子,确保仅在组件挂载时订阅一次,并在组件卸载时取消订阅,避免重复注册:
修改App组件代码如下:
const App = ({store}) => { const [todos, setTodos] = React.useState([]) const [goals, setGoals] = React.useState([]) React.useEffect(() => { // 订阅store变化,返回取消订阅的函数 const unsubscribe = store.subscribe(() => { const newState = store.getState() setTodos(newState.todos) setGoals(newState.goals) }) // 组件卸载时取消订阅 return unsubscribe }, [store]) // 依赖仅为store,确保只执行一次 return ( <div> <Todos items={todos} /> <Goals items={goals} /> </div> ) }
解决方案二:使用官方react-redux库(推荐)
手动订阅是Redux的基础用法,但在React项目中,官方推荐使用react-redux库简化集成,无需手动处理订阅、状态同步:
步骤1:引入react-redux库
在HTML的head中添加:
<script src="https://unpkg.com/react-redux@8.1.3/dist/react-redux.min.js"></script>
步骤2:用Provider包裹根组件
将store通过Provider注入整个应用,子组件可直接获取:
// Root const root = ReactDOM.createRoot(document.getElementById("app")) root.render( <ReactRedux.Provider store={store}> <App /> </ReactRedux.Provider> )
步骤3:用useSelector和useDispatch替代手动订阅和dispatch
修改App、Todos组件:
const App = () => { // 直接从store中选择所需状态 const todos = ReactRedux.useSelector(state => state.todos) const goals = ReactRedux.useSelector(state => state.goals) return ( <div> <Todos items={todos} /> <Goals items={goals} /> </div> ) } const Todos = ({items}) => { const newTodoInput = React.useRef() // 获取dispatch函数 const dispatch = ReactRedux.useDispatch() const handleAddTodo = () => { const newTodo = newTodoInput.current dispatch( addTodo({ id: (new Date()).getTime().toString(36), text: newTodo.value, complete: false }) ) newTodo.value = "" } const handleRemoveTodo = (e) => { dispatch(removeTodo(e.target.id)) } const handleToggle = (e) => { dispatch(toggleTodo(e.target.id)) } return ( <div> <h3>My Todos:</h3> <div> <input type="text" placeholder="new todo ..." ref={newTodoInput} /> <button onClick={handleAddTodo}>Save</button> </div> <List items={items} removeItem={handleRemoveTodo} toggle={handleToggle} /> </div> ) }
完整修改后的代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React + Redux Demo</title> <!-- Redux --> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js" integrity="sha512-1/8Tj23BRrWnKZXeBruk6wTnsMJbi/lJsk9bsRgVwb6j5q39n0A00gFjbCTaDo5l5XrPVv4DZXftrJExhRF/Ug==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <!-- react & react-dom --> <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> <!-- react-redux --> <script src="https://unpkg.com/react-redux@8.1.3/dist/react-redux.min.js"></script> <!-- Babel --> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> </head> <body> <div id="app"></div> <script type="text/babel"> // Redux部分保持不变 const [ADD_TODO, REMOVE_TODO, TOGGLE_TODO] = ["ADD_TODO", "REMOVE_TODO", "TOGGLE_TODO"] const [ADD_GOAL] = ["ADD_GOAL"] const addTodo = todo => ({type: ADD_TODO, todo}) const removeTodo = id => ({type: REMOVE_TODO, id}) const toggleTodo = id => ({type: TOGGLE_TODO, id}) const addGoal = goal => ({type: ADD_GOAL, goal}) const todosReducer = (state=[], action) => { switch (action.type) { case ADD_TODO: return [...state, action.todo] case REMOVE_TODO: return state.filter(s => s.id !== action.id) case TOGGLE_TODO: return state.map(s => s.id === action.id ? {...s, complete: !s.complete} : s) default: return state } } const goalsReducer = (state=[], action) => { switch (action.type) { case ADD_GOAL: return [...state, action.goal] default: return state } } const store = Redux.createStore( Redux.combineReducers({todos: todosReducer, goals: goalsReducer}) ) // React组件部分 const List = ({items, removeItem, toggle}) => { return ( <ul> {items.map(itm => ( <li key={itm.id}> <span id={itm.id} onClick={toggle || null} style={{textDecoration: itm.complete ? 'line-through' : ''}} > {itm.text} </span> {removeItem && <button id={itm.id} onClick={removeItem}> X </button>} </li> ))} </ul> ) } const Todos = ({items}) => { const newTodoInput = React.useRef() const dispatch = ReactRedux.useDispatch() const handleAddTodo = () => { const newTodo = newTodoInput.current if (!newTodo.value.trim()) return dispatch(addTodo({ id: (new Date()).getTime().toString(36), text: newTodo.value, complete: false })) newTodo.value = "" } const handleRemoveTodo = (e) => { dispatch(removeTodo(e.target.id)) } const handleToggle = (e) => { dispatch(toggleTodo(e.target.id)) } return ( <div> <h3>My Todos:</h3> <div> <input type="text" placeholder="new todo ..." ref={newTodoInput} /> <button onClick={handleAddTodo}>Save</button> </div> <List items={items} removeItem={handleRemoveTodo} toggle={handleToggle} /> </div> ) } const Goals = ({items}) => { return ( <div> <h3>My Goals:</h3> <List items={items} /> </div> ) } const App = () => { const todos = ReactRedux.useSelector(state => state.todos) const goals = ReactRedux.useSelector(state => state.goals) return ( <div> <Todos items={todos} /> <Goals items={goals} /> </div> ) } const root = ReactDOM.createRoot(document.getElementById("app")) root.render( <ReactRedux.Provider store={store}> <App /> </ReactRedux.Provider> ) </script> </body> </html>
关键说明
- 手动订阅时,
useEffect的依赖数组设为[store],确保仅在组件挂载时订阅一次,返回的unsubscribe函数会在组件卸载时执行,清理订阅。 - 使用
react-redux时,Provider将store注入上下文,useSelector自动监听状态变化,仅在所选状态更新时触发组件重渲染,useDispatch直接获取dispatch函数,无需手动传递store。
内容的提问来源于stack exchange,提问作者osomaher
相关产品推荐
相关产品推荐

