Prześledź, dlaczego komponent React jest ponownie renderowany

156

Czy istnieje systematyczne podejście do debugowania tego, co powoduje ponowne renderowanie komponentu w Reakcie? Umieściłem prostą console.log (), aby zobaczyć, ile razy renderuje, ale mam problem ze zrozumieniem, co powoduje, że komponent renderuje się wiele razy, tj. (4 razy) w moim przypadku. Czy istnieje narzędzie, które pokazuje oś czasu i / lub wszystkie renderowane i porządkowane drzewo komponentów?

jasan
źródło
Może przydałoby shouldComponentUpdatesię wyłączenie automatycznej aktualizacji komponentów, a następnie rozpoczęcie śledzenia z tego miejsca. Więcej informacji można znaleźć tutaj: facebook.github.io/react/docs/optimizing-performance.html
Reza Sadr
Odpowiedź @jpdelatorre jest prawidłowa. Ogólnie rzecz biorąc, jedną z mocnych stron Reacta jest to, że możesz łatwo prześledzić przepływ danych w łańcuchu, patrząc na kod. React przedłużacza DevTools może pomóc w tym. Mam również listę przydatnych narzędzi do wizualizacji / śledzenia ponownego renderowania komponentu React w ramach mojego katalogu dodatków Redux oraz kilka artykułów na temat [Monitorowanie wydajności React] (
htt

Odpowiedzi:

253

Jeśli chcesz krótki fragment bez żadnych zewnętrznych zależności, uważam to za przydatne

componentDidUpdate(prevProps, prevState) {
  Object.entries(this.props).forEach(([key, val]) =>
    prevProps[key] !== val && console.log(`Prop '${key}' changed`)
  );
  if (this.state) {
    Object.entries(this.state).forEach(([key, val]) =>
      prevState[key] !== val && console.log(`State '${key}' changed`)
    );
  }
}

Oto mały haczyk, którego używam do śledzenia aktualizacji komponentów funkcji

function useTraceUpdate(props) {
  const prev = useRef(props);
  useEffect(() => {
    const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
      if (prev.current[k] !== v) {
        ps[k] = [prev.current[k], v];
      }
      return ps;
    }, {});
    if (Object.keys(changedProps).length > 0) {
      console.log('Changed props:', changedProps);
    }
    prev.current = props;
  });
}

// Usage
function MyComponent(props) {
  useTraceUpdate(props);
  return <div>{props.children}</div>;
}
Jacob Rask
źródło
5
@ yarden.refaeli Nie widzę powodu, aby mieć blok if. Krótkie i zwięzłe.
Isaac
Oprócz tego, jeśli zauważysz, że część stanu jest aktualizowana i nie jest oczywiste, gdzie lub dlaczego, możesz zastąpić setStatemetodę (w komponencie klasy), setState(...args) { super.setState(...args) }a następnie ustawić punkt przerwania w debugerze, który będziesz mógł wtedy aby prześledzić z powrotem do funkcji ustawiającej stan.
redbmk
Jak dokładnie mam używać funkcji haka? Gdzie dokładnie mam zadzwonić useTraceUpdatepo zdefiniowaniu tego, co napisałeś?
damon
W komponencie funkcji, można go używać tak function MyComponent(props) { useTraceUpdate(props); }i będzie zalogować ilekroć rekwizyty zmian
Jacob Rask
1
@DawsonB prawdopodobnie nie masz żadnego stanu w tym komponencie, więc this.statejest niezdefiniowany.
Jacob Rask
67

Oto kilka przypadków ponownego renderowania komponentu React.

  • Ponowne wydanie komponentu nadrzędnego
  • Wywołanie this.setState()w ramach komponentu. Spowoduje to następujące metody elementów cyklu życia shouldComponentUpdate> componentWillUpdate> render>componentDidUpdate
  • Zmiany w komponencie props. To spowoduje componentWillReceiveProps> shouldComponentUpdate> componentWillUpdate> render> componentDidUpdate( connectmetoda react-reduxwyzwalania to gdy istnieją obowiązujące zmiany w sklepie Redux)
  • dzwonienie, this.forceUpdatektóre jest podobne dothis.setState

