elmish / react

Elmish React extensions for writing SPA and Native apps
https://elmish.github.io/react
Other
104 stars 21 forks source link

Excessive re-rerendering #55

Closed ArtemyB closed 2 years ago

ArtemyB commented 3 years ago

Description

While working on my Feliz + Elmish app I've discovered that the whole global state dependent part of the app tends to re-render on every app state change (according to React Dev Tools with "Highlight updates when components render" setting being enabled), even though I pass only necessary parts of the global state to child views. For example one of the pages has a form, the state of which is a part of the global Elmish state. Even one form's input change causes the whole app UI to re-render. Here is a link to my demo-app: https://github.com/ArtemyB/ElmishDemo

Probably this is not an issue of the library, but my incorrect use of it. If so I would be glad to know any remarks on that.

Repro code

https://github.com/ArtemyB/ElmishDemo

Expected and actual results

Expected

To see re-rendering only the changing UI component. In case of the Login form from the provided example it should be only LoginFormView at least.

Actual

To observe the issue I am describing "React Developer Tools" browser extension should be installed and its "Highlight updates when components render" setting should be enabled. And then it is enough just to type something into any of the Login form fields to see that on any change all the app UI components are highlighted by "React Developer Tools".

Related information

zanaptak commented 2 years ago

Lazy views can probably help with this.

I believe similar behavior occurs even in a plain React function-component application. I think "render" in this context only means "execute the function" and doesn't necessarily indicate a visual browser render. There will still be React's DOM reconciliation process happening afterward.

Try the following in a plain React app and note the Child component without any changes still gets highlighted by the dev tools upon Parent component update.

import React, { useState } from 'react';

function Child() {
    return <p>Child component</p>;
}

function Parent() {
    const [count, setCount] = useState(0);
    return (
      <div>
        <p>Parent component</p>
        <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
        <Child/>
      </div>
    );
}

export default Parent;

If you make the Child a memo then the dev tools will no longer highlight it:

const Child = React.memo(
    function Child() {
        return <p>Child component</p>;
    }
);

Using lazy view in Elmish should be analogous to this.

MangelMaxime commented 2 years ago

Hello @ArtemyB ,

sorry for taking this long before looking at this issue, I was focusing on releasing Nacara.

React integration of Elmish, has been intensively tested in the past and should be correct in theory. At least when using Fable.React bindings.

As mentioned by @zanaptak, when I take a pure JavaScript project and look at the flash all the components flash too.

https://user-images.githubusercontent.com/4760796/140612933-ab571ac7-95f8-4bd3-9567-d2e6f37476da.mp4

Example from https://github.com/arnab-datta/counter-app


During a Fable Conf, there has been a great talk about how to measure performance in a React application with Fable. https://www.youtube.com/watch?v=9VJoaNoutm4

In the talk, he uses the profiler to see which components was consider for the render and which has been actually rendered.

In your application, it right that everything seems to be re-rendered:

image

The problem is that we can't really isolate if this is due to Feliz, Mui, or Elmish as everything is mixed together in the sample.

It would be better, if we had a minimal example in F# and JS to have a fair comparaison.


Like that there are some things to know about Elmish and React:

  1. dispatch and functions in general are not stable by default meaning that if you pass a function as a props to a component, React will consider that the props changes and so re-render the components.

To counter that, you can make dispatch stable by using React.useCallback I believe and you can also use functional components like React.functionComponent, React.meno (from Feliz) or FunctionComponent.Of, Memo.Of (from Fable.React) with a custom comparer like memoEqualsButFunctions or equalsButFunctions which ignores the functions when doing the comparaison.

  1. Generating a Guid in the component means that this components will always be different between rendering because you are generating a new Guid at each render. Example

  2. About LazyViews they are components offered by Elmish which can be used to optimized the React diff. I think, they are not used that much now days, because it was before we had Hooks and real function component from React.

MangelMaxime commented 2 years ago

Actually, I think dispatch has been made stable in Elmish 3

https://github.com/elmish/elmish/issues/157#issuecomment-439927352

But this is true only for the top level dispatch as when you due MyMsg >> dispatch you are creating a new function on each call.

MangelMaxime commented 2 years ago

Using the lazyView components allow to have finer control over what is re-rendered by React:

image

image

But I am unsure how React does its diffing or if the extension works correctly. Because, even for simple application I have some strange behaviour.

Hello @vbfox , I am pinging you in case you are still doing some Fable / React stuff as I know you invested a lot of time in it in the past.

ArtemyB commented 2 years ago

@MangelMaxime @zanaptak thank you for the detailed answers! 👍 For the link to the video from FableConf -- special thanks. However it required working with React (and JS) for some time to better understand what is actually happening in apps and to feel less confused. But all the tips from the answers above helped a lot (also they addressed questions/topics that needed to be examined in more detail). So closing this as it actually appeared to be just a question, not an issue with Elmish.React. Again, thanks a lot!