React - adding key interferes with state of children - javascript

having a parent element which requires a key (because it's in a list creating a warning message), I added key={uuid.v4()}.
This made the message disappear.
Funny things started happening with it's child components, though. When I use the functional setState hook, it doesn't actually assign it to the value anymore (see below [1]). When I remove the key from the parent Component, the same code works (but leaves me with the warning).
When adding a static key, e.g. key={'someComponent'}, the whole component doesn't render at all.
Any tips what I'm missing here?
[1] Child Component which does not update it's state:
function zoomIntoRegion(countryName, filter) {
props.changeFilter({SLMetric: filter})
if (regionZoom === countryName && filter === props.filter) {
setRegionZoom(undefined)
} else {
console.log('before setzoom', countryName) // # before setzoom GERMANY
setRegionZoom(countryName)
console.log('after', regionZoom) // # after undefined
}
}

Keys in React are used for quickly comparing the current children to the children before any change occured. When you set the key on a component to uuid.v4() in the render function, every time the parent component rerenders, you can see that it generates a new key. From the docs:
Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.
which seems to accurately define what you are facing.
To work around this problem,
If you do not see the order of your children changing at all, then it is okay to use index as key.
If you do see it changing and have no key in the data that you can use, then move the key generation setup to where you are fetching/generating the data so that it is executed only once. Ensure that keys do not change on render, but only when the data itself is changing.
function App() {
const [items, setItems] = useState([]);
useEffect(() => {
getData()
// mapping over array data and adding the key here
.then((data) => data.map((item) => ({...item, id: uuid.v4() })))
.then((data) => setItems(data))
}, []);
return (
<Fragment>
// using the key
{items.map((item) => {
<div key={item.id}>
</div>
})}
</Fragment>
)
}

Related

Filtering Table data with useEffect Hook causing component to re-render infinitely

I'm trying to use a search bar component to dynamically filter the content of a table that's being populated by API requests, however when I use this implementation the component re-renders infinitely and repeatedly sends the same API requests.
The useEffect() Hook:
React.useEffect(() => {
const filteredRows = rows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
});
if (filteredRows !== rows){
setRows(filteredRows);
}
}, [rows, search]);
Is there something I've missed in this implementation that would cause this to re-render infinitely?
Edit 1:
For further context, adding in relevant segments of code from that reference this component which might cause the same behaviour.
Function inside the parent component that renders the table which calls my API through a webHelpers library I wrote to ease API request use.
function fetchUsers() {
webHelpers.get('/api/workers', environment, "api", token, (data: any) => {
if (data == undefined || data == null || data.status != undefined) {
console.log('bad fetch call');
}
else {
setLoaded(true);
setUsers(data);
console.log(users);
}
});
}
fetchUsers();
Edit 2:
Steps taken so far to attempt to fix this issue, edited the hook according to comments:
React.useEffect(() => {
setRows((oldRows) => oldRows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
}));
}, [search]);
Edit 3:
Solution found, I've marked the answer by #Dharmik pointing out how Effect calls are managed as this caused me to investigate the parent components and find out what was causing the component to re-render repeatedly. As it turns out, there was a useEffect hook running repeatedly by a parent element which re-rendered the page and caused a loop of renders and API calls. My solution was to remove this hook and the sub-components continued rendering as they should without loops.
It is happening because you've added rows to useEffect dependency array and when someone enters something into search bar, The rows get filtered and rows are constantly updating.
And because of that useEffect is getting called again and again. Remove rows from the useEffect dependency array and it should work fine.
I would like to complement Dharmik answer. Dependencies should stay exhaustive (React team recomendation). I think a mistake is that filteredRows !== rows uses reference equality. But rows.filter(...) returns a new reference. So you can use some kind of deep equality check or in my opinion better somethink like:
React.useEffect(() => {
setRows((oldRows) => oldRows.filter((row) => {
return row.name.toLowerCase().includes(search.toLowerCase());
}));
}, [search]);

how to use react memo

