Próbuję po raz pierwszy przechwytywać Reacta i wszystko wydawało się dobre, dopóki nie zdałem sobie sprawy, że kiedy otrzymuję dane i aktualizuję dwie różne zmienne stanu (dane i flaga ładowania), mój komponent (tabela danych) jest renderowany dwukrotnie, mimo że oba wywołania do aktualizacji stanu mają miejsce w tej samej funkcji. Oto moja funkcja API, która zwraca obie zmienne do mojego komponentu.
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 };
};
W normalnym komponencie klasy można wykonać pojedyncze wywołanie w celu zaktualizowania stanu, który może być złożonym obiektem, ale „sposób zaczepienia” wydaje się polegać na podzieleniu stanu na mniejsze jednostki, czego efektem ubocznym wydaje się renderuje się, gdy są aktualizowane oddzielnie. Jakieś pomysły, jak to złagodzić?
javascript
reactjs
react-hooks
jonhobbs
źródło
źródło
useReducer
Odpowiedzi:
Możesz połączyć
loading
stan idata
stan w jeden obiekt stanu, a następnie możesz wykonać jednosetState
wywołanie i będzie tylko jeden render.Uwaga: W przeciwieństwie do
setState
komponentów in class,setState
zwracany zuseState
nie łączy obiektów z istniejącym stanem, całkowicie zastępuje obiekt. Jeśli chcesz wykonać scalenie, musisz przeczytać poprzedni stan i samodzielnie scalić go z nowymi wartościami. Zapoznaj się z dokumentacją .Nie martwiłbym się zbytnio o wywoływanie renderów, dopóki nie ustalisz, że masz problem z wydajnością. Renderowanie (w kontekście Reacta) i wysyłanie aktualizacji wirtualnego DOM do rzeczywistego DOM to różne sprawy. Renderowanie tutaj odnosi się do generowania wirtualnych DOMów, a nie do aktualizowania DOM przeglądarki. React może grupować
setState
wywołania i aktualizować DOM przeglądarki do ostatecznego nowego stanu.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>
Alternatywa - napisz własny punkt zaczepienia do łączenia stanów
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>
źródło
useMergeState
hook, dzięki czemu możesz automatycznie scalić stan.setState
Ma to również inne rozwiązanie wykorzystujące
useReducer
! najpierw określamy nasze nowesetState
.const [state, setState] = useReducer( (state, newState) => ({...state, ...newState}), {loading: true, data: null, something: ''} )
po tym możemy go po prostu używać jak starych dobrych klas
this.setState
, tylko bezthis
!setState({loading: false, data: test.data.results})
Jak być może zauważyłeś w naszym nowym
setState
(tak jak to, co mieliśmy wcześniejthis.setState
), nie musimy aktualizować wszystkich stanów razem! na przykład mogę zmienić jeden z naszych stanów w ten sposób (i nie zmienia to innych stanów!):setState({loading: false})
Świetnie, Ha ?!
Więc złóżmy wszystkie elementy razem:
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 }
źródło
Expected 0 arguments, but got 1.ts(2554)
jest błędem, który otrzymuję podczas próby użycia useReducer w ten sposób. DokładniejsetState
. Jak to naprawić? Plik to js, ale nadal używamy kontroli ts.Aktualizacja wsadowa w react-hookach https://github.com/facebook/react/issues/14259
źródło
To wystarczy:
const [state, setState] = useState({ username: '', password: ''}); // later setState({ ...state, username: 'John' });
źródło
Jeśli używasz haków innych firm i nie możesz scalić stanu w jeden obiekt lub użyć
useReducer
, rozwiązaniem jest użycie:ReactDOM.unstable_batchedUpdates(() => { ... })
Polecany przez Dana Abramowa tutaj
Zobacz ten przykład
źródło
Replikować
this.setState
zachowanie scalania z komponentów klasy, React docs zaleca się skorzystać z formularza funkcjonalnyuseState
z obiektu spread - nie ma potrzeby dlauseReducer
:setState(prevState => { return {...prevState, loading, data}; });
Te dwa stany są teraz skonsolidowane w jeden, co pozwoli zaoszczędzić cykl renderowania.
Jest jeszcze jedna zaleta z jednym obiektem stanu:
loading
idata
są stanami zależnymi . Nieprawidłowe zmiany stanu stają się bardziej widoczne, gdy stan jest złożony:setState({ loading: true, data }); // ups... loading, but we already set data
Można nawet lepiej zapewnić spójne stany o 1.) czyni status -
loading
,success
,error
, itd. - wyraźny w swoim stanie i 2.), używającuseReducer
do hermetyzacji logiki państwa w reduktor: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 ) }
Pokaż fragment kodu
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: Upewnij się, że niestandardowe hooki przedrostkiem
use
(useData
zamiastgetData
). Przeszedł również do oddzwanianiauseEffect
nie możnaasync
.źródło
Mały dodatek do odpowiedzi https://stackoverflow.com/a/53575023/121143
Fajne! Dla tych, którzy planują użyć tego hooka, mógłby on być napisany w nieco niezawodny sposób, aby pracować z funkcją jako argumentem, na przykład:
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]; };
Aktualizacja : zoptymalizowana wersja, stan nie zostanie zmodyfikowany, jeśli przychodzący stan częściowy nie został zmieniony.
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]; };
źródło
setMergedState
sięuseMemo(() => setMergedState, [])
, ponieważ, jak docs powiedzieć,setState
nie zmienia się między re-renderuje: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.
, w ten sposób funkcja setstate nie jest odtwarzany na re-renderuje.Można również użyć
useEffect
do wykrycia zmiany stanu i odpowiedniej aktualizacji innych wartości stanuźródło
useEffect
.