Unikanie wywołania render

0

Uczę się Reacta klepiąc własny program (https://github.com/tarsa/TarsaLZP/tree/master/scalajs/TarsaLZP) i zastanawiam się nad sposobami unikania przerenderowania elementów, które się nie zmieniły. Chodzi mi o unikanie wywołania metody render, która tworzy wirtualnego DOMa, bo unikaniem renderowania rzeczywistego DOMa zajmuje się oczywiście sam React.

Pytanie w zasadzie powinno brzmieć: czy zawsze wywołanie metody render w nadkomponencie wywoła metodę render we wszystkich podkomponentach?

<mała_dygresja>
Do obsługi stanu wykorzystuję bibliotekę https://github.com/ochrons/diode i tam jest tak, że jest jedno miejsce gdzie znajduje się stan - circuit. Circuit oprócz przetrzymywania stanu zajmuje się kolejkowaniem zdarzeń które go aktualizują, ale przede wszystkim można się zapiąć na niego, czyli nasłuchiwać na zmiany w przetrzymywanym stanie. Diode udostępnia specjalny komponent (nakładkę), który opakowuje dowolny inny komponent Reactowy i nasłuchuje na zmiany w modelu i jeśli ten model się zmieni to nakładka się przerenderowuje.
</mała_dygresja>

Obecnie mam taką hierarchię:

  • MainView_nasłuchujący,
    -- coś_tam_1,
    -- OptionsView_nasłuchujący,
    -- coś_tam_2,

Z obecnych eksperymentów wynika, że przy renderowaniu MainView_nasłuchujący podkomponent OptionsView_nasłuchujący zawsze będzie odmonowany, zamontowany i przerenderowany.

Debug logi:
[MainView] componentWillReceiveProps
[OptionsView] componentWillUnmount
[OptionsView] componentWillMount
[OptionsView] componentDidMount
[MainView] componentDidUpdate

Zastanawiam się czy w ogóle 'walka' z przerenderowywaniem podkomponentu w ogóle ma sens i jest zgodna z Reactowymi wzrocami. Zamiast kombinować mógłbym opakować wszystkie dynamiczne podkomponenty komponentu MainView_nasłuchujący w nasłuchujące nakładki z Diody i wtedy MainView_nasłuchujący zamieniłby się w statyczny MainView. Konkretniej wyszłoby coś takiego:

  • MainView,
    -- Coś_Tam_1_nasłuchujący,
    -- OptionsView_nasłuchujący,
    -- Coś_Tam_2_nasłuchujący,

W tym przypadku nie ma tego problemu, że nadkomponent się przerenderowuje i nie ma też specjalnych kombinacji z unikaniem wywołań metody render. Z drugiej strony muszę jednak robić klasy z komponentami Coś_Tam_1_nasłuchujący i Coś_Tam_2_nasłuchujący. To jest dobre rozwiązanie czy znacie jakieś lepsze?

0

Nie znam Diode, ale:

Z obecnych eksperymentów wynika, że przy renderowaniu MainView_nasłuchujący podkomponent OptionsView_nasłuchujący zawsze będzie odmonowany, zamontowany i przerenderowany.

To sugeruje, że coś jest BARDZO BARDZO skopane. Zdecydowanie bardziej niż renderowanie całego drzewa od nowa. Bez kodu to jedyne co można powiedzieć na pewno.

0

Pytanie w zasadzie powinno brzmieć: czy zawsze wywołanie metody render w nadkomponencie wywoła metodę render we wszystkich podkomponentach?

Domyślnie tak - przed wyrenderowaniem komponentu odpalana jest metoda shouldComponentUpdate, którą możesz nadpisać (jak nie trudno wywnioskować domyślnie zwraca true). Zazwyczaj wystarczy użyć zamiast React.Component - React.PureComponent, który ma już zaimplementowaną odpowiednią logikę dla shouldComponentUpdate.

Jeśli korzystasz ze stateles components to taką logikę trzeba zawrzeć poza komponentem, nie wiem jak w ScalaJS, ale taki Redux na przykład robi takie optymalizacje za Ciebie.

0

gdzie jest kod, w którym piszesz komponenty? Nie widzę w tym projekcie.

Hmm... odmontowanie i zamontowanie czasem się bierze z nieoptymalnego ustawienia właściwości key, więc jak masz ileś elementów w jednym pojemniku to React się gubi..

poza tym możesz spróbować z shouldComponentUpdate
https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate

0

Widok jest generalnie tutaj: https://github.com/tarsa/TarsaLZP/tree/master/scalajs/TarsaLZP/src/main/scala/pl/tarsa/tarsalzp/ui <- jest to jednak trochę stara wersja, obecnie dalej grzebię, kombinuję, dodaję printlny by sprawdzić co się dzieje

Samo połączenie Diody z Reactem jest tutaj: https://github.com/ochrons/diode/blob/master/diode-react/src/main/scala/diode/react/ReactConnector.scala w metodzie connect na samym dole, która tworzy "DiodeWrapper".

Zastanawiająca jest metoda render z DiodeWrappera:

def render(s: S, compB: ModelProxy[S] => ReactElement) = wrap(modelReader)(compB)

Z tego co widzę to przy każdym renderze tworzy nowy ReactElement.

....

Pogrzebałem chwilę i dodałem logowanie. Aktualna wersja z dodatkowym logowaniem jest tutaj: https://github.com/tarsa/TarsaLZP/tree/548ae5713347660f3d3f2b252a160a9968c71959/scalajs/TarsaLZP/src/main/scala/pl/tarsa/tarsalzp/ui

Przykładowe logi z konsoli:

Building MainView
[MainView] componentWillMount
Rendering MainView
Building OptionsView
[OptionsView] componentWillMount
Rendering OptionsView
[OptionsView] componentDidMount
[MainView] componentDidMount
Action: ChangedMode
Listener got fired!
Building MainView
[MainView] componentWillReceiveProps
[MainView] componentWillUpdate
Rendering MainView
[OptionsView] componentWillUnmount
Building OptionsView
[OptionsView] componentWillMount
Rendering OptionsView
[OptionsView] componentDidMount
[MainView] componentDidUpdate

Aktualna stan z brancha jest tutaj: dist.7z Trzeba wejść w katalog 'classes' i odpalić 'index-dev.html'

Hmm, skoro przy każdym renderze DiodeWrapper konstruuje nowy ReactElement to chyba nie ma siły, żeby React pominął odmontowanie starej instancji elementu i podmontowania nowej. Muszę to jeszcze rozkminić.

Aktualizacja - poprawka:
W tej metodzie:

def render(s: S, compB: ModelProxy[S] => ReactElement) = wrap(modelReader)(compB)

tworzony jest nowy ReactComponent, tzn jako compB przekazuję funkcję tworzącą komponent z propsów.

1

Ech, doszedłem w końcu o co chodzi i kwestia rozbija się bardziej o użycie Diody niż Reacta.

Metoda:

  def apply(proxy: ModelProxy[MainModel]) = {
    println("Building MainView")
    val optionsViewProxy = proxy.connect(_.options)
    val optionsView = optionsViewProxy(OptionsView.apply)
    component(Props(proxy, optionsView))
}

z https://github.com/tarsa/TarsaLZP/blob/548ae5713347660f3d3f2b252a160a9968c71959/scalajs/TarsaLZP/src/main/scala/pl/tarsa/tarsalzp/ui/views/MainView.scala była wywoływana podczas każdego wywołania render z DiodeWrappera, a więc przy każdej zmianie modelu był tworzony kolejny DiodeWrapper.

Rozwiązaniem jest więc wyciągnięcie tworzenia OptionsView wyżej, tak by DiodeWrapper dla niego był tworzony tylko raz. Commit jest tutaj: https://github.com/tarsa/TarsaLZP/commit/cdffa2f075ada35e660dfa0b4ea49592d832d458

Nie ma już w logach powtarzającego componentWillUnmount :)

