Nested component list does not update correctly - javascript

I have a recursively defined component tree which is something like this:
class MyListItem extends Component {
...
componentDidMount() {
this.listener = dataUpdateEvent.addListener(event, (newState) => {
if(newState.id == this.state.id) {
this.setState(newState)
}
})
}
...
render() {
return (
<div>
<h1>{this.state.title}</h1>
<div>
{this.state.children.map( child => {
return (<MyListItem key={child.id} data={child} />)
})}
</div>
</div>
)
}
}
So basically this view renders a series of nested lists to represent a tree-like data structure. dataUpdateEvent is triggered various ways, and is intended to trigger a reload of the relevant component, and all sub-lists.
However I'm running into some strange behavior. Specifically, if one MyListItem component and its child update in quick succession, I see the top level list change as expected, but the sub-list remains in an un-altered state.
Interestingly, if I use randomized keys for the list items, everything works perfectly:
...
return (<MyListItem key={uuid()} data={child} />)
...
Although there is some undesirable UI lag. My thought is, maybe there is something to do with key-based caching that causes this issue.
What am I doing wrong?

React uses the keys to map changes so you need those. There should be a warning in the console if you don't use unique keys. Do you have any duplicate ids? Also try passing all your data in as props instead of setting state, then you won't need a listener at all.

Related

Update list display in Next.js when passing data from children

I have a Page component in Next.js that looks somewhat like this:
export default function Index({ containers }) {
const [containerListState, setContainerListState] = useState(containers);
const updateContainerList = (container) => {
containers.push(container);
setContainerListState(containers);
};
return (
<>
<FormCreateContainer updateContainerList={updateContainerList} />
<ContainerList containers={containerListState} />
</>
);
}
FormCreateContainer allows for the creation of a container. When that API call resolves, I call updateContainerList and pass the newly created container to the parent. This works.
However, I also want to pass this container to ContainerList (which is a simple dynamic list using containers.map()) as part of the containers prop. While pushing to containers works as intended, Next does not live-update my list of containers; the newly created container only shows up when I reload the page.
I thought that including useEffect and changing updateContainerList as follows might work, but alas it did not.
const updateContainerList = (container) => {
containers.push(container);
};
useEffect(() => {
setContainerListState(containers);
}, [containers]);
How do I correctly pass data from a child to a parent component, therethrough passing it to a different child component and updating a dynamic list without reloading the page myself?
I really do appreciate any help as I have done extensive research that did not help me achieve my goal.
First and foremost, you should never mutate the value of a prop.
I think you should just use setContainerListState to update the data without mutating the prop like this:
const updateContainerList = (container) => {
setContainerListState(containers => [...containers, container]);
};
This will re-render your component and all its children automatically.

React ref inside of a loop breaks on re render

I have a gallery component that takes in an array of components. In each of the child components I am assigning a ref. The reason for this is because within the child component there are many other children components and I am attempting to access some functions on a component that is about 5 component deep. The below code shows the initial setup:
export class Gallery extends React.Component {
render() {
const galleryItems = data.map((item, index) => {
return (
<GalleryItem
ref={React.createRef()}
/>
);
});
return (
<div >
<Gallery
items={heroGalleryItems}
/>
</div>
);
}
}
When the Gallery component renders all the refs in the array of GalleryItem component are correct. But as soon as the Gallery component re renders for any reason the refs in the GalleryItem components become null values.
I have tried several things in the children components but nothing I do fixes the issue. I believe the reason is because something is happening in the code above.
I have also tried to change up the code after reading the following:
Issue storing ref elements in loop
However its not really clear to me what the person is saying to do when I look at my own implementation.
You need to move out React.createRef() from the loop (and also render) as it is creating a new ref on every render.
Depending on your code/usage, you'd need to do this in constructor and CWRP methods (basically whenever data changes).
Then creating galleryItems would be like
...
<GalleryItem ref={item.ref} />
...

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: loading and updating nested resources when URI changes without excess duplication?

