I'm following a React beginners tutorial making a todo app as an example.
in the App.js, there is a handleChange method that will update the state whether the checkbox is checked or not, and passes it into the TodoItem component
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
console.log(id)
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItems = this.state.todos.map(item => <TodoItem key={item.id} item={item} handleChange={this.handleChange}/>)
return (
<div className="todo-list">
{todoItems}
</div>
)
}
}
export default App
TodoItem.js
function TodoItem(props) {
return (
<div className="todo-item">
<input
type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<p>{props.item.text}</p>
</div>
)
}
export default TodoItem
it successfully displays the list and the console log correctly displays the checkbox clicked, however, the checkbox does not change. Can anyone tell me the problem?
I think you are running into a state mutation problem which is causing some unexpected behavior. The reason for this is because inside your if statement within map you are not returning your modified array item and you are actually modifying your state array and your new array.
How to fix: return your modified array item inside the if statement
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
return {
todos: updatedTodos
}
})
Or use a one liner with conditional (ternary) operator:
this.setState(prevState => ({
...prevState,
todos: prevState.todos.map(todo => todo.id === id ? { ...todo, ...{ completed: !todo.completed } } : todo)
}))
Look at this example I created in playground to get a better understanding:
I defined two arrays, one array uses map() without the return and the other array is using map() with the return.
Our goal is to keep our two arrays exactly the same and using map() to create a new modified array. Look at the log results and notice how our initial array gets modified aswell. Our second todo item in this array should have a completed value of true but it gets changed to false after our map() which we would want to avoid. By returning our array item in the correct way we avoid this.
To get a better understanding of what state mutation is and how to avoid check this.
Related
I have a config file which contains an array that gets data from redux
"datapath": [
"common.registration.rgsnInfoRs[0].value", // street name
"common.registration.rgsnInfoRs[1].value", // city
"common.registration.rgsnInfoRs[2].value", // state
"common.registration.rgsnInfoRs[3].value" // zip code
]
In my react component, when I tried to render the data from this array it only returns back the data for the zip code. How can I make my component render all of index data in my render method?
constructor(props) {
super(props);
this.state = {
// data: '',
data: []
}
}
componentDidMount(){
if(_.get(this.props, 'datapath')){
_.map(this.props.datapath, (item, i) => {
let data=_.get(this.props.state,`i`);
this.setState({data})
})
}
}
// static getDerivedStateFromProps(props, state) {
// if(_.get(props, 'datapath')){
// _.map(props.datapath, (item, i) => {
// let data=_.get(props.state,item);
// state.data = data;
// })
// }
// }
static getDerivedStateFromProps(props, state) {
if(_.get(props, 'datapath')){
_.map(props.datapath, (item, i) => {
let data=_.get(props.state,item);
this.setState((prevState) => {
return { data: [...prevState.data, data] }
});
})
}
}
render() {
const {type, label} = this.props;
return (
type === "grid" ? (
<Grid.Column style={style}>
<strong>{label ? `${label}:` : null}</strong> {this.state.data}
</Grid.Column>
) : (
null
)
)
}
Your initial data type for this.state.data is an array. But in the following line, it gets the value of the item (in this case, it's either street name, city, state or zipcode). These values are string type and override the this.state.data datatype from array to string.
let data=_.get(props.state, `i`); // This is incorrect
You should append the value in the state data instead of assign in componentDidMount
componentDidMount() {
if(_.get(this.props, 'datapath')){
_.map(this.props.datapath, (item, i) => {
let data=_.get(this.props.state,`i`);
this.setState((prevState) => {
return { data: [...prevState.data, data] }
});
})
}
}
Now Let's look at getDerivedStateFromProps function. This is a static function and you can not use this inside a static function. According to the react docs, this function returns the updated state.
static getDerivedStateFromProps(props, state) {
const newData = [];
if(_.get(props, 'datapath')) {
_.map(props.datapath, (item, i) => {
let data=_.get(props.state,item);
newData.push(data);
});
return { data: newData };
}
}
I think that you are not asking the right questions. It is generally a bad idea to store data from props in state because it can lead to stale and outdated data. If you can already access it from props, then you don't need to also have it in state.
Your component receive a prop state which is an object and a prop datapath which is an array of paths used to access properties on that state. This is already weird and you should look into better patterns for accessing redux data through selector functions.
But if we can't change that, we can at least change this component. You can convert an array of paths to an array of values with one line of code:
const data = this.props.datapath.map( path => _.get(this.props.state, path) );
All of the state and lifecycle in your component is unnecessary. It could be reduced to this:
class MyComponent extends React.Component {
render() {
const { type, label = "", state, datapath = [] } = this.props;
const data = datapath.map((path) => _.get(state, path, ''));
return type === "grid" ? (
<Grid.Column style={style}>
<strong>{label}</strong> {data.join(", ")}
</Grid.Column>
) : null;
}
}
I think, when you are looping through the data and setting the state you are overriding the old data present in the state already.
may be you will have to include the previous data as well as set new data in it something like this.
this.setState({data : [...this.state.data, data]})
I'm creating a todo app with reactjs and i'm trying to update my list of todo items when the user clicks on the input. When the user clicks I can change the item but the item does not get updated in state. I can't figure out why state is not updating. I think it has something to do with .map() and returning the todo object to the new array but I can't quite figure out what is going wrong. Any ideas?
App.js
import React from 'react'
import TodoItem from './components/TodoItem'
import TodosData from './TodosData'
import './App.scss'
class App extends React.Component {
constructor() {
super()
this.state = {
todoItems: TodosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState( prevState => {
const updatedTodos = prevState.todoItems.map( todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todoItems: updatedTodos
}
})
}
render() {
const todosComponents = this.state.todoItems.map( item => {
return <TodoItem key={item.id} todo={item} handleChange={this.handleChange}/>
})
return (
<div className="TodoList">
{todosComponents}
</div>
)
}
}
export default App;
TodosItem.js
import React from 'react'
function TodoItem(props) {
return (
<div className="TodoItem">
<input
type="checkbox"
checked={props.todo.completed}
onChange={ () => props.handleChange(props.todo.id) }
/>
<p>{props.todo.text}</p>
</div>
)
}
export default TodoItem
Issue
You are mutating your state objects. Since the nested object reference is the same React bails on rendering anything that may've updated. In other words, since the object reference is the same as the previous render React assumes nothing changed.
handleChange(id) {
this.setState( prevState => {
const updatedTodos = prevState.todoItems.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed // <-- object mutation!!
}
return todo
})
return {
todoItems: updatedTodos
}
})
}
Solution
Along with shallow copying the array you also need to shallow copy any nested state that you are updating.
handleChange(id) {
this.setState( prevState => {
const updatedTodos = prevState.todoItems.map(todo => {
if (todo.id === id) {
return {
...todo // <-- copy todo
completed: !todo.completed, // <-- update propety
}
}
return todo
})
return {
todoItems: updatedTodos
}
})
}
I've fetched a todo data from jsonplaceholder.typicode and I set up a action using redux fetching that data (I use thunk middleware). And I successfully get that data from jsonplaceholder to my component and I add a logic in reducer for toggling the todos:
//initState is { todos: [] }
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo => {
if (todo.id === action.payload) {
return {
...todo, completed: !todo.completed
}
}
return todo
})
}
But the problem is when I toggle a todo using checkbox, the 'line-through' style is blinking for some reason (it shows the strike in text but disappearing after I think .5 sec ) thats why I need a understanding why it happens, Is it because I fetched it in the internet? Or somethings wrong it the logic? Sorry for my noob question.
Here's my Todo component:
const dispatch = useDispatch()
const strikeStyle = {
textDecoration: todo.completed ? 'line-through' : ''
}
const onChangeHandler = () => {
dispatch(toggleTodo(todo.id))
}
...
<label>
<input onChange={onChangeHandler} type='checkbox' />
<p style={strikeStyle}>{todo.title}</p>
</label>
I just add a bracket at my useEffect because If I didn't add a bracket, I think it continuing to fetch the data and keeping the default value of all the data thats why its blinking.
const dispatch = useDispatch()
const todos = useSelector(state => state.TodoReducer.todos)
useEffect(() => {
dispatch(fetchTodoData())
}, [])
return (
<div>
{todos.map(todo => <Todo key={todo.id} todo={todo}/>)}
</div>
I think I've either misunderstood something, or am doing something deeply wrong, when attempting to subscribe to changes on a specific item in a collection in my store. Unless I add a direct list subscription, my component does not receive updates.
The following works:
const mapStateToProps = (state, props) => ({
list: state.podcasts.items,
getItem: props.id
? state.podcasts.items.filter(item => item.clientId === props.id)[0] || {}
: {},
});
If I remove the list item I only receive the the initial state of the collection item I'm subscribing to.
How I'm updating the list in the reducer:
PODCAST_GRADIENT_UPDATED: (state, { payload }) => ({
...state,
items: state.items.map(item => {
if (item.clientId === payload.clientId) {
item.gradient = payload.gradient; // eslint-disable-line
}
return item;
}),
}),
Should the above work without the list subscription?
if not, how should this be done?
this was a rookie error, in that there is some state mutation in the above example. changing my function in the reducer in the following way resolved this:
PODCAST_GRADIENT_UPDATED: (state, { payload }) => ({
...state,
items: state.items.map(item => {
if (item.clientId === payload.clientId) {
return { ...item, gradient: payload.gradient };
}
return { ...item };
}),
}),
notice specifically the use of the spread operator to create and return a new item.
I have this class:
class Board {
this.state = {
lists : [{
id: 0,
title: 'To Do',
cards : [{id : 0}]
}]
}
And want to use setState on the 'cards' array inside of the 'lists' state array. Previously, I had the cards array in a child component but I have now moved it up to the Board class. This is the function that I had before.
deleteCards(id){
this.setState({
cards: this.state.cards.filter(card => card.id !== id)
});
}
How can I change it so that it works now that cards is inside another array?
I was unable to solve it looking at these posts:
ReactJS - setState of Object key in Array
How to edit an item in a state array?
To do it all within setState (note that the first argument to setState is an updater function where its first argument is a reference to the previous state):
If you can provide the listId from the caller:
deleteCards(listId, cardId) {
this.setState(prevState => ({
lists: prevState.lists.map((list) => {
if (list.id !== listId) {
return list
}
return {
...list,
cards: list.cards.filter(card => card.id !== cardId)
}
})
}))
}
If you can not provide the listId from the caller:
deleteCards(id) {
this.setState(prevState => ({
lists: prevState.lists.map((list) => {
if (list.cards.some(card => card.id === id)) {
return {
...list,
cards: list.cards.filter(card => card.id !== id)
}
}
return list
})
}))
}
You should attempt to use the new rest and spread syntax...
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
const newListItem = {this.state.lists[0].cards.filter....}
this.setState({lists: [...this.state.lists.cards, newListItem]})
I would have made this a comment but it would be pretty hard to read. This is just an example you need to actually write a filter.