首页 > 解决方案 > 使用 React 效果时状态未正确更新

问题描述

我正在开发一个小型 todo 应用程序作为使用 React 的练习。我有这样的模拟服务:

export default class TodoService {

    constructor(todos) {
        this.todos = new Map();
        todos.forEach(todo => {
            this.todos.set(todo.id, todo);
        });
    }

    findAll() {
        return Array.from(this.todos.values());
    }

    saveTodo(todo) {
        this.todos[todo.id] = todo
    }

    completeTodo(todo) {
        this.todos.delete(todo.id)
    }
}

在我的 React 应用程序中,我有一些包含待办事项的状态:

const [todos, setTodos] = useState([]);
const [flip, flipper] = useState(true);

const completeTodo = (todo) => {
    todoService.completeTodo(todo);
    flipper(!flip);
}

useEffect(() => {
    setTodos(todoService.findAll());
}, [flip])

completeTodoTodo是一个函数,当我想完成这样的待办事项时,我将它传递到我的组件中:

import React from "react";
const Todo = ({ todo, completeFn }) => {
    return (
        <form className="todo">
            <div className="form-check">
                <input
                    className="form-check-input"
                    type="checkbox"
                    value=""
                    name={todo.id}
                    id={todo.id}
                    onClick={() => {
                        console.log(`completing todo...`)
                        completeFn(todo)
                    }} />
                <label className="form-check-label" htmlFor={todo.id}>
                    {todo.description}
                </label>
            </div>
        </form>
    )
}
export default Todo

所以发生的情况是,每当用户单击复选框completeFntodo,它就会从服务对象中删除,并且状态应该更新,但最奇怪的事情发生了。

何时TodoService.completeTodo()调用 todo 被正确删除,但何时findAll()调用旧的 todo 仍然存在!如果我将内容写入控制台,我可以看到该项目被删除,然后在我调用时以某种方式传送回来findAll。为什么会这样?我这是因为一些我不明白的 React 魔法?

编辑:更疯狂的是,如果我将其修改为仅对初始加载使用效果,如下所示:

const [todos, setTodos] = useState([]);

const completeTodo = (todo) => {
    todoService.completeTodo(todo);
    setTodos(todoService.findAll());
}

useEffect(() => {
    setTodos(todoService.findAll());
}, [])

我得到一个非常奇怪的结果:

在此处输入图像描述

谁可以给我解释一下这个?

Edit2:这是一个完整的可重现示例(其中没有index.html带有 a <div id="root"></div>)。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const Todo = ({ todo, completeFn }) => {
    return (
        <div>
            <input
                type="checkbox"
                name={todo.id}
                id={todo.id}
                onClick={() => {
                    console.log(`completing todo...`)
                    completeFn(todo)
                }} />
            <label className="form-check-label" htmlFor={todo.id}>
                {todo.description}
            </label>
        </div>
    )
}

class TodoService {

    constructor(todos) {
        this.todos = new Map();
        todos.forEach(todo => {
            this.todos.set(todo.id, todo);
        });
    }

    findAll() {
        return Array.from(this.todos.values());
    }

    saveTodo(todo) {
        this.todos[todo.id] = todo
    }

    completeTodo(todo) {
        this.todos.delete(todo.id)
    }
}

const App = () => {

    let todoService = new TodoService([{
        id: 1,
        description: "Let's go home."
    }, {
        id: 2,
        description: "Take down the trash"
    }, {
        id: 3,
        description: "Play games"
    }]);

    const [todos, setTodos] = useState([]);
    const [flip, flipper] = useState(true);

    const completeTodo = (todo) => {
        todoService.completeTodo(todo);
        flipper(!flip);
    }

    useEffect(() => {
        setTodos(todoService.findAll());
    }, [flip])

    return (
        <div>
            {todos.map(todo => <Todo key={todo.id} todo={todo} completeFn={completeTodo} />)}
        </div>
    )
};
ReactDOM.render(<App />, document.getElementById("root"));

标签: javascriptreactjsreact-hooks

解决方案


useEffect在这种情况下您不需要调用。您已经在useEffect其中添加了一个依赖项,可以使用它来停止无限循环。但这里没有必要。你真的什么都没做fetch

您可以将代码更新为这样。

import React, { useState, useCallback, useEffect } from "react";

const Todo = ({ todo, completeFn }) => {
  const handleOnclick = useCallback(() => {
    // useCallback since function is passed down from parent
    console.log(`completing todo...`);
    completeFn(todo);
  }, [completeFn, todo]);

  return (
    <div>
      <input
        type="checkbox"
        name={todo.id}
        id={todo.id}
        onClick={handleOnclick}
      />
      <label className="form-check-label" htmlFor={todo.id}>
        {todo.description}
      </label>
    </div>
  );
};

class TodoService {
  constructor(todos) {
    this.todos = new Map();
    todos.forEach(todo => {
      this.todos.set(todo.id, todo);
    });
  }

  findAll() {
    console.log(Array.from(this.todos.values()));
    return Array.from(this.todos.values());
  }

  saveTodo(todo) {
    this.todos[todo.id] = todo;
  }

  completeTodo(todo) {
    this.todos.delete(todo.id);
  }
}

const todoService = new TodoService([
  {
    id: 1,
    description: "Let's go home."
  },
  {
    id: 2,
    description: "Take down the trash"
  },
  {
    id: 3,
    description: "Play games"
  }
]);

export default function App() {
  const [todos, setTodos] = useState([]); // Set initial state

  const completeTodo = todo => {
    todoService.completeTodo(todo);
    setTodos(todoService.findAll()); // Update state
  };

  useEffect(() => {
    setTodos(todoService.findAll());
  }, []); // Get and update from service on first render only

  return (
    <div>
      {todos.map(todo => (
        <Todo key={todo.id} todo={todo} completeFn={completeTodo} />
      ))}
    </div>
  );
}

工作示例

https://codesandbox.io/s/craky-hertz-sewc5?file=/src/App.js


推荐阅读