I am trying to make a simple task manager app and I want to implement react memo in TaskRow (task item) but when I click the checkbox to finish the task, the component properties are the same and I cannot compare them and all tasks are re-rendered again, any suggestions? Thanks
Sand Box: https://codesandbox.io/s/interesting-tharp-ziwe3?file=/src/components/Tasks/Tasks.jsx
Tasks Component
import React, { useState, useEffect, useCallback } from 'react'
import TaskRow from "../TaskRow";
function Tasks(props) {
const [taskItems, setTaskItems] = useState([])
useEffect(() => {
setTaskItems(JSON.parse(localStorage.getItem('tasks')) || [])
}, [])
useEffect(() => {
if (!props.newTask) return
newTask({ id: taskItems.length + 1, ...props.newTask })
}, [props.newTask])
const newTask = (task) => {
updateItems([...taskItems, task])
}
const toggleDoneTask = useCallback((id) => {
const taskItemsCopy = [...taskItems]
taskItemsCopy.map((t)=>{
if(t.id === id){
t.done = !t.done
return t
}
return t
})
console.log(taskItemsCopy)
console.log(taskItems)
updateItems(taskItemsCopy)
}, [taskItems])
const updateItems = (tasks) => {
setTaskItems(tasks)
localStorage.setItem('tasks', JSON.stringify(tasks))
}
return (
<React.Fragment>
<h1>learning react </h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
{
props.show ? taskItems.map((task, i) =>
<TaskRow
task={task}
key={task.id}
toggleDoneTask={()=>toggleDoneTask(task.id)}>
</TaskRow>)
:
taskItems.filter((task) => !task.done)
.map((task) =>
<TaskRow
show={props.show}
task={task}
key={task.id}
toggleDoneTask={()=>toggleDoneTask(task.id)}></TaskRow>
)
}
</tbody>
</table>
</React.Fragment>
)
}
export default Tasks
Item task (TaskRow component)
import React, { memo } from 'react'
function TaskRow(props) {
return (<React.Fragment>
{console.log('render', props.task)}
<Tr show={props.show} taskDone={props.task.done}>
<td>
{props.task.title}
</td>
<td>
{props.task.description}
</td>
<td>
<input type="checkbox"
checked={props.task.done}
onChange={props.toggleDoneTask}
/>
</td>
</Tr>
</React.Fragment>)
}
export default memo(TaskRow, (prev,next)=>{
console.log('prev props', prev.task)
console.log('next props', next.task)
})
I'm afraid there are quite a lot of problems with the code you've shared, only some of them due to React.memo. I'll start there and work through the ones I've spotted.
You've provided an equality testing function to memo, but you are not using it to test anything. The default behaviour, which requires no testing function, will shallowly compare props between the previous and the next render. This means it will pick up on differences between primitive values (e.g. string, number, boolean) and references to objects (e.g. literals, arrays, functions), but it will not automatically deeply compare those objects.
Remember, memo will only allow rerenders when the equality testing function returns false. You've provided no return value to the testing function, meaning it returns undefined, which is falsy. I've provided a simple testing function for an object literal with primitive values which will do the job needed here. If you have more complex objects to pass in the future, I suggest using a comprehensive deep equality checker like the one provided by the lodash library, or, even better, do not pass objects at all if you can help it and instead try to stick to primitive values.
export default memo(TaskRow, (prev, next) => {
const prevTaskKeys = Object.keys(prev.task);
const nextTaskKeys = Object.keys(next.task);
const sameLength = prevTaskKeys.length === nextTaskKeys.length;
const sameEntries = prevTaskKeys.every(key => {
return nextTaskKeys.includes(key) && prev.task[key] === next.task[key];
});
return sameLength && sameEntries;
});
While this solves the initial memoisation issue, the code is still broken for a couple of reasons. The first is that despite copying your taskItems in toggleTaskDone, for similar reasons to those outlined above, your array of objects is not deeply copied. You are placing the objects in a new array, but the references to those objects are preserved from the previous array. Any changes you make to those objects will be directly mutating the React state, causing the values to become out of sync with the rest of your effects.
You can solve this by mapping the copy and spreading the objects. You would have to do this for every level of object reference in any state object you attempt to change, which is part of the reason that React advises against complex objects in useState (one level of depth is usually fine).
const taskItemsCopy = [...taskItems].map((task) => ({ ...task }));
Side Note: You are not doing anything with the result of taskItemsCopy in your original code. map is not a mutating method - calling it without assigning the result to a variable does nothing.
The next issue is more subtle, and demonstrates one of the pitfalls and potential complications when memoising your components. The toggleTaskDone callback has taskItems in its dependency array. However, you are passing it as a prop in an anonymous function to TaskRow. This prop is not being considered by React.memo - we're specifically ignoring it because we only want to rerender on changes to the task object itself. This means that when a task does change its done status, all the other tasks are becoming out of sync with the new value of taskItems - when they change their done status, they will be using the value of taskItems as it was the last time they were rendered.
Inline anonymous functions are recreated on every render, so they are always unequal by reference. You could actually fix this somewhat by adjusting the way the callback is passed and executed:
// Tasks.jsx
toggleDoneTask={toggleDoneTask}
// TaskRow.jsx
onChange={() => props.toggleDoneTask(props.task.id)}
In this way you would be able to check for reference changes in your memo equality function, but since the callback changes every time taskItems changes, this would make the memoisation completely useless!
So, what to do. This is where the implementation of the rest of the Tasks component starts to limit us a bit. We can't have taskItems in the dependency of toggleTaskDone, and we also can't call updateItems because that has the same (implicit) dependency. I've provided a solution which technically works, although I would consider this a hack and not really recommended for actual usage. It relies on the callback version of setState which will allow us to have access to the current value of taskItems without including it as a dependency.
const toggleDoneTask = useCallback((id) => {
setTaskItems((prevItems) => {
const prevCopy = [...prevItems].map((task) => ({ ...task }));
const newItems = prevCopy.map((t) => {
if (t.id === id) t.done = !t.done;
return t;
});
localStorage.setItem("tasks", JSON.stringify(newItems));
return newItems;
});
}, []);
Now it doesn't matter that we aren't equality checking the handler prop, because the function never alters from the initial render of the component. With these changes implemented my fork of your sandbox seems to be working as expected.
On a broader note, I really think you should consider writing React code using create-react-app when you're learning the framework. I was a little surprised to see you had a custom webpack set up, and you don't seem to have proper linting for React (bundled automatically in CRA) which would highlight a lot of these issues for you as warnings. Specifically, the misuse of the dependency array in a number of places in the Task component which is going to make it unstable and error prone even with the essential fixes I've suggested.

