React gives wrong index to Child with React.memo - javascript

I've got a problem in my project which is pretty large therefore I can't post every part of the code here, but I'll try to summarize the main parts.
I've got a parent component which state is managed through a useReducer and which render returns a mapping of this state.
Each child has to take both the value and the index of the mapping. I'm also using the Context API to pass the dispatcher
on some of the childs.
function ParentComponent(props) {
const [state, dispatch] = useReducer(initialData, reducer);
return (
<div>
<MyContext.Provider value={dispatch}>
{state.myArray.map((value, index) => (
<ChildComponent value={value} index={index} />
))}
</MyContext.Provider>
</div>
);
}
/* Other file */
function ChildComponent({value, index}) {
const dispatch = useContext(MyContext);
return <div>
{/* Uses value and index to display some data */}
</div>
}
export default React.memo(ChildComponent, (prevProps, nextProps) => !_.isEqual(prevProps, nextProps));
Some of the childs in the tree components have to use React.memo to avoid useless re-renders, ChildComponent is one of them.
I'm using the lodash functions _.isEqual to compare the props to know when the component has to re-render.
The problem is the following. One of my components in the component tree adds items to the myArray attribute of the ParentComponent state.
When I add an item, anyway, each ChildComponent rendered from the ParentComponent mapping receives the same index of 0, creating a lot of problems.
The ParentComponent has the right indices (when I log them inside the mapping they are the right ones) but it's like the ChildComponent isn't getting it.
Is there any way to solve this? Maybe it has something to do with React.memo?

React.memo() takes a callback that should determine whether prevProps and nextProps are equal, but you're returning the negation of that.
Also using the index as a key should be considered a last resort, since this will fail to behave correctly when elements in the array are re-ordered relative to each other. You should always source the key from the data whenever possible.

Related

React.useMemo does not work to prevent re-renders

I have the following simple component:
const Dashboard = () => {
const [{ data, loading, hasError, errors }] = useApiCall(true)
if (hasError) {
return null
}
return (
<Fragment>
<ActivityFeedTitle>
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
}
export default Dashboard
I would like to prevent ALL re-renders of the ActivityFeedTitle component, so that it only renders once, on load. My understanding is that I should be able to use the React.useMemo hook with an empty dependencies array to achieve this. I changed by return to be:
return (
<Fragment>
{React.useMemo(() => <ActivityFeedTitle>, [])}
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
As far as I'm concerned, this should prevent all re-renders of that component? However, the ActivityFeedTitle component still re-renders on every render of the Dashboard component.
What am I missing?
EDIT:
Using React.memo still causes the same issue. I tried memoizing my ActivityFeedTitle component as follows:
const Memo = React.memo(() => (
<ActivityFeedTitle />
))
And then used it like this in my return:
return (
<Fragment>
{<Memo />}
<ActivityFeed data={data} isLoading={loading} />
</Fragment>
)
Same problem occurs. I also tried passing in () => false the following as the second argument of React.memo, but that also didn't work.
Use React.memo() instead to memoized components based on props.
React.memo(function ActivityFeedTitle(props) {
return <span>{props.title}</span>
})
Take note:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
The second argument passed to React.memo would need to return true in order to prevent a re-render. Rather than computing whether the component should update, it's determining whether the props being passed are equal.
Your usage of useMemo is incorrect.
From react hooks doc:
Pass a “create” function and an array of dependencies. useMemo will
only recompute the memoized value when one of the dependencies has
changed. This optimization helps to avoid expensive calculations on
every render.
If no array is provided, a new value will be computed on every render.
You need to use useMemo like useEffect here for computation of value rather than rendering the component.
React.memo
React.memo() is the one you are looking for. It prevents re-rendering unless the props change.
You can use React.memo and use it where your define your component not where you are making an instance of the component, You can do this in your ActivityFeedTitle component as
const ActivityFeedTitle = React.memo(() => {
return (
//your return
)
})
Hope it helps
It's because rendering a parent causes it's children to re-render for the most part. The better optimization here would be to place your data fetching logic either in the ActivityFeed component or into a HOC that you wrap ActivityFeed in.

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

Share data between React components with no relation?

I'm working on a React component library that allows for client-side data filtering by passing an array of objects and an <input/> as props to a <SearchFilter/> component. I want to return the filtered results to a separate <SearchResults/> component that can be rendered elsewhere in the tree (i.e. the results component doesn't have to be a child of the input component).
I've got the filtering figured out, but I'm not sure the best route to take in React on getting the filtered data to the <SearchResults/> component.
This is what I'd like to end up with...
<SearchFilter
data={data}
input={<input type="text" value={value} onChange={this.handleChange}/>}
/>
Then, using Render Props to return the data and map over that to return JSX, there would be the results component. Something like this...
<SearchResults
render={data => (
data.map(el => (
<div>
<span>{data.someProperty}</span>
</div>
)
)}
/>
This is what I'd like to achieve because I want to allow for rendering the <SearchFilter/> component at one place in the tree, and allow the <SearchResults/> component to be rendered elsewhere, so that there's maximum flexibility in how the tree is composed and, therefore, how the view is rendered.
I've looked into the Context API, but it seems like that would require a handful more components to be a part of my library, which further complicates what I'm trying to achieve. If that's the only way around it, then that's fine, but I wanted to ask and see if anyone can think of another solution.
Thanks!
The bigger issue is that you will need to manage a state that is shared between components on a higher level, i.e., any component that will wrap these other two components, ultimately. With plain React, this state would be managed by the parent (or ancestor) component, passing down the relevant values as props. This opposed to the, usually bad, idea to have sibling components influence each other's state, since you well get into the "who's boss here"-problem.
The thing the Context API handles is not having to pass down props for things that typically don't change (or: typically shouldn't cause renders to trigger often).
A global state store, such as Redux, can help you modelling this, but in essence it's not much more than 'a' component managing state, and other components rendering according to that state. Events within the lower components trigger changes in the data, which will cause the state to change, which will cause the props of the children to change, which then will cause re-renders.
I'd advise you to try using this simple pattern:
class Search ... {
state = {data: [], text: ""}
render() {
return (
<div>
<SearchFilter
data={this.state.data}
onSearch={() => this.fetchNewData()}
onChange={(e) => this.setState({text: e.targetElement.value})}
text={this.state.text}
/>
<SearchResults data={this.state.data} />
</div>
);
}
fetchNewData() {
fetch('/url?text=' + this.state.text)
.then((newData) => { this.setState({data: newData}); })
}
}
Something along these lines. If you have trouble modelling stuff like this, you can use Redux to force you to do it in a similar way, and avoid managing local state intermixing with global state (which is typically something that is hard to manage).
If you do this right, components that have no state (i.e., aren't responsible for managing state and thus have no event handlers) can all become pure components, i.e. stateless components, i.e. functions that return JSX based on props:
const SearchResults = ({data}) => <div>{data.map( () => <etc /> )}</div>
You could create a data store class that holds your filter, pass it in as a property to both components, and have your SearchFilter component change a value in that.

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