从子组件内的 useEffect 钩子调度数据时,UseReducer 被调用两次
UseReducer is called twice when dispatching data from useEffect hook within child component
我正在用 ReactJS 创建一个 todo/shopping 列表。除了能够通过输入手动将项目添加到列表之外,用户还应该能够以编程方式添加项目。
我正在使用 createContext()
和 useReducer
来管理 state()
。
当我通过 props 提供数组以编程方式添加项目并监听 useEffect
中的变化时,useEffect
和 dispatch
会触发两次,尽管我只更改了 props 一次.
然而,当我第一次通过道具提供项目数组时,这并没有发生。
因此,在第一次之后,当 dispatch
触发两次时,列表会得到重复项(也是重复键)。
是否由于某些我不知道的重新渲染过程而发生?非常感谢任何帮助,因为我真的坚持这个。
代码如下:
包含 useEffect 的上下文提供程序组件,当 props 发生变化时会触发 useReducer 的调度方法:
import React, { createContext, useEffect, useReducer } from 'react';
import todosReducer from '../reducers/todos.reducer';
import { ADD_INGREDIENT_ARRAY } from '../constants/actions';
const defaultItems = [
{ id: '0', task: 'Item1', completed: false },
{ id: '1', task: 'Item2', completed: false },
{ id: '2', task: 'Item3', completed: false }
];
export const TodosContext = createContext();
export const DispatchContext = createContext();
export function TodosProvider(props) {
const [todos, dispatch] = useReducer(todosReducer, defaultItems)
useEffect(() => {
if (props.ingredientArray.length) {
dispatch({ type: ADD_INGREDIENT_ARRAY, task: props.ingredientArray });
}
}, [props.ingredientArray])
return (
<TodosContext.Provider value={todos}>
<DispatchContext.Provider value={dispatch}>
{props.children}
</DispatchContext.Provider>
</TodosContext.Provider>
);
}
我的 reducer 函数(ADD_INGREDIENT_ARRAY
是从上面的代码片段调用的函数):
import uuidv4 from "uuid/dist/v4";
import { useReducer } from "react";
import {
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
EDIT_TODO,
ADD_INGREDIENT_ARRAY
} from '../constants/actions';
const reducer = (state, action) => {
switch (action.type) {
case ADD_TODO:
return [{ id: uuidv4(), task: action.task, completed: false }, ...state];
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.id);
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case EDIT_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, task: action.task } : todo
);
case ADD_INGREDIENT_ARRAY:
console.log('THE REDUCER WAS CALLED')
return [...action.task.map(ingr => ({ id: uuidv4(), task: ingr.name, completed: false }) ), ...state]
default:
return state;
}
};
export default reducer;
呈现每个项目并使用上面代码片段的上下文的列表组件:
import React, { useContext, useEffect, useState } from 'react';
import { TodosContext, DispatchContext } from '../contexts/todos.context';
import Todo from './Todo';
function TodoList() {
const todos = useContext(TodosContext);
return (
<ul style={{ paddingLeft: 10, width: "95%" }}>
{todos.map(todo => (
<Todo key={Math.random()} {...todo} />
))}
</ul>
);
}
export default TodoList;
并且包含列表的应用程序组件包装在传递道具的上下文提供者中:
import React, { useEffect, useReducer } from 'react';
import { TodosProvider } from '../contexts/todos.context';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
function TodoApp({ ingredientArray }) {
return (
<TodosProvider ingredientArray={ingredientArray}>
<TodoForm/>
<TodoList/>
</TodosProvider>
);
}
export default TodoApp;
以及传递 props 的顶级组件:
import React, { useEffect, useContext } from 'react';
import TodoApp from './TodoApp';
import useStyles from '../styles/AppStyles';
import Paper from '@material-ui/core/Paper';
function App({ ingredientArray }) {
const classes = useStyles();
return (
<Paper className={classes.paper} elevation={3}>
<div className={classes.App}>
<header className={classes.header}>
<h1>
Shoppinglist
</h1>
</header>
<TodoApp ingredientArray={ingredientArray} />
</div>
</Paper>
);
}
export default App;
创建 ingredientArray 的父组件。它获取 state.recipes 数组中的最后一个菜谱并将其作为 props 传递给 shoppingList:
...
const handleSetNewRecipe = (recipe) => {
recipe.date = state.date;
setState({ ...state, recipes: [...state.recipes, recipe] })
}
...
{recipesOpen ? <RecipeDialog
visible={recipesOpen}
setVisible={setRecipesOpen}
chosenRecipe={handleSetNewRecipe}
/> : null}
...
<Grid item className={classes.textAreaGrid}>
<ShoppingList ingredientArray={state.recipes.length ? state.recipes.reverse()[0].ingredients : []}/>
</Grid>
....
我做错了什么?
很高兴我们解决了这个问题。根据对 main post 的评论,直接改变 React 状态而不是通过 setter 函数更新它会导致状态的实际值与依赖组件和效果不同步树.
我仍然无法完全解释为什么在这种情况下它会导致您的特定问题,但无论如何,删除对 reverse
的可变调用并将其替换为这个简单的索引计算似乎已经解决了问题:
state.recipies[state.recipies.length-1].ingredients
我正在用 ReactJS 创建一个 todo/shopping 列表。除了能够通过输入手动将项目添加到列表之外,用户还应该能够以编程方式添加项目。
我正在使用 createContext()
和 useReducer
来管理 state()
。
当我通过 props 提供数组以编程方式添加项目并监听 useEffect
中的变化时,useEffect
和 dispatch
会触发两次,尽管我只更改了 props 一次.
然而,当我第一次通过道具提供项目数组时,这并没有发生。
因此,在第一次之后,当 dispatch
触发两次时,列表会得到重复项(也是重复键)。
是否由于某些我不知道的重新渲染过程而发生?非常感谢任何帮助,因为我真的坚持这个。
代码如下:
包含 useEffect 的上下文提供程序组件,当 props 发生变化时会触发 useReducer 的调度方法:
import React, { createContext, useEffect, useReducer } from 'react';
import todosReducer from '../reducers/todos.reducer';
import { ADD_INGREDIENT_ARRAY } from '../constants/actions';
const defaultItems = [
{ id: '0', task: 'Item1', completed: false },
{ id: '1', task: 'Item2', completed: false },
{ id: '2', task: 'Item3', completed: false }
];
export const TodosContext = createContext();
export const DispatchContext = createContext();
export function TodosProvider(props) {
const [todos, dispatch] = useReducer(todosReducer, defaultItems)
useEffect(() => {
if (props.ingredientArray.length) {
dispatch({ type: ADD_INGREDIENT_ARRAY, task: props.ingredientArray });
}
}, [props.ingredientArray])
return (
<TodosContext.Provider value={todos}>
<DispatchContext.Provider value={dispatch}>
{props.children}
</DispatchContext.Provider>
</TodosContext.Provider>
);
}
我的 reducer 函数(ADD_INGREDIENT_ARRAY
是从上面的代码片段调用的函数):
import uuidv4 from "uuid/dist/v4";
import { useReducer } from "react";
import {
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
EDIT_TODO,
ADD_INGREDIENT_ARRAY
} from '../constants/actions';
const reducer = (state, action) => {
switch (action.type) {
case ADD_TODO:
return [{ id: uuidv4(), task: action.task, completed: false }, ...state];
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.id);
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case EDIT_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, task: action.task } : todo
);
case ADD_INGREDIENT_ARRAY:
console.log('THE REDUCER WAS CALLED')
return [...action.task.map(ingr => ({ id: uuidv4(), task: ingr.name, completed: false }) ), ...state]
default:
return state;
}
};
export default reducer;
呈现每个项目并使用上面代码片段的上下文的列表组件:
import React, { useContext, useEffect, useState } from 'react';
import { TodosContext, DispatchContext } from '../contexts/todos.context';
import Todo from './Todo';
function TodoList() {
const todos = useContext(TodosContext);
return (
<ul style={{ paddingLeft: 10, width: "95%" }}>
{todos.map(todo => (
<Todo key={Math.random()} {...todo} />
))}
</ul>
);
}
export default TodoList;
并且包含列表的应用程序组件包装在传递道具的上下文提供者中:
import React, { useEffect, useReducer } from 'react';
import { TodosProvider } from '../contexts/todos.context';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
function TodoApp({ ingredientArray }) {
return (
<TodosProvider ingredientArray={ingredientArray}>
<TodoForm/>
<TodoList/>
</TodosProvider>
);
}
export default TodoApp;
以及传递 props 的顶级组件:
import React, { useEffect, useContext } from 'react';
import TodoApp from './TodoApp';
import useStyles from '../styles/AppStyles';
import Paper from '@material-ui/core/Paper';
function App({ ingredientArray }) {
const classes = useStyles();
return (
<Paper className={classes.paper} elevation={3}>
<div className={classes.App}>
<header className={classes.header}>
<h1>
Shoppinglist
</h1>
</header>
<TodoApp ingredientArray={ingredientArray} />
</div>
</Paper>
);
}
export default App;
创建 ingredientArray 的父组件。它获取 state.recipes 数组中的最后一个菜谱并将其作为 props 传递给 shoppingList:
...
const handleSetNewRecipe = (recipe) => {
recipe.date = state.date;
setState({ ...state, recipes: [...state.recipes, recipe] })
}
...
{recipesOpen ? <RecipeDialog
visible={recipesOpen}
setVisible={setRecipesOpen}
chosenRecipe={handleSetNewRecipe}
/> : null}
...
<Grid item className={classes.textAreaGrid}>
<ShoppingList ingredientArray={state.recipes.length ? state.recipes.reverse()[0].ingredients : []}/>
</Grid>
....
我做错了什么?
很高兴我们解决了这个问题。根据对 main post 的评论,直接改变 React 状态而不是通过 setter 函数更新它会导致状态的实际值与依赖组件和效果不同步树.
我仍然无法完全解释为什么在这种情况下它会导致您的特定问题,但无论如何,删除对 reverse
的可变调用并将其替换为这个简单的索引计算似乎已经解决了问题:
state.recipies[state.recipies.length-1].ingredients