Dzięki za odzew.

Aktualizacja:
Podrążyłem temat jeszcze bardziej i doszedłem do ciekawego miejsca. Metoda diode.react.ReactConnector#connect tworzy DiodeWrappera za pomocą ReactComponentB i wywołuje na nim metodę build. Ta metoda wygląda tak:

def build: Output = {
      val c = React.createClass(buildSpec)
      val f = React.createFactory(c)
      val r = new ReqProps(f, c, undefined, undefined)
      buildFn(r)
    }

A więc każde wywołanie connect tworzy nową klasę Reactową. Stąd (jak mniemam) React jest zmuszony do odmontowania starego wrappera i zamontowania nowego (o ile wywołujemy connect w kółko).

0

A więc każde wywołanie connect tworzy nową klasę Reactową.

Niestety z tym trzeba bardzo uważać. Jak coś jest takie samo nawet (za każdym razem tworzone w identyczny sposób), ale inne (bo tworzone na nowo), to dla Reacta już jest to całkowicie inny komponent, np. miałem tak, że używałem w funkcji render komponentów wyższego rzędu, które zwracały za każdym razem identyczny komponent, ale React uważał je za inne i odmontowywał/zamontowywał na nowo.

Potem więc to jakoś zmieniłem i tworzyłem komponenty wyższego rzędu poza funkcją render, żeby były te same dokładnie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1