javascript - Flere kall til tilstand-oppdaterer fra useState i komponenten forårsaker flere gjengivelser
Jeg prøver React-hooks for første gang, og alt virket bra inntil jeg innså at når jeg henter data og oppdaterer to forskjellige state-variabler (data og lastestatus), blir komponenten min (en datatabell) rendret to ganger, selv om begge kallene til state-oppdatereren skjer i samme funksjon. Her er API-funksjonen min som returnerer begge variablene til komponenten min.
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 };
};
I en vanlig klassekomponent ville du gjort ett enkelt kall for å oppdatere tilstanden, som kan være et komplekst objekt, men "hooks-metoden" ser ut til å være å dele tilstanden opp i mindre enheter, en bivirkning av dette ser ut til å være flere rendringer når de oppdateres separat. Noen ideer om hvordan man kan dempe dette?
Svar #1
Du kan kombinere loading-tilstanden og data-tilstanden til ett tilstandsobjekt, og deretter kan du gjøre ett setState-kall, og det vil bare bli en gjengivelse.
Merk: I motsetning til setState i klassekomponenter, slår ikke setState som returneres fra useState sammen objekter med eksisterende state, det erstatter objektet helt. Hvis du vil gjøre en sammenslåing, må du lese den forrige staten og slå den sammen med de nye verdiene selv. Se dokumentasjonen.
Jeg ville ikke bekymre meg for mye om å kalle renderinger for mye før du har bestemt at du har et ytelsesproblem. Rendering (i React-konteksten) og å utføre oppdateringer av den virtuelle DOM-en til den faktiske DOM-en er to forskjellige saker. Rendering her refererer til å generere virtuelle DOM-er, og ikke til å oppdatere nettleserens DOM. React kan samle setState-kallene og oppdatere nettleserens DOM med den endelige nye tilstanden.
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 /umd/react.development.js"></script>
<script src="https://unpkg.com/[email /umd/react-dom.development.js"></script>
<div id="app"></div>Alternativt - skriv din egen state merger-krok
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 /umd/react.development.js"></script>
<script src="https://unpkg.com/[email /umd/react-dom.development.js"></script>
<div id="app"></div>Svar #2
Dette har også en annen løsning som bruker useReducer! Først definerer vi vår nye setState.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
etter det kan vi ganske enkelt bruke det som de gode gamle klassene this.setState, bare uten this!
setState({loading: false, data: test.data.results})
Som du kanskje har lagt merke til i vår nye setState (akkurat som det vi tidligere hadde med this.setState), trenger vi ikke oppdatere alle tilstandene samtidig! For eksempel kan jeg endre en av våre tilstander slik (og det påvirker ikke de andre tilstandene!):
setState({loading: false})
Fantastisk, ikke sant?!
Så la oss sette alle bitene sammen:
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
}
Typescript-støtte. Takk til P. Galbraith som svarte med denne løsningen. De som bruker Typescript kan bruke dette:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
der MyState er typen på din tilstandsobjekt.
f.eks. I vårt tilfelle vil det være slik:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
Tidligere tilstandsstøtte. I kommentarer spurte user2420374 om en måte å få tilgang til prevState inne i vår setState, så her er en måte å oppnå dette målet på:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction sjekker om det gitte argumentet er en funksjon (noe som betyr at du prøver å få tilgang til prevState) eller et vanlig objekt. Du kan finne denne implementeringen av isFunction av Alex Grande her.
Merk. For de som ønsker å bruke dette svaret mye, har jeg bestemt meg for å gjøre det om til et bibliotek. Du kan finne det her:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Svar #3
Batching-oppdatering i react-hooks https://github.com/facebook/react/issues/14259
React vil for øyeblikket samle tilstandsoppdateringer i en batch hvis de utløses fra en React-basert hendelse, som et knappetrykk eller en endring i input. Det vil ikke samle oppdateringer i en batch hvis de utløses utenfor en React-hendelsesbehandler, som et asynkront kall.
Svar #4
Dette vil gjøre det:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
Svar #5
For å replikere this.setState-sammenslåingsadferden fra klassekomponenter, anbefaler React-dokumentasjonen å bruke den funksjonelle formen av useState med objektsprer - ingen behov for useReducer:
setState(prevState => {
return {...prevState, loading, data};
});
De to statene er nå konsolidert til én, noe som vil spare deg en gjengivelsessyklus.
Det er en annen fordel med ett tilstandsobjekt: loading og data er avhengige tilstander. Ugyldige tilstandsendringer blir mer tydelige når tilstanden settes sammen:
setState({ loading: true, data }); // ups... loading, but we already set data
Du kan til og med bedre sikre konsistente tilstander ved 1.) å gjøre status - loading, success, error, osv. - eksplicit i tilstanden din og 2.) bruke useReducer for å innkapsle tilstandslogikk i en reducer:
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
)
}
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: Sørg for å prefiksere egendefinerte Hooks med use (useData i stedet for getData). Callbacken som sendes til useEffect kan heller ikke være async.
