从子组件内的 useEffect 钩子调度数据时,UseReducer 被调用两次

UseReducer is called twice when dispatching data from useEffect hook within child component

我正在用 ReactJS 创建一个 todo/shopping 列表。除了能够通过输入手动将项目添加到列表之外,用户还应该能够以编程方式添加项目。

我正在使用 createContext()useReducer 来管理 state()

当我通过 props 提供数组以编程方式添加项目并监听 useEffect 中的变化时,useEffectdispatch 会触发两次,尽管我只更改了 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