Let's say I have a nested URI structure, something like the following:
http://example.com/collections/{id}
http://example.com/collections/{collectionId}/categories/{id}
http://example.com/collections/{collectionId}/categories/{categoryId}/book/{id}
I can use react-router to render the correct component on page load, and when the URI changes.
Let's take the first case:
http://example.com/collections/{id}
Let's assume we have a CollectionShow component.
When the component first loads, I can pull the collection ID out of the URI and load the correct collection:
componentDidMount () {
this.loadCollection(this.props.match.params.id);
}
(Assume that loadCollection loads a collection with an AJAX call and sets it into the component's state.)
However, when the URI changes (through, e.g., the user clicking on a <Link>, react-router doesn't entirely re-build the component, it simply updates its props, forcing it to rerender. So, in order to update the compomnent's state, we also need to update the state on update:
componentDidUpdate(prevProps) {
if (!this.state.collection || this.collectionDidChange(prevProps)) {
this.loadCollection(this.props.match.params.id);
}
}
collectionDidChange(prevProps) {
return String(prevProps.match.params.id) !== String(this.props.match.params.id)
}
So far so good. But what about the second URL?
http://example.com/collections/{collectionId}/categories/{id}
Let's assume we have a CategoryShow component.
Now we don't only have to consider the collectionId changing, but also the category ID. We have to reload the collection if that ID changes, and we also have to reload the category if that changes.
The problem compounds with a third-level nesting (a BookShow component). We end up with something like this:
componentDidUpdate(prevProps) {
if (!this.state.collection || this.collectionDidChange(prevProps)) {
this.loadCollection(this.props.match.params.collectionId);
}
if (!this.state.category || this.collectionDidChange(prevProps) || this.categoryDidChange(prevProps)) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId);
}
if (!this.state.book || this.collectionDidChange(prevProps) || this.categoryDidChange(prevProps) || this.bookDidChange(prevProps)) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId)
.then(() => this.loadBook(this.props.match.params.id);
}
}
Not only is this unwieldy, it also results in a fair amount of code duplication across the three components, CollectionShow, CategoryShow and BookShow.
Using redux won't help matters much, because we still have to update the global state when the URI changes.
Is there a clean, efficient, React-friendly way of handling updates of nested resources such as these?
You could create a CollectionPage component that handles all the AJAX calls and keeps data in state.
This could pass down the collection, category/categories and books to the components (CollectionShow, CategoryShow and BookShow).
In CollectionPage you could use componentDidUpdate and componentDidMount as you presented it.
Your <*>Show components will know nothing about props.match.params.* and will only get the data needed to render the wanted content.
CollectionPage can be use for all your routes or you could change the route to something like
/collections/:collectionId?/:categoryId?/:bookId?
making all params options. You can check for the available ids in CollectionPage.
Hope it helps!
If I understood your problem it is something architectural. The parent component is the one that should be doing this management and injecting the result through subcomponents. Split your component in small components and render each one accordingly.
The code you shared will be splint in 3 others
The mponentDidUpdate(prevProps) method will go to the parent component simply as a componentDidMount().
Then if the router changes the component will be recreated and the new values will be sent across the modules.
If you dont wanna split you code you should at least do the step 2.
//everytime you get to the router this will be triggered and depending of the parameters of your router, you get the values you need and set the state
componentDidMount() {
if (!this.state.collection) {
this.loadCollection(this.props.match.params.collectionId);
}
if (!this.state.category) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId);
}
if (!this.state.book) {
this.loadCollection(this.props.match.params.collectionId)
.then(() => this.loadCategory(this.props.match.params.categoryId)
.then(() => this.loadBook(this.props.match.params.id);
}
}
render() {
return (
//you can add conditions to render as well
<CollectionComponent {...this.props} {...{
collection: this.collection
}} />
<CategoryComponent {...this.props} {...{
categ: this.categ
}} />
<BookComponent {...this.props} {...{
book: this.book
}} />
)
}

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