Mehrere Aufrufe des Statusaktualisierers von useState in der Komponente führen zu mehreren erneuten Rendern

111

Ich versuche zum ersten Mal, React-Hooks zu verwenden, und alles schien gut zu sein, bis mir klar wurde, dass meine Komponente (eine Datentabelle) beim Abrufen von Daten und Aktualisieren von zwei verschiedenen Statusvariablen (Daten- und Ladeflag) zweimal gerendert wird, obwohl beide Aufrufe vorliegen zum State Updater geschehen in der gleichen Funktion. Hier ist meine API-Funktion, die beide Variablen an meine Komponente zurückgibt.

const getData = url => {

    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(async () => {

        const test = await api.get('/people')

        if(test.ok){
            setLoading(false);
            setData(test.data.results);
        }

    }, []);

    return { data, loading };
};

In einer normalen Klassenkomponente würden Sie einen einzelnen Aufruf ausführen, um den Status zu aktualisieren, der ein komplexes Objekt sein kann. Der "Hooks-Weg" scheint jedoch darin zu bestehen, den Status in kleinere Einheiten aufzuteilen, deren Nebeneffekt mehrere Wiederholungen zu sein scheint wird gerendert, wenn sie separat aktualisiert werden. Irgendwelche Ideen, wie man das abmildern kann?

Jonhobbs
quelle
Wenn Sie abhängige Staaten haben, sollten Sie wahrscheinlich verwendenuseReducer
thinklinux
Beeindruckend! Ich habe das gerade erst entdeckt und es hat mein Verständnis darüber, wie das Rendern von Reaktionen funktioniert, völlig zerstört. Ich kann keinen Vorteil für diese Arbeitsweise verstehen - es scheint ziemlich willkürlich, dass sich das Verhalten in einem asynchronen Rückruf von dem in einem normalen Ereignishandler unterscheidet. Übrigens scheint es in meinen Tests, dass die Abstimmung (dh die Aktualisierung des realen DOM) erst erfolgt, nachdem alle setState-Aufrufe verarbeitet wurden, sodass die Render-Zwischenaufrufe ohnehin verschwendet werden.
Andy

Antworten:

111

Sie können den loadingStatus und den dataStatus in einem Statusobjekt kombinieren und dann einen setStateAufruf ausführen, und es wird nur ein Rendering ausgeführt.

Hinweis: Im Gegensatz zu den setStateIn-Class-Komponenten werden bei der setStateRückgabe von useStatekeine Objekte mit dem vorhandenen Status zusammengeführt, sondern das Objekt vollständig ersetzt. Wenn Sie eine Zusammenführung durchführen möchten, müssen Sie den vorherigen Status lesen und selbst mit den neuen Werten zusammenführen. Siehe die Dokumente .

Ich würde mir keine Sorgen machen, wenn Sie Renderings übermäßig aufrufen, bis Sie festgestellt haben, dass Sie ein Leistungsproblem haben. Das Rendern (im React-Kontext) und das Festschreiben der virtuellen DOM-Aktualisierungen an das reale DOM sind verschiedene Aspekte. Das Rendern bezieht sich hier auf das Generieren virtueller DOMs und nicht auf das Aktualisieren des Browser-DOM. React kann die setStateAnrufe stapeln und das Browser-DOM mit dem endgültigen neuen Status aktualisieren.

const {useState, useEffect} = React;