Changing React state happens a re-render later, then it should

I have a functional component managing several states, amongst other things a state, which stores the index, with which I am rendering a type of table, when clicking a suitable button. OnClick that button calls a callback function, which runs a click handler. That click handler changes the index state, to the same 'index' as the array entry, in which I store an object with information for the rendering of a child component.
I would expect, that onClick the state would change before the rendering happens, so the component could render correctly. Yet it only happens a render later.
I already tried calling the useEffect-hook, to re-render, when that index state changes, but that didn't help neither.
Here is a shortened version of the code:
export const someComponent = () => {
[index, setIndex] = useState(-1);
const handleClick = (id) => {
setIndex(id);
// This is a function, I use to render the table
buildNewComponent(index);
}
}
Further 'down' in the code, I got the function, which is rendering the table entries. There I pass the onClick prop in the child component of the table as following:
<SomeEntryComponent
onClick={() => handleClick(arrayEntry.id)}
>
// some code which is not affecting my problem
</SomeEntryComponent>
So as told: when that onClick fires, it first renders the component when one presses it the second time, because first then the state changes.
Could anyone tell me why that happens like that and how I could fix it to work properly?
As other have stated, it is a synchronicity issue, where index is being updated after buildComponent has been invoked.
But from a design standpoint, it would be better to assert index existence by its value, as opposed to flagging it in a handler. I don't know the details behind buildComponent, but you can turn it into a conditional render of the component.
Your component rendering becomes derived from its data, as opposed to manual creation.
export const someComponent = () => {
[index, setIndex] = useState(-1);
const handleClick = (id) => setIndex(id);
const indexHasBeenSelected = index !== -1
return (
<div>
{indexHasBeenSelected && <NewComponent index={index} />}
</div>
)
}
When calling buildNewComponent the index is not yet updated. You just called setState, there is no guarantee that the value is updated immediately after that. You could use id here or call buildNewComponent within a useEffect that has index as its dependency.
I believe that you can have the correct behavior if you use useEffect and monitor index changes.
export const someComponent = () => {
[index, setIndex] = useState(-1);
useEffect(() => {
// This is a function, I use to render the table
buildNewComponent(index);
}, [buildNewComponent, index])
const handleClick = (id) => {
setIndex(id);
}
}
The process will be:
When the use clicks and call handleClick it will dispatch setIndex with a new id
The index will change to the same id
The useEffect will see the change in index and will call buildNewComponent.
One important thing is to wrap buildNewComponent with useCallback to avoid unexpected behavior.

