Wielokrotne wywołania aktualizacji stanu z useState w komponencie powodują wielokrotne ponowne renderowanie

122

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ć?

jonhobbs
źródło
Jeśli masz stany zależne, prawdopodobnie powinieneś użyćuseReducer
thinklinux
Łał! Dopiero ja to odkryłem i całkowicie zniweczyło to moje zrozumienie, jak działa renderowanie reakcyjne. Nie rozumiem żadnej korzyści z pracy w ten sposób - wydaje się raczej arbitralne, że zachowanie w wywołaniu zwrotnym asynchronicznym jest inne niż w normalnej obsłudze zdarzeń. Przy okazji, w moich testach wydaje się, że uzgadnianie (tj. Aktualizacja prawdziwego DOM) następuje dopiero po przetworzeniu wszystkich wywołań setState, więc pośrednie wywołania renderowania i tak są marnowane.
Andy,

Odpowiedzi:

122

Możesz połączyć loadingstan i datastan w jeden obiekt stanu, a następnie możesz wykonać jedno setStatewywołanie i będzie tylko jeden render.

Uwaga: W przeciwieństwie do setStatekomponentów in class, setStatezwracany z useStatenie łą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ć setStatewywoł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>

Yangshun Tay
źródło
1
Zgadzam się, że nie jest to idealne rozwiązanie, ale nadal jest to rozsądny sposób na zrobienie tego. Możesz napisać własny, niestandardowy punkt zaczepienia, który dokonuje scalenia, jeśli nie chcesz wykonywać scalania samodzielnie. Zaktualizowałem ostatnie zdanie, aby było jaśniejsze. Czy wiesz, jak działa React? Jeśli nie, oto kilka linków, które mogą pomóc Ci lepiej zrozumieć - reactjs.org/docs/reconciliation.html , blog.vjeux.com/2013/javascript/react-performance.html .
Yangshun Tay
2
@jonhobbs Dodałem alternatywną odpowiedź, która tworzy useMergeStatehook, dzięki czemu możesz automatycznie scalić stan.
Yangshun Tay
1
Wspaniale, jeszcze raz dziękuję. Jestem coraz bardziej zaznajomiony z używaniem Reacta, ale mogę się wiele dowiedzieć o jego wewnętrznych cechach. Przeczytam im.
jonhobbs
2
Ciekawi Cię, w jakich okolicznościach to może być prawda ?: „ React możesetState
grupować
1
Fajnie ... myślałem o połączeniu stanu lub użyciu oddzielnego stanu ładunku i odczytaniu innych stanów z obiektu ładunku. Bardzo dobry post. 10/10 przeczyta ponownie
Anthony Moon Beam Toorie
63

Ma to również inne rozwiązanie wykorzystujące useReducer! najpierw określamy nasze nowe setState.

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 bez this!

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śniej this.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
}
Vahid Al
ź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ładniej setState. Jak to naprawić? Plik to js, ​​ale nadal używamy kontroli ts.
ZenVentzi
Niesamowity stary, dzięki
Nisharg Shah
Myślę, że to wciąż ten sam pomysł, że dwa stany się połączyły. Ale w niektórych przypadkach nie można scalić stanów.
user1888955
45

Aktualizacja wsadowa w react-hookach https://github.com/facebook/react/issues/14259

Obecnie React będzie zbiorczo aktualizować stan, jeśli zostaną wyzwolone z poziomu zdarzenia opartego na reakcji, takiego jak kliknięcie przycisku lub zmiana danych wejściowych. Nie będzie zbiorczo aktualizować, jeśli zostaną wyzwolone poza obsługą zdarzeń React, jak wywołanie asynchroniczne.

Sureshraj
źródło
5
To bardzo pomocna odpowiedź, ponieważ prawdopodobnie w większości przypadków rozwiązuje problem bez konieczności wykonywania jakichkolwiek czynności. Dzięki!
davnicwil
7

To wystarczy:

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

// later
setState({
    ...state,
    username: 'John'
});
Mehdi Dehghani
źródło
4

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

Mohamed Ramrami
źródło
Prawie rok później ta funkcja nadal wydaje się być całkowicie nieudokumentowana i nadal oznaczana jako niestabilna :-(
Andy,
4

Replikować this.setStatezachowanie scalania z komponentów klasy, React docs zaleca się skorzystać z formularza funkcjonalny useStatez obiektu spread - nie ma potrzeby dla useReducer:

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: loadingi datasą 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ąc useReducerdo 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 
  )
}

PS: Upewnij się, że niestandardowe hooki przedrostkiemuse ( useDatazamiast getData). Przeszedł również do oddzwaniania useEffectnie można async.

ford04
źródło
1
Głosował za tym! To jedna z najbardziej pomocnych funkcji, jakie znalazłem w React API. Znalazłem go, gdy próbowałem zapisać funkcję jako stan (wiem, że przechowywanie funkcji jest dziwne, ale musiałem to zrobić z jakiegoś powodu, którego nie pamiętam) i nie działało zgodnie z oczekiwaniami z powodu tej zarezerwowanej sygnatury wywołania
elquimista
1

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];
};
mschayna
źródło
Fajne! Chciałbym tylko owinąć setMergedStatesię useMemo(() => setMergedState, []), ponieważ, jak docs powiedzieć, setStatenie 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.
tonix
0

Można również użyć useEffectdo wykrycia zmiany stanu i odpowiedniej aktualizacji innych wartości stanu

JeanAlesi
źródło
Czy możesz rozwinąć? On już używa useEffect.
bluenote10