function App() {
  const [userRequest, setUserRequest] = useState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    // Note that this replaces the entire object and deletes user key!
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>

Alternative - schreiben Sie Ihren eigenen State Merger Hook

const {useState, useEffect} = React;

function useMergeState(initialState) {
  const [state, setState] = useState(initialState);
  const setMergedState = newState => 
    setState(prevState => Object.assign({}, prevState, newState)
  );
  return [state, setMergedState];
}

function App() {
  const [userRequest, setUserRequest] = useMergeState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>

Yangshun Tay
quelle
1
Ich bin damit einverstanden, dass es nicht ideal ist, aber es ist immer noch eine vernünftige Art, es zu tun. Sie können Ihren eigenen benutzerdefinierten Hook schreiben, der das Zusammenführen ausführt, wenn Sie das Zusammenführen nicht selbst durchführen möchten. Ich habe den letzten Satz aktualisiert, um klarer zu sein. Kennen Sie die Funktionsweise von React? Wenn nicht, finden Sie hier einige Links, die Ihnen helfen könnten, besser zu verstehen: reactjs.org/docs/reconciliation.html , blog.vjeux.com/2013/javascript/react-performance.html .
Yangshun Tay
2
@jonhobbs Ich habe eine alternative Antwort hinzugefügt, die einen useMergeStateHook erstellt, damit Sie den Status automatisch zusammenführen können.
Yangshun Tay
1
Super, nochmals vielen Dank. Ich werde mit der Verwendung von React vertraut, könnte aber viel über die Interna lernen. Ich werde sie lesen.
Jonhobbs
2
Neugierig, unter welchen Umständen dies zutreffen kann?: " React kann die setStateAnrufe
stapeln
1
Schön .. Ich dachte darüber nach, den Zustand zu kombinieren oder einen separaten Nutzlastzustand zu verwenden und die anderen Zustände aus dem Nutzlastobjekt lesen zu lassen. Sehr guter Beitrag. 10/10 wird wieder gelesen
Anthony Moon Beam Toorie
57

Dies hat auch eine andere Lösung mit useReducer! Zuerst definieren wir unser neues setState.

const [state, setState] = useReducer(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null, something: ''}
)

danach können wir es einfach wie die guten alten Klassen benutzen this.setState, nur ohne die this!

setState({loading: false, data: test.data.results})

Wie Sie vielleicht in unserer neuen Version bemerkt haben setState(genau wie zuvor this.setState), müssen wir nicht alle Status zusammen aktualisieren! Zum Beispiel kann ich einen unserer Zustände so ändern (und es ändert keine anderen Zustände!):

setState({loading: false})

Genial, Ha?!

Also lasst uns alle Teile zusammenfügen:

import {useReducer} from 'react'

const getData = url => {
  const [state, setState] = useReducer(
    (state, newState) => ({...state, ...newState}),
    {loading: true, data: null}
  )

  useEffect(async () => {
    const test = await api.get('/people')
    if(test.ok){
      setState({loading: false, data: test.data.results})
    }
  }, [])

  return state
}
Vahid Al
quelle
Expected 0 arguments, but got 1.ts(2554)ist ein Fehler, den ich beim Versuch bekomme, useReducer auf diese Weise zu verwenden. Genauer gesagt setState. Wie behebt man das? Die Datei ist js, aber wir verwenden immer noch ts-Checks.
ZenVentzi
Super
Ich denke, es ist immer noch die gleiche Idee, dass zwei Staaten fusioniert sind. In einigen Fällen können Sie die Status jedoch nicht zusammenführen.
user1888955
43

Batching-Update in React-Hooks https://github.com/facebook/react/issues/14259

React aktualisiert derzeit den Batch-Status, wenn sie innerhalb eines React-basierten Ereignisses ausgelöst werden, z. B. durch Klicken auf eine Schaltfläche oder Eingabeänderung. Es werden keine Stapelaktualisierungen durchgeführt, wenn sie außerhalb eines React-Ereignishandlers wie eines asynchronen Aufrufs ausgelöst werden.

Sureshraj
quelle
5
Dies ist eine sehr hilfreiche Antwort, da sie das Problem wahrscheinlich in den meisten Fällen löst, ohne etwas tun zu müssen. Vielen Dank!
Davnicwil
7

Das wird es tun:

const [state, setState] = useState({ username: '', password: ''});

// later
setState({
    ...state,
    username: 'John'
});
Mehdi Dehghani
quelle
4

Wenn Sie Hooks von Drittanbietern verwenden und den Status nicht zu einem Objekt zusammenführen oder verwenden können useReducer, lautet die Lösung:

ReactDOM.unstable_batchedUpdates(() => { ... })

Empfohlen von Dan Abramov hier

Siehe dieses Beispiel

Mohamed Ramrami
quelle
Fast ein Jahr später scheint diese Funktion immer noch völlig undokumentiert und immer noch als instabil markiert zu sein :-(
Andy
4

Um das this.setStateZusammenführungsverhalten von Klassenkomponenten zu replizieren , empfehlen die React-Dokumente die Verwendung der Funktionsform useStatemit Objektverteilung - keine Notwendigkeit für useReducer:

setState(prevState => {
  return {...prevState, loading, data};
});

Die beiden Zustände werden jetzt zu einem zusammengefasst, wodurch Sie einen Renderzyklus sparen.

Es gibt einen weiteren Vorteil bei einem Zustandsobjekt: loadingund datasind abhängige Zustände. Ungültige Zustandsänderungen werden deutlicher, wenn der Zustand zusammengestellt wird:

setState({ loading: true, data }); // ups... loading, but we already set data

Sie können noch besser konsistente Zustände gewährleisten durch 1.) der Status zu machen - loading, success, erroretc. - explizit in Ihrem Staat und 2.) mit useReducereinzukapseln Zustandslogik in einem Druckminderer:

const useData = () => {
  const [state, dispatch] = useReducer(reducer, /*...*/);

  useEffect(() => {
    api.get('/people').then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);
};

const reducer = (state, [status, payload]) => {
  if (status === "success") return { ...state, data: payload, status };
  // keep state consistent, e.g. reset data, if loading
  else if (status === "loading") return { ...state, data: undefined, status };
  return state;
};

const App = () => {
  const { data, status } = useData();
  return status === "loading" ? <div> Loading... </div> : (
    // success, display data 
  )
}

PS: Stellen Sie sicher, dass benutzerdefinierten Hooks ( anstelle von ) vorangestellt werden . Auch Rückruf übergeben kann nicht sein .useuseDatagetDatauseEffectasync

ford04
quelle
1
Upvoted das! Dies ist eine der hilfreichsten Funktionen, die ich in der React-API gefunden habe. Ich habe es gefunden, als ich versucht habe, eine Funktion als Status zu speichern (ich weiß, dass es seltsam ist, Funktionen zu speichern, aber ich musste mich aus irgendeinem Grund nicht erinnern) und es hat aufgrund dieser reservierten Anrufsignatur
elquimista
1

Eine kleine Ergänzung zur Antwort https://stackoverflow.com/a/53575023/121143

Cool! Für diejenigen, die planen, diesen Hook zu verwenden, könnte er auf eine etwas robuste Art und Weise geschrieben werden, um mit der Funktion als Argument zu arbeiten, wie zum Beispiel:

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newState =>
    typeof newState == "function"
      ? setState(prevState => ({ ...prevState, ...newState(prevState) }))
      : setState(prevState => ({ ...prevState, ...newState }));
  return [state, setMergedState];
};

Update : Optimierte Version, Status wird nicht geändert, wenn der eingehende Teilstatus nicht geändert wurde.

const shallowPartialCompare = (obj, partialObj) =>
  Object.keys(partialObj).every(
    key =>
      obj.hasOwnProperty(key) &&
      obj[key] === partialObj[key]
  );

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newIncomingState =>
    setState(prevState => {
      const newState =
        typeof newIncomingState == "function"
          ? newIncomingState(prevState)
          : newIncomingState;
      return shallowPartialCompare(prevState, newState)
        ? prevState
        : { ...prevState, ...newState };
    });
  return [state, setMergedState];
};
mschayna
quelle
Cool! Ich würde nur setMergedStatemit einbrechen useMemo(() => setMergedState, []), da sich, wie in den Dokumenten angegeben, setStatezwischen erneuten Rendern nicht ändert: Auf React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.diese Weise wird die setState-Funktion beim erneuten Rendern nicht neu erstellt.
Tonix
0

Sie können auch eine Statusänderung useEffecterkennen und andere Statuswerte entsprechend aktualisieren

JeanAlesi
quelle