Możesz zminimalizować ponowne wyrejestrowanie komponentu, wdrażając sprawdzenie wewnątrz swojego shouldComponentUpdatei zwracając, falsejeśli nie jest to konieczne.

Innym sposobem jest użycie React.PureComponent komponentów bezstanowych. Czyste i bezstanowe komponenty renderują się ponownie tylko wtedy, gdy są zmiany w ich właściwościach.

jpdelatorre
źródło
6
Nitpick: „bezstanowy” oznacza po prostu każdy składnik, który nie używa stanu, niezależnie od tego, czy jest zdefiniowany za pomocą składni klasy, czy składni funkcjonalnej. Ponadto elementy funkcjonalne są zawsze renderowane ponownie. Musisz użyć shouldComponentUpdatelub rozszerzyć React.PureComponent, aby wymusić ponowne renderowanie tylko w przypadku zmiany.
markerikson
1
Masz rację, jeśli chodzi o składnik bezstanowy / funkcjonalny, który jest zawsze renderowany ponownie. Zaktualizuję moją odpowiedź.
jpdelatorre
czy możesz wyjaśnić, kiedy i dlaczego komponenty funkcjonalne są zawsze renderowane ponownie? W mojej aplikacji używam sporo funkcjonalnych komponentów.
jasan
Więc nawet jeśli używasz funkcjonalnego sposobu tworzenia swojego komponentu, np. const MyComponent = (props) => <h1>Hello {props.name}</h1>;(To jest komponent bezstanowy). Wyrenderuje się ponownie przy każdym ponownym renderowaniu komponentu nadrzędnego.
jpdelatorre
2
To z pewnością świetna odpowiedź, ale nie odpowiada ona prawdziwemu pytaniu: - Jak prześledzić, co spowodowało ponowne renderowanie. Odpowiedź Jacoba R. wygląda obiecująco, dając odpowiedź na rzeczywisty problem.
Sanuj,
10

Odpowiedź @jpdelatorre świetnie podkreśla ogólne powody, dla których składnik React może renderować się ponownie.

Chciałem tylko zagłębić się w jeden przykład: kiedy zmieniają się rekwizyty . Rozwiązywanie tego, co powoduje ponowne renderowanie komponentu React, jest częstym problemem, az mojego doświadczenia wynika, że ​​często śledzenie tego problemu wymaga określenia, które właściwości ulegają zmianie .

Komponenty React renderują się ponownie, gdy otrzymają nowe rekwizyty. Mogą otrzymać nowe rekwizyty, takie jak:

<MyComponent prop1={currentPosition} prop2={myVariable} />

lub jeśli MyComponentjest podłączony do sklepu Redux:

function mapStateToProps (state) {
  return {
    prop3: state.data.get('savedName'),
    prop4: state.data.get('userCount')
  }
}

Anytime wartość prop1, prop2, prop3, czy prop4zmiany MyComponentbędą ponownie render. Przy 4 rekwizytach nie jest zbyt trudno wyśledzić, które rekwizyty się zmieniają, umieszczając console.log(this.props)na tym początku renderbloku. Jednak przy bardziej skomplikowanych komponentach i coraz większej liczbie rekwizytów ta metoda jest nie do utrzymania.

Oto przydatne podejście (przy użyciu lodash dla wygody) do określenia, które zmiany właściwości powodują ponowne renderowanie komponentu:

componentWillReceiveProps (nextProps) {
  const changedProps = _.reduce(this.props, function (result, value, key) {
    return _.isEqual(value, nextProps[key])
      ? result
      : result.concat(key)
  }, [])
  console.log('changedProps: ', changedProps)
}

Dodanie tego fragmentu kodu do komponentu może pomóc w ujawnieniu sprawcy powodującego wątpliwe ponowne renderowanie, a często pomaga to rzucić światło na niepotrzebne dane przesyłane potokiem do komponentów.

