React简易Todo应用切换isCompleted状态时出现TypeError报错
Hey there! Let's break down exactly what's causing that frustrating error and get your todo app working smoothly.
1. Missing isCompleted when adding new todos
First up: when you create a new todo in your addTodo method, you're only setting the text property—but forgetting to include isCompleted: false. That means any new todo you add doesn't have the isCompleted field at all. When your component tries to render item.isCompleted for these new items, it throws an error because the property doesn't exist on the object.
Fix: Update your addTodo method to include the isCompleted property, and use prevState to get the latest newTodo value (safer than relying on this.state directly):
addTodo = event => { event.preventDefault(); this.setState(prevState => ({ newTodo: "", todoList: [...prevState.todoList, { text: prevState.newTodo, isCompleted: false }] })); };
2. Empty map function in toggleTodo
Right now, your toggleTodo method has an empty map callback—so when you call setState, it replaces your todoList with an array full of undefined values. That's the main reason you're seeing "Cannot read property 'isCompleted' of undefined" after trying to toggle a todo.
On top of that, using event.target.parentNode.id to get the todo ID is fragile. If someone clicks the text inside the toggle button (like "( )"), event.target might be a text node instead of the button, leading to incorrect parent node references. A better approach is to pass the todo's index directly from the render method.
Fix:
First, modify the TodoListTable to pass the index when triggering toggleTodo:
<button className="toggleTodo" onClick={() => props.toggleTodo(index)}> {item.isCompleted ? "(X)" : "( )"} </button>
Then update the toggleTodo method to properly toggle the isCompleted state for the correct todo:
toggleTodo = (todoIndex) => { this.setState(prevState => ({ todoList: prevState.todoList.map((todo, i) => { if (i === todoIndex) { // Create a new object with toggled isCompleted value return { ...todo, isCompleted: !todo.isCompleted }; } // Return unchanged todos for other indices return todo; }) })); };
(You can remove event.preventDefault() here—buttons inside a list don't trigger form submissions by default.)
3. Bonus: Make removeTodo more reliable
Same issue with your removeTodo method—let's fix it to use the index directly instead of DOM nodes:
Update the delete button in TodoListTable:
<button className="deleteTodo" onClick={() => props.removeTodo(index)}> X </button>
Then update the removeTodo method:
removeTodo = (todoIndex) => { this.setState(prevState => ({ todoList: prevState.todoList.filter((_, i) => i !== todoIndex) })); };
4. One more thing: Missing newTodo prop in Input
You were also missing the newTodo prop in your Input component, which would have caused the input field to not clear correctly after submitting a todo. Fix that in your App render method:
<Input addTodo={this.addTodo} handleChange={this.handleChange} newTodo={this.state.newTodo} />
Full Corrected Code
Here's the complete working code with all these fixes:
import React, { Component } from 'react'; const Input = (props) => { return ( <form onSubmit={props.addTodo}> <input placeholder="What needs to be done?" value={props.newTodo} className="new-todo" type="text" onChange={props.handleChange} /> <input type="submit" value="Submit" /> </form> ); } const TodoListTable = props => { return ( <ul className="todo-list"> {props.todoList.map((item, index) => { return ( <li key={index}> <button className="toggleTodo" onClick={() => props.toggleTodo(index)}> {item.isCompleted ? "(X)" : "( )"} </button> <span className="textTodo">{item.text}</span> <button className="deleteTodo" onClick={() => props.removeTodo(index)}> X </button> </li> ); })} </ul> ); }; const FilterListTable = props => { return <ul className="filters"> <li> <a href="#/">All</a> </li> <li> <a href="#/active">Active</a> </li> <li> <a href="#/completed">Completed</a> </li> </ul>; }; class App extends Component { constructor(props) { super(props); this.state = { newTodo: "", todoList: [ { text: "zrobić pranie", isCompleted: false }, { text: "zrobić lunch", isCompleted: true }, { text: "zrobić pranie", isCompleted: false } ] } } handleChange = event => { this.setState({ newTodo: event.target.value }); }; addTodo = event => { event.preventDefault(); this.setState(prevState => ({ newTodo: "", todoList: [...prevState.todoList, { text: prevState.newTodo, isCompleted: false }] })); }; editTodo = event => { // event.preventDefault(); // console.log(this.state.todoList); }; toggleTodo = (todoIndex) => { this.setState(prevState => ({ todoList: prevState.todoList.map((todo, i) => { if (i === todoIndex) { return { ...todo, isCompleted: !todo.isCompleted }; } return todo; }) })); }; removeTodo = (todoIndex) => { this.setState(prevState => ({ todoList: prevState.todoList.filter((_, i) => i !== todoIndex) })); }; render() { return ( <div className="todoapp"> <h1>React Todo</h1> <Input addTodo={this.addTodo} handleChange={this.handleChange} newTodo={this.state.newTodo} /> <TodoListTable todoList={this.state.todoList} toggleTodo={this.toggleTodo} removeTodo={this.removeTodo} /> <hr /> <FilterListTable /> <br /> </div> ); } } export default App;
These changes should resolve all your errors and let you toggle, add, and delete todos without issues!
内容的提问来源于stack exchange,提问作者Henry Case

