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?
quelle
useReducer
Antworten:
Sie können den
loading
Status und dendata
Status in einem Statusobjekt kombinieren und dann einensetState
Aufruf ausführen, und es wird nur ein Rendering ausgeführt.Hinweis: Im Gegensatz zu den
setState
In-Class-Komponenten werden bei dersetState
Rückgabe vonuseState
keine 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
setState
Anrufe 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>
quelle
useMergeState
Hook erstellt, damit Sie den Status automatisch zusammenführen können.setState
AnrufeDies hat auch eine andere Lösung mit
useReducer
! Zuerst definieren wir unser neuessetState
.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 diethis
!setState({loading: false, data: test.data.results})
Wie Sie vielleicht in unserer neuen Version bemerkt haben
setState
(genau wie zuvorthis.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 }
quelle
Expected 0 arguments, but got 1.ts(2554)
ist ein Fehler, den ich beim Versuch bekomme, useReducer auf diese Weise zu verwenden. Genauer gesagtsetState
. Wie behebt man das? Die Datei ist js, aber wir verwenden immer noch ts-Checks.Batching-Update in React-Hooks https://github.com/facebook/react/issues/14259
quelle
Das wird es tun:
const [state, setState] = useState({ username: '', password: ''}); // later setState({ ...state, username: 'John' });
quelle
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
quelle
Um das
this.setState
Zusammenführungsverhalten von Klassenkomponenten zu replizieren , empfehlen die React-Dokumente die Verwendung der FunktionsformuseState
mit Objektverteilung - keine Notwendigkeit füruseReducer
: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:
loading
unddata
sind 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
,error
etc. - explizit in Ihrem Staat und 2.) mituseReducer
einzukapseln 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 ) }
Code-Snippet anzeigen
const useData = () => { const [state, dispatch] = useReducer(reducer, { data: undefined, status: "loading" }); useEffect(() => { fetchData_fakeApi().then(test => { if (test.ok) dispatch(["success", test.data.results]); }); }, []); return state; }; const reducer = (state, [status, payload]) => { if (status === "success") return { ...state, data: payload, status }; // e.g. make sure to reset data, when loading. else if (status === "loading") return { ...state, data: undefined, status }; else return state; }; const App = () => { const { data, status } = useData(); const count = useRenderCount(); const countStr = `Re-rendered ${count.current} times`; return status === "loading" ? ( <div> Loading (3 sec)... {countStr} </div> ) : ( <div> Finished. Data: {JSON.stringify(data)}, {countStr} </div> ); } // // helpers // const useRenderCount = () => { const renderCount = useRef(0); useEffect(() => { renderCount.current += 1; }); return renderCount; }; const fetchData_fakeApi = () => new Promise(resolve => setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000) ); ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>
PS: Stellen Sie sicher, dass benutzerdefinierten Hooks ( anstelle von ) vorangestellt werden . Auch Rückruf übergeben kann nicht sein .
use
useData
getData
useEffect
async
quelle
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]; };
quelle
setMergedState
mit einbrechenuseMemo(() => setMergedState, [])
, da sich, wie in den Dokumenten angegeben,setState
zwischen erneuten Rendern nicht ändert: AufReact 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.Sie können auch eine Statusänderung
useEffect
erkennen und andere Statuswerte entsprechend aktualisierenquelle