Cumulo Nimbus
źródło
3
Teraz nazywa się UNSAFE_componentWillReceiveProps(nextProps)i jest przestarzałe. „Ten cykl życia był wcześniej nazwany componentWillReceiveProps. Ta nazwa będzie działać aż do wersji 17.” Z dokumentacji Reacta .
Emile Bergeron
1
Możesz osiągnąć to samo dzięki componentDidUpdate, co i tak jest prawdopodobnie lepsze, ponieważ chcesz tylko dowiedzieć się, co spowodowało, że składnik faktycznie się zaktualizował.
zobacz ostrzejszy
5

Dziwne, że nikt nie udzielił takiej odpowiedzi, ale uważam ją za bardzo przydatną, zwłaszcza że zmiany rekwizytów są prawie zawsze głęboko zagnieżdżone.

Miłośnicy haków:

import deep_diff from "deep-diff";
const withPropsChecker = WrappedComponent => {
  return props => {
    const prevProps = useRef(props);
    useEffect(() => {
      const diff = deep_diff.diff(prevProps.current, props);
      if (diff) {
        console.log(diff);
      }
      prevProps.current = props;
    });
    return <WrappedComponent {...props} />;
  };
};

„Starzy” fani szkolni:

import deep_diff from "deep-diff";
componentDidUpdate(prevProps, prevState) {
      const diff = deep_diff.diff(prevProps, this.props);
      if (diff) {
        console.log(diff);
      }
}

PS Nadal wolę używać HOC (komponent wyższego rzędu), ponieważ czasami zniszczyłeś swoje rekwizyty na górze, a rozwiązanie Jacoba nie pasuje

Zastrzeżenie: brak jakiegokolwiek powiązania z właścicielem pakietu. Samo kliknięcie dziesiątki razy dookoła, aby spróbować dostrzec różnicę w głęboko zagnieżdżonych obiektach, jest uciążliwe.

ZenVentzi
źródło
4

Na npm jest teraz dostępny haczyk do tego:

https://www.npmjs.com/package/use-trace-update

(Ujawnienie, opublikowałem) Aktualizacja: Opracowana w oparciu o kod Jacoba Raska

Damian Green
źródło
14
To praktycznie ten sam kod, który opublikował Jacob. Mógł go tam przypisać.
Christian Ivicevic
2

Używając haków i elementów funkcjonalnych, nie tylko zmiana rekwizytu może spowodować wycofanie się. Zacząłem używać raczej ręcznego dziennika. Bardzo mi pomogłem. Może ci się też przydać.

Wklejam tę część do pliku komponentu:

const keys = {};
const checkDep = (map, key, ref, extra) => {
  if (keys[key] === undefined) {
    keys[key] = {key: key};
    return;
  }
  const stored = map.current.get(keys[key]);

  if (stored === undefined) {
    map.current.set(keys[key], ref);
  } else if (ref !== stored) {
    console.log(
      'Ref ' + keys[key].key + ' changed',
      extra ?? '',
      JSON.stringify({stored}).substring(0, 45),
      JSON.stringify({now: ref}).substring(0, 45),
    );
    map.current.set(keys[key], ref);
  }
};

Na początku metody mam odniesienie do WeakMap:

const refs = useRef(new WeakMap());

Następnie po każdym „podejrzanym” wezwaniu (rekwizyty, haki) piszę:

const example = useExampleHook();
checkDep(refs, 'example ', example);
Miklos Jakab
źródło
1

Powyższe odpowiedzi są bardzo pomocne, na wypadek gdyby ktoś szukał konkretnej metody wykrywania przyczyny ponownego renderowania to znalazłem tę bibliotekę redux-logger bardzo pomocna.

To, co możesz zrobić, to dodać bibliotekę i włączyć różnicowanie między stanami (jest w dokumentacji), na przykład:

const logger = createLogger({
    diff: true,
});

I dodaj oprogramowanie pośredniczące w sklepie.

Następnie umieść console.log()w funkcji renderowania komponentu, który chcesz przetestować.

Następnie możesz uruchomić aplikację i sprawdzić, czy są dzienniki konsoli. Gdziekolwiek jest dziennik tuż przed, pokaże Ci różnicę między stanami (nextProps and this.props)i możesz zdecydować, czy renderowanie jest tam naprawdę potrzebnewprowadź opis obrazu tutaj

Będzie podobny do powyższego obrazu wraz z kluczem diff.

pritesh
źródło