Children prop triggering re-render when using map and redux

I am using Redux to create a quiz app that includes a form with some nested fields. I have just realized (I think) that every key press to my input fields triggers a re-render if I use the children prop, i.e. designing the app like this:
const keys = Object.keys(state)
<QuizContainer>
{keys.map(key =>
<QuizForm key={key}>
{state[key].questions.map(({ questionId }) =>
<Question key={questionId} questionId={questionId}>
{state[key]questions[questionId].answers.map(({ answerId })=>
<Answer answerId={answerId} key={answerId} />
)}
</Question>
)}
</QuizForm>
)}
</QuizContainer>
QuizContainer is connected to redux with mapStateToProps and mapDispatchToProps and spits out an array of arrays that all have objects inside them. The store structure is designed according to Dan Abramov's "guide to redux nesting" (using the store kind of like a relational database) and could be described like this.
{
['quizId']: {
questions: ['questionId1'],
['questionId1']:
{ question: 'some question',
answers: ['answerId1', 'answerId2']
},
['answerId1']: { some: 'answer'},
['answerId2']: { some: 'other answer'}
}
The code above works in terms of everything being updated etc, etc, no errors but it triggers an insane amount of re-renders, but ONLY if I use the composition syntax. If I put each component inside another (i.e. not using props.children) and just send the quizId-key (and other id-numbers as props) it works as expected - no crazy re-rendering. To be crystal clear, it works when I do something like this:
// quizId and questionId being passed from parent component's map-function (QuizForm)
const Question ({ answers }) =>
<>
{answers.map(({ answerId }) =>
<Answer key={answerId} answerId={answerId} />
)}
</>
const mapStateToProps = (state, { questionId, quizId }) => ({
answers: state[quizId][questionId].answers
})
export default connect(mapStateToProps)(Question)
But WHY? What is the difference between the two? I realize that one of them is passed as a prop to the parent instead than being rendered as the child of that very parent, so to speak, but why does that give a different result in the end? Isn't the point that they should be equal but allow for better syntax?
Edit: I can now verify that the children prop is causing the problem. Setting
shouldComponentUpdate(nextProps) {
if (nextProps.children.length === this.props.children.length) {
return false
} else {
return true
}
}
fixes the problem. However, seems like a pretty black-box solution, not really sure what I am missing out on right now...
When you use the render prop pattern you are effectively declaring a new function each time the component renders.
So any shallow comparison between props.children will fail. This is a known drawback to the pattern, and your 'black-box' solution is a valid one.
Okay so I figured it out:
I had done two bad things:
I had my top component connected to state like so: mapStateToProps(state) => ({keys: Object.keys(state)}). I thought the object function would return a "static" array and prevent me from listening to the entire state but turns out I was wrong. Obviously (to me now), every time I changed the state I got a fresh array (but with the same entries). I now store them once on a completely separate property called quizIds.
I put my map-function in a bad place. I now keep render the QuizContainer like so:
<QuizContainer>
{quizIds.map(quizId =>
<QuizForm>
<Question>
<Answer />
</Question>
</QuizForm>
)}
</QuizContainer>
And then I render my arrays of children, injecting props for them to be able to use connect individually like so:
{questions.map((questionId, index) => (
<React.Fragment key={questionId}>
{React.cloneElement(this.props.children, {
index,
questionId,
quizId
})}
</React.Fragment>
))}
That last piece of code will not work if you decide to put several elements as children. Anyway, looks cleaner and works better now! :D

React is rerendering my list even though each child in array has its unique key

So, as far as I understand react only rerenders new elements with new keys. Thats not working for me though.
I have a list of posts, that are limited to 3.
When the user scrolls to bottom of page I add 3 to the limit, which means at the bottom of the page 3 older posts are supposed to be shown.
What I have now works, but the entire list is being rerendered. And it jumps to the top which is also not wanted (this I can fix though, main problem is the rerendering). They all have unique keys. How can I prevent this behaviour?
thisGetsCalledWhenANewPostComesIn(newPost){
let newPosts = _.clone(this.state.posts);
newPosts.push(newPost);
newPosts.sort((a,b) => b.time_posted - a.time_posted);
this.setState({posts: newPosts});
}
render(){
return (
<div ref={ref => {this.timelineRef = ref;}} style={styles.container}>
{this.state.posts.map(post =>
<Post key={post.id} post={post} />
)}
</div>
);
}
Having unique keys alone does not prevent rerendering components that have not changed. Unless you extend PureComponent or implement shouldComponentUpdate for the components, React will have to render() the component and compare it to the last result.
So why do we need keys when it's really about shouldComponentUpdate?
The purpose of giving each component in a list a unique key is to pass the props to the "right" component instances, so that they can correctly compare new and old props.
Imagine we have a list of items, e.g.:
A -> componentInstanceA
B -> componentInstanceB
C -> componentInstanceC
After applying a filter, the list must be rerendered to show the new list of components, e.g.:
C -> ?
Without proper unique keys, the component that previously rendered A will now receive the prop(s) for C. Even if C is unchanged, the component will have to rerender as it received completely different data:
C -> componentInstanceA // OH NO!
With proper unique keys, the component that rendered C will receive C again. shouldComponentUpdate will then be able to recogize that the render() output will be the same, and the component will not have to rerender:
C -> componentInstanceC
If your list of items take a long time to render, e.g. if it's a long list or each element is a complex set of data, then you will benefit from preventing unnecessary rerendering.
Personal anecdote
In a project with a list of 100s of items which each produced 1000s of DOM elements, changing from
list.map((item, index) => <SomeComp key={index} ... />)
to
list.map(item => <SomeComp key={item.id} ... />)
reduced the rendering time by several seconds. Never use array index as key.
You will have to implement shouldComponentUpdate(nextProps, nextState) in the Post component. Consider extending the PureComponent class for the Post component instead of the default React Component.
Good luck!
PS: you can use a string as ref parameter for your div in the render method like so:
render() {
return (
<div
ref='myRef'
style={styles.container}
>
{this.getPostViews()}
</div>
);
}
Then, if you want to refer to this element, use it like this.refs.myRef. Anyway, this is just a personal preference.
Okay, my bad. I thought I'd only post the "relevant" code, however it turns out, the problem was in the code I left out:
this.setState({posts: []}, ()=> {
this.postListenerRef = completedPostsRef.orderByChild('time')
.startAt(newProps.filter.fromDate.getTime())
.endAt(newProps.filter.toDate.getTime())
.limitToLast(this.props.filter.postCount)
.on('child_added', snap => {
Database.fetchPostFromKey(snap.key)
.then(post => {
let newPosts = _.clone(this.state.posts);
newPosts.push(_.assign(post, {id: snap.key}));
newPosts.sort((a,b) => b.time_posted - a.time_posted);
this.setState({posts: newPosts});
}).catch(err => {throw err;});
});
});
I call setState({posts: []}) which I am 99% sure is the problem.

Categories