I described my problem on stack overflow.
Is there a solution for this problem?
Sorry but I'm going to close this. GH issues are meant for bug reports and discussing feature requests, not for promoting Stack Overflow questions.
_(I answered in StackOverflow and copying my answer here to help folks who end up here from Google or GitHub searches. BTW this issue is a dupe of #94.)_
@acerola1 - To prevent react-window from re-rendering every item in the list, you need to:
itemData prop to communicate between the parent component and per-row render function.This is the strategy used in the react-window example sandbox here: https://codesandbox.io/s/github/bvaughn/react-window/tree/master/website/sandboxes/memoized-list-items
Here's an example using your code: https://codesandbox.io/s/a-quick-react-tree-component-based-on-react-window-8psp0
There may be a simpler way to do it using useMemo but I couldn't figure that out.
If you examine the DOM nodes using Chrome dev tools, you'll see that DOM nodes are no longer being re-created for the entire tree when one node is expanded. This removes the flicker that you'd see before (in Chrome, but not in Firefox) when selecting a new node.
But there's also something else that's preventing your rotation animations from working properly. In the CodeSandbox example I linked above, I added some ugly animations on the border properties to show how animation of some CSS properties is working, but animation of the transform CSS properties is not working. I suspect this problem has nothing to do with react-window, so you'll probably want to debug it separately to figure out why your transforms aren't animating but other properties are animating OK.
Very helpful response. Thanks, @justingrant!
Hi @bvaughn - Happy to help. I ran into the same problem and spent a bunch of time figuring out the answer, so wanted to leave bread crumbs for the next victim. Out of curiosity, do you know why React's reconciliation re-creates those DOM nodes when there's no memoization on the Row function? Even if each row is re-rendered, if the same content is being output into the DOM, why isn't reconciliation smart enough to make it a no-op?
For example, using the approach in your memoization example, even if I change itemData (which causes a re-render of the entire List), only deltas are applied to the DOM. But with the OP's original code, all DOM nodes inside the List are re-created every time the List re-renders. Why is the behavior different? I'm curious about this question in the context of react-window, but also wondering if I'm misunderstanding something more fundamental about how React works.
Short answer: Inline functions create a _new element type_ each render. React responds to this by unmounting and remounting the entire tree below the new type (just as it would if you rendered a Bar component where you had previous rendered a Foo component). This is just part of how React works when deciding whether to _create_ or _update_ a node in the tree. (This may be a little easier to think about if you imagine the type being a class component rather than a function component. You probably wouldn't expect React to try to re-use an _instance of Foo_ created in one render for the output of an _instance of Bar_ created in another.)
This is why all of the examples on react-window's documentation site avoid showing inline functions. (This is one of the cases where they actually _do_ matter for perf.)
That being said, maybe this API design is a decision I'll revisit when I eventually look at doing a v2 since a "small" mistake here can have a relatively big impact on performance. Sebastian and I were talking about this general API just today during lunch. It's something I need to give more thought to I guess. I think there are pros and cons to both approaches.
Also related: this thread
@bvaughn - Ah, OK that clears up one mystery. React doesn't actually look at the DOM output of a function component-- instead it assumes that if the functions are different (!==) then unmounting and remounting is required. Right?
Has the React team considered changing this behavior so that nested function components can participate in DOM reconciliation somehow? I searched through existing issues but didn't find a match. I know the current behavior matches how class components work, but inline classes aren't really a thing while inline functions are used frequently. If the problem just caused extra renders then it wouldn't be a big deal, but breaking reconciliation is much more impactful: it's 10x+ slower than a non-DOM render, it can cause flashing or visual jank, and it breaks CSS transitions and animations.
Also, nested function components are probably going to get more common with the increasing use of Hooks, because if you nest components then you can simply use hook data (via closures) which is much easier than refactoring nested components out to module scope.
If changing behavior isn't practical, could an ESLint rule warn about this case?
Regardless of any possible future changes, the current behavior seems like important info to have in the docs, given that many devs' mental model was probably like mine: that React diffs the DOM in all cases. What do you think? I can PR some content if you think that'd be helpful. Something like this:
React will only reconcile DOM nodes for a function component if the exact same function (f1 === f2) was used in the previous render. If the functions are different, React considers the components to be different types, just like a <Foo /> class component instance is different from a <Bar /> class component instance. Even if two functions emit the same DOM nodes, React will still unmount (destroy) all DOM nodes rendered by the old function and mount (create) DOM nodes rendered by the new function.
Unfortunately, this means that function components created inside other functions will not participate in DOM reconciliation, because inline functions are re-created when the parent function runs. For example, the following function component will cause React to always re-create the DOM nodes inside the Child component whenever Parent is rendered:
function Parent() {
const Child = () => <div>DOM nodes are always re-created</div>;
return <div><Child /></div>;
}
Re-creating DOM nodes during rendering is often bad. It's much slower than reconciled renders, it can cause visual jank or flashing, and it can break CSS transitions and other animations. To ensure that function components do participate in DOM reconciliation, you can call the function directly:
return <div>{ Child() }</div>; // Good. Child DOM nodes are reconciled.
return <div><Child /></div>; // Bad. Child DOM nodes are always re-created.
Or you can refactor the component to module scope:
function Child() {
return <div>DOM nodes are never re-created</div>
}
function Parent() {
return <div><Child /></div>
}
Note that how the function is defined doesn't matter. None of the functions below will be reconciled between renders.
function Parent() {
const Child1 = () => <div>DOM nodes are always re-created</div>;
const Child2 = function () { return <div>DOM nodes are always re-created</div>; }
function Child3() { return <div>DOM nodes are always re-created</div>; }
return <div><Child1 /><Child2 /><Child3 /></div>;
}
You might think that React.memo could help, but memoization isn't useful if the function is re-memoized every time the parent function runs.
// Correct. Child1 will always be the same function.
const Child1 = React.memo (() => {
return <div>DOM nodes are never re-created</div>
});
function Parent() {
// Wrong. Every time Parent renders, Child2 will be a different function.
const Child2 = React.memo(() => <div>DOM nodes are always re-created</div>);
return <div><Child1 /><Child2 /></div>;
}
Finally, a tip: to identify inline-function components that must be refactored to prevent the problems above, search your JSX files for indented declarations of functions whose names start with a capital letter. You can use a regular expression like this: \s\s(const|var|let|function) [A-Z][a-z ]
@justingrant This is extremely helpful and ought to be highlighted in the docs.
For me (and I'm sure more than a few people might make this mistake too) - I was correctly memoizing the row-rendering function, but it was in the same scope as the parent function.
Most helpful comment
_(I answered in StackOverflow and copying my answer here to help folks who end up here from Google or GitHub searches. BTW this issue is a dupe of #94.)_
@acerola1 - To prevent react-window from re-rendering every item in the list, you need to:
itemDataprop to communicate between the parent component and per-row render function.This is the strategy used in the react-window example sandbox here: https://codesandbox.io/s/github/bvaughn/react-window/tree/master/website/sandboxes/memoized-list-items
Here's an example using your code: https://codesandbox.io/s/a-quick-react-tree-component-based-on-react-window-8psp0
There may be a simpler way to do it using
useMemobut I couldn't figure that out.If you examine the DOM nodes using Chrome dev tools, you'll see that DOM nodes are no longer being re-created for the entire tree when one node is expanded. This removes the flicker that you'd see before (in Chrome, but not in Firefox) when selecting a new node.
But there's also something else that's preventing your rotation animations from working properly. In the CodeSandbox example I linked above, I added some ugly animations on the
borderproperties to show how animation of some CSS properties is working, but animation of thetransformCSS properties is not working. I suspect this problem has nothing to do withreact-window, so you'll probably want to debug it separately to figure out why your transforms aren't animating but other properties are animating OK.