useEffect function not called after updating state of a 2d array - reactjs

I have a 2d array state full of objects rendered like this:
<div id="pieces">
{
//pieces of the board
pieces.map(pieceRow => {
return pieceRow.map(piece => {
return piece && <Piece key={piece.id} piece={piece}/>
})
})
}
</div>
The useEffect function is in the Piece Component:
function Piece({piece}){
const pieceRef = useRef(null);
useEffect(() => {
//this's the useEffect function, where piece is the obj of the 2d array passed by props to the component
...
}, [piece])
return ...
When i update the state moving an element from a row to another row the useEffect is called:
newPieces[3][0] = null;
newPieces[4][0] = obj;
[obj.x, obj.y] = [4, 0]
setPieces(newPieces);
This piece of code works, but if i try to move the element to another position in the same row the useEffect isn't called (but the state is updated).
This setPieces doesn't call the useEffect:
newPieces[3][0] = null;
newPieces[3][1] = obj;
[obj.x, obj.y] = [3, 1]
setPieces(newPieces);
Any suggestion?

It is because the id of the piece is the same. So React do not re-render the component. Try to include the row in the key. This way the component will re-render when the piece is moved inside its own row.
pieces.map(pieceRow => {
return pieceRow.map( (piece,index) => {
const key = `${piece.id}-row-${index}`;
return piece && <Piece key={key} piece={piece}/>
})
})
``

Related

React component not re-rendering on component state change

I am working on a sidebar using a recursive function to populate a nested list of navigation items.
Functionally, everything works except for the re-render when I click on one of the list items to toggle the visibility of the child list.
Now, when I expand or collapse the sidebar (the parent component with its visibility managed in its own state), the list items then re-render as they should. This shows me the state is being updated.
I have a feeling this possibly has something to do with the recursive function?
import React, { useState } from "react";
import styles from "./SidebarList.module.css";
function SidebarList(props) {
const { data } = props;
const [visible, setVisible] = useState([]);
const toggleVisibility = (e) => {
let value = e.target.innerHTML;
if (visible.includes(value)) {
setVisible((prev) => {
let index = prev.indexOf(value);
let newArray = prev;
newArray.splice(index, 1);
return newArray;
});
} else {
setVisible((prev) => {
let newArray = prev;
newArray.push(value);
return newArray;
});
}
};
const hasChildren = (item) => {
return Array.isArray(item.techniques) && item.techniques.length > 0;
};
const populateList = (data) => {
return data.map((object) => {
return (
<>
<li
key={object.name}
onClick={(e) => toggleVisibility(e)}
>
{object.name}
</li>
{visible.includes(object.name) ? (
<ul id={object.name}>
{hasChildren(object) && populateList(object.techniques)}
</ul>
) : null}
</>
);
});
};
let list = populateList(data);
return <ul>{list}</ul>;
}
export default SidebarList;
There are many anti patterns with this code but I will just focus on rendering issue. Arrays hold order. Your state does not need to be ordered so it's easier to modify it, for the case of demo I will use object. Your toggle method gets event, but you want to get DOM value. That's not necessary, you could just sent your's data unique key.
See this demo as it fixes the issues I mentioned above.

Re-render flat-list in functional component

I have an empty array that I pass to my flat-list.
I also use useEffect to fetch data from the server and update the list.
However, after setting the new state of the array with the data, the flat-list is not re-rendered.
const [listData, setlistData] = React.useState<Transaction[]>([])
const [dataUpdated, setDataUpdated] = React.useState<boolean>(false)
React.useEffect(() => {
if(route.params.showAllData)
{
fetchTransactions(1, TRANSACTION_PAGE_SIZE, 1)
.then((res: Transaction) => {
console.log(`TransactionsScreen: userEffect [] => fetched ${JSON.stringify(res)}`);
setlistData(prevState => ({...prevState, ...res}));
setDataUpdated(true)
})
.catch(err => {
//TODO: handle error scenerio
ToastAndroid.show(`Failed to fetch transacrions`, ToastAndroid.SHORT)
})
}}, [])
<FlatList
data={listData}
renderItem={item => _renderItem(item)}
ItemSeparatorComponent={TransactionListSeparator}
extraData={dataUpdated} // extraData={listData <--- didn't work either}
keyExtractor={item => item.id.toString()}/>
I tried to add the data array as extraData value but it didn't work, I also tried to add another boolean notifying that the data was updated but it didn't work either.
How can re-render the flat-list correctly?
You can force flatlist to rerender by passing the updated list as an extraData prop, i.e extraData={listData}. However, when using functional components a common mistake is passing the same instance of the list data as the prop. This will not trigger a rerender even if the content in the list or the length of the list has changed. FlatList sees this as the same and will not rerender.
To trigger a rerender you have to create an entirely new instance of the list.
Egs:
//This update will NOT trigger a rerender
const copy = listData;
copy.push(something)
setListData(copy)
////////
<FlatList extraData={listData}
.....
/>
//This WILL trigger a rerender
const copy = [...listData]
copy.push(something)
setListData(copy)
////////
<FlatList extraData={listData}
.....
/>
I solved the problem this way :
useEffect(() => {
setDataUpdated(!dataUpdated);
}, [listData]);
NOTE :
Your updating the dataUpdated to true directly. I do not recommend this, do it this way instead : setDataUpdated(!dataUpdated)
Plus ensure that all elements of the FlatList have a unique key, if not it will not re-render at any cost
Try this, make sure you are returning your child component and giving dependency array with useEffect upon which chnage you want to re-render your component
const [listData, setlistData] = React.useState<Transaction[]>([])
const [dataUpdated, setDataUpdated] = React.useState<boolean>(false)
React.useEffect(() => {
if(route.params.showAllData)
{
fetchTransactions(1, TRANSACTION_PAGE_SIZE, 1)
.then((res: Transaction) => {
console.log(`TransactionsScreen: userEffect [] => fetched ${JSON.stringify(res)}`);
setlistData(prevState => ({...prevState, ...res}));
setDataUpdated(true)
})
.catch(err => {
//TODO: handle error scenerio
ToastAndroid.show(`Failed to fetch transacrions`, ToastAndroid.SHORT)
})
}}, [listData])
return (
<FlatList
data={listData}
renderItem={item => _renderItem(item)}
ItemSeparatorComponent={TransactionListSeparator}
extraData={dataUpdated} // extraData={listData <--- didn't work either}
keyExtractor={item => item.id.toString()}/>
)
You can't spread the array as you did. For example, if you have two arrays.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
And spread them like:
const arr3 = { ...arr1, ...arr2 };
The result will be:
{ 0: 4, 1: 5, 2: 6 }
Change curly braces to the square.
setlistData(prevState => ([...prevState, ...res]));
Since your FlatList expects data to be an array, not an object.

My component is mutating its props when it shouldn't be

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.
propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

ReactJs UseState : insert element into array not updating

I am trying to use React Hooks but somehow my state is not updating. When I click on the checkbox (see in the example), I want the index of the latter to be added to the array selectedItems, and vice versa
My function looks like this:
const [selectedItems, setSelectedItems] = useState([]);
const handleSelectMultiple = index => {
if (selectedItems.includes(index)) {
setSelectedItems(selectedItems.filter(id => id !== index));
} else {
setSelectedItems(selectedItems => [...selectedItems, index]);
}
console.log("selectedItems", selectedItems, "index", index);
};
You can find the console.log result
here
An empty array in the result, can someone explain to me where I missed something ?
Because useState is asynchronous - you wont see an immediate update after calling it.
Try adding a useEffect which uses a dependency array to check when values have been updated.
useEffect(() => {
console.log(selectedItems);
}, [selectedItems])
Actually there isn't a problem with your code. It's just that when you log selectedItems the state isn't updated yet.
If you need selectedItems exactly after you update the state in your function you can do as follow:
const handleSelectMultiple = index => {
let newSelectedItems;
if (selectedItems.includes(index)) {
newSelectedItems = selectedItems.filter(id => id !== index);
} else {
newSelectedItems = [...selectedItems, index];
}
setSelectedItems(newSelectedItems);
console.log("selectedItems", newSelectedItems, "index", index);
};

Filtering an icon from an array of icon strings for re-render

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Resources