Jak zmusić komponent do ponownego renderowania za pomocą haków w Reakcie?

116

Rozważając poniżej przykład haków

   import { useState } from 'react';

   function Example() {
       const [count, setCount] = useState(0);

       return (
           <div>
               <p>You clicked {count} times</p>
               <button onClick={() => setCount(count + 1)}>
                  Click me
               </button>
          </div>
        );
     }

Zasadniczo używamy metody this.forceUpdate (), aby wymusić na komponencie natychmiastowe ponowne renderowanie w komponentach klasy React, jak na poniższym przykładzie

    class Test extends Component{
        constructor(props){
             super(props);
             this.state = {
                 count:0,
                 count2: 100
             }
             this.setCount = this.setCount.bind(this);//how can I do this with hooks in functional component 
        }
        setCount(){
              let count = this.state.count;
                   count = count+1;
              let count2 = this.state.count2;
                   count2 = count2+1;
              this.setState({count});
              this.forceUpdate();
              //before below setState the component will re-render immediately when this.forceUpdate() is called
              this.setState({count2: count
        }

        render(){
              return (<div>
                   <span>Count: {this.state.count}></span>. 
                   <button onClick={this.setCount}></button>
                 </div>
        }
 }

Ale moje zapytanie brzmi: Jak zmusić powyższy komponent funkcjonalny do natychmiastowego ponownego renderowania za pomocą haków?

Hemadri Dasari
źródło
1
Czy możesz opublikować wersję swojego oryginalnego komponentu, która używa this.forceUpdate()? Może jest sposób na osiągnięcie tego samego bez tego.
Jacob
Ostatnia linia w setCount jest obcinana. Nie jest jasne, jaki jest cel setCount w obecnym stanie.
Estus Flask
To tylko akcja po this.forceUpdate (); Dodałem to tylko po to, aby to wyjaśnić. ForceUpdate () w moim pytaniu
Hemadri Dasari
Co jest warte: zmagałem się z tym, ponieważ myślałem, że potrzebuję ręcznego ponownego renderowania, i w końcu zdałem sobie sprawę, że muszę po prostu przenieść zewnętrzną zmienną do haka stanu i wykorzystać funkcję ustawień, która naprawiła wszystkie moje problemy bez ponowne renderowanie. Nie znaczy to, że nigdy nie jest potrzebne, ale warto przyjrzeć się trzeciemu i czwartemu spojrzeniu, aby sprawdzić, czy jest rzeczywiście potrzebne w Twoim konkretnym przypadku użycia.
Alexander Nied

Odpowiedzi:

77

Jest to możliwe z useStatelub useReducer, ponieważ useStateużywa useReducerwewnętrznie :

const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);

forceUpdatenie jest przeznaczony do użytku w normalnych okolicznościach, tylko w testach lub innych wyjątkowych przypadkach. Do tej sytuacji można podejść w bardziej konwencjonalny sposób.

setCountjest przykładem nieprawidłowego użycia forceUpdate, setStatejest asynchroniczny ze względu na wydajność i nie powinien być wymuszany na synchronizacji tylko dlatego, że aktualizacje stanu nie zostały wykonane poprawnie. Jeśli stan opiera się na wcześniej ustawionym stanie, należy to zrobić za pomocą funkcji Updater ,

Jeśli chcesz ustawić stan na podstawie poprzedniego stanu, przeczytaj o argumencie aktualizującym poniżej.

<...>

Gwarantujemy, że stan i właściwości otrzymane przez funkcję aktualizującą są aktualne. Dane wyjściowe aktualizatora są płytko łączone ze stanem.

setCount może nie być ilustracyjnym przykładem, ponieważ jego cel jest niejasny, ale tak jest w przypadku funkcji aktualizatora:

setCount(){
  this.setState(({count}) => ({ count: count + 1 }));
  this.setState(({count2}) => ({ count2: count + 1 }));
  this.setState(({count}) => ({ count2: count + 1 }));
}

Jest to tłumaczone 1: 1 na podpięcia, z wyjątkiem funkcji, które są używane jako wywołania zwrotne, powinny być lepiej zapamiętywane:

   const [state, setState] = useState({ count: 0, count2: 100 });

   const setCount = useCallback(() => {
     setState(({count}) => ({ count: count + 1 }));
     setState(({count2}) => ({ count2: count + 1 }));
     setState(({count}) => ({ count2: count + 1 }));
   }, []);
Estus Flask
źródło
Jak to const forceUpdate = useCallback(() => updateState({}), []);działa? Czy to w ogóle wymusza aktualizację?
Dávid Molnár
2
@ DávidMolnár useCallbackzapamiętuje forceUpdate, więc pozostaje niezmienna przez cały okres eksploatacji elementu i może być bezpiecznie przekazywana jako rekwizyt. updateState({})aktualizuje stan nowym obiektem przy każdym forceUpdatewywołaniu, co powoduje ponowne renderowanie. Więc tak, wymusza aktualizację po wywołaniu.
Estus Flask
3
Tak więc ta useCallbackczęść nie jest naprawdę konieczna. Powinno po prostu działać bez niego.
Dávid Molnár
I nie zadziała, updateState(0)ponieważ 0jest to prymitywny typ danych? Czy musi to być obiekt, czy też updateState([])(np. Użycie z tablicą) również zadziała?
Andru
1
@Andru Tak, stan jest aktualizowany raz, ponieważ 0 === 0. Tak, tablica będzie działać, ponieważ jest również obiektem. Wszystko, co nie przejdzie kontroli równości, może zostać użyte, na przykład updateState(Math.random())lub jako licznik.
Estus Flask
29

Jak wspomnieli inni, useStatedziała - oto jak mobx-react-lite wdraża aktualizacje - możesz zrobić coś podobnego.

Zdefiniuj nowy hak, useForceUpdate-

import { useState, useCallback } from 'react'

export function useForceUpdate() {
  const [, setTick] = useState(0);
  const update = useCallback(() => {
    setTick(tick => tick + 1);
  }, [])
  return update;
}

i użyj go w komponencie -

const forceUpdate = useForceUpdate();
if (...) {
  forceUpdate(); // force re-render
}

Zobacz https://github.com/mobxjs/mobx-react-lite/blob/master/src/utils.ts i https://github.com/mobxjs/mobx-react-lite/blob/master/src/useObserver .ts

Brian Burns
źródło
2
Z tego, co rozumiem z hooków, może to nie działać, ponieważ useForceUpdatezwróci nową funkcję za każdym razem, gdy funkcja zostanie ponownie renderowana. Aby forceUpdatedziałać, gdy jest używany w useEffect, powinien zwrócić useCallback(update)Zobacz kentcdodds.com/blog/usememo-and-usecallback
Martin Ratinaud
Dzięki, @MartinRatinaud - tak, może to spowodować wyciek pamięci bez useCallback (?) - naprawione.
Brian Burns
27

Ogólnie rzecz biorąc, możesz użyć dowolnego podejścia do obsługi stanu, które chcesz wyzwolić aktualizację.

Dzięki TypeScript

przykład kodów i skrzynki

useState

const forceUpdate: () => void = React.useState()[1].bind(null, {})  // see NOTE below

useReducer

const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void

jako niestandardowy haczyk

Po prostu zapakuj dowolne podejście, które preferujesz w ten sposób

function useForceUpdate(): () => void {
  return React.useReducer(() => ({}), {})[1] as () => void // <- paste here
}

Jak to działa?

Wyzwolenie aktualizacji oznacza powiadomienie silnika React, że jakaś wartość uległa zmianie i że powinien on ponownie zwrócić Twój komponent.

[, setState]od useState()wymaga parametru. Pozbywamy się go, wiążąc świeży obiekt {}.
() => ({})in useReducerjest fikcyjnym reduktorem, który zwraca nowy obiekt za każdym razem, gdy wykonywana jest akcja.
{} (świeży obiekt) jest wymagany, aby wyzwalał aktualizację poprzez zmianę odwołania w stanie.

PS: useStatepo prostu zawija się useReducerwewnętrznie. źródło

UWAGA: Używanie .bind z useState powoduje zmianę odwołania do funkcji między renderowaniami. Możliwe jest owinięcie go wewnątrz useCallback, jak już wyjaśniono tutaj , ale wtedy nie byłby to seksowny one-liner ™ . Wersja Reducer już utrzymuje równość odwołań między renderowaniami. Jest to ważne, jeśli chcesz przekazać funkcję forceUpdate we właściwościach.

zwykły JS

const forceUpdate = React.useState()[1].bind(null, {})  // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
Qwerty
źródło
17

Alternatywa dla odpowiedzi @ MinhKha:

Może być znacznie czystszy dzięki useReducer:

const [, forceUpdate] = useReducer(x => x + 1, 0);

Zastosowanie: forceUpdate()- odkurzacz bez parametrów

nullhook
źródło
13

Możesz po prostu zdefiniować useState w ten sposób:

const [, forceUpdate] = React.useState(0);

I zastosowanie: forceUpdate(n => !n)

Mam nadzieję, że to pomoże !

Minh Kha
źródło
7
Nie powiedzie się, jeśli forceUpdate zostanie wywołana parzystą liczbę razy na renderowanie.
Izhaki
Po prostu zwiększaj wartość.
Gary
2
Jest to podatne na błędy i należy je usunąć lub edytować.
slikts
11

Oficjalne rozwiązanie React Hooks FAQ dla forceUpdate:

const [_, forceUpdate] = useReducer((x) => x + 1, 0);
// usage
<button onClick={forceUpdate}>Force update</button>

Przykład roboczy

ford04
źródło
10

Najlepiej, aby twój komponent był zależny tylko od stanu i właściwości i będzie działał zgodnie z oczekiwaniami, ale jeśli naprawdę potrzebujesz funkcji, aby zmusić komponent do ponownego renderowania, możesz użyć useStatehaka i wywołać funkcję w razie potrzeby.

Przykład

const { useState, useEffect } = React;

function Foo() {
  const [, forceUpdate] = useState();

  useEffect(() => {
    setTimeout(forceUpdate, 2000);
  }, []);

  return <div>{Date.now()}</div>;
}

ReactDOM.render(<Foo />, document.getElementById("root"));
<script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Tholle
źródło
Ok, ale dlaczego React wprowadził this.forceUpdate (); na pierwszym miejscu, gdy komponent renderuje się ponownie z setState we wcześniejszych wersjach?
Hemadri Dasari
1
@ Pomyśl - dwa razy Osobiście nigdy go nie używałem i nie mogę wymyślić dobrego przypadku użycia dla niego w tej chwili, ale myślę, że jest to luk ewakuacyjny dla tych naprawdę specjalnych przypadków użycia. „Normalnie należy starać się unikać wszelkich zastosowań forceUpdate()i tylko do odczytu z this.propsi this.statew render().”
Tholle
1
Zgoda. Z mojego doświadczenia nawet tego nie korzystałem, ale wiem, jak to działa, więc po prostu próbuję zrozumieć, jak to samo można zrobić w hakach
Hemadri Dasari
1
@Tholle and I can't think of a good use case for it right now Mam jeden, a co powiesz na to, że stan nie jest kontrolowany przez React. Nie używam Reduxa, ale zakładam, że musi dokonać jakiejś wymuszonej aktualizacji. Osobiście używam proxy do utrzymania stanu, komponent może następnie sprawdzić zmiany właściwości, a następnie zaktualizować. Wydaje się też działać bardzo wydajnie. Np. Wszystkie moje komponenty są następnie kontrolowane przez proxy, ten serwer proxy jest obsługiwany przez SessionStorage, więc nawet jeśli użytkownik odświeży swoją stronę internetową, stan listy rozwijanej itp. Jest utrzymywany. IOW: W ogóle nie używam stanu, wszystko jest kontrolowane za pomocą rekwizytów.
Keith,
8

Prosty kod

const forceUpdate = React.useReducer(bool => !bool)[1];

Posługiwać się:

forceUpdate();
GilCarvalhoDev
źródło
5

Możesz (ab) użyć normalnych hooków, aby wymusić ponowne wydanie, wykorzystując fakt, że React nie drukuje wartości logicznych w kodzie JSX

// create a hook
const [forceRerender, setForceRerender] = React.useState(true);

// ...put this line where you want to force a rerender
setForceRerender(!forceRerender);

// ...make sure that {forceRerender} is "visible" in your js code
// ({forceRerender} will not actually be visible since booleans are
// not printed, but updating its value will nonetheless force a
// rerender)
return (
  <div>{forceRerender}</div>
)

Fergie
źródło
1
W takim przypadku, gdy setBoolean zmieni się dwukrotnie, element podrzędny React.useEffect może nie rozpoznać aktualizacji.
alma
O ile rozumiem, React traktuje każdą aktualizację logiczną jako powód do ponownego wyrejestrowania strony, nawet jeśli wartość logiczna szybko się z powrotem przełączy. To powiedziawszy, React oczywiście nie jest standardem, a dokładnie to, jak działa w tym przypadku, jest nieokreślone i może ulec zmianie.
Fergie
1
Nie wiem Z jakiegoś powodu pociąga mnie ta wersja. Łaskocze mnie :-). Poza tym wydaje się czysty. Coś się zmieniło, od czego zależy mój JSX, więc ponownie się poddaję. Jego niewidzialność nie umniejsza tej IMO.
Adrian Bartholomew
4

Potencjalną opcją jest wymuszenie aktualizacji tylko przy użyciu określonego komponentu key. Aktualizacja klucza wyzwala renderowanie komponentu (którego wcześniej nie udało się zaktualizować)

Na przykład:

const [tableKey, setTableKey] = useState(1);
...

useEffect(() => {
    ...
    setTableKey(tableKey + 1);
}, [tableData]);

...
<DataTable
    key={tableKey}
    data={tableData}/>
Idan
źródło
1
Jest to często najczystszy sposób, jeśli istnieje relacja 1: 1 między wartością stanu a wymogiem ponownego renderowania.
jaimefps
1
to jest łatwe rozwiązanie
Priyanka V
3

Rozwiązanie jednoprzewodowe:

const useForceUpdate = () => useState()[1];

useState zwraca parę wartości: aktualny stan i funkcję, która go aktualizuje - stan i ustawiający , tutaj używamy tylko ustawiającego, aby wymusić ponowne renderowanie.

arielhad
źródło
2

Moja odmiana forceUpdatenie polega na a, counterale raczej poprzez obiekt:

// Emulates `forceUpdate()`
const [unusedState, setUnusedState] = useState()
const forceUpdate = useCallback(() => setUnusedState({}), [])

Ponieważ za {} !== {}każdym razem.

katamfetamina
źródło
Co to jest useCallback()? Skąd to pochodzi? ups. Widzę to teraz ...
zipzit
2

Spowoduje to trzykrotne renderowanie komponentów zależnych (tablice z równymi elementami nie są równe):

const [msg, setMsg] = useState([""])

setMsg(["test"])
setMsg(["test"])
setMsg(["test"])
Janek Olszak
źródło
1
Uważam, że nie musisz nawet umieszczać elementu w tablicy. Pusta tablica nie jest równa innej pustej tablicy po prostu przez inne odniesienie, tak jak obiekty.
Qwerty
tak, chciałem tylko to pokazać jako sposób przekazywania danych
Janek Olszak
2

react-tidyma niestandardowy punkt zaczepienia, który służy do tego, o nazwie useRefresh:

import React from 'react'
import {useRefresh} from 'react-tidy'

function App() {
  const refresh = useRefresh()
  return (
    <p>
      The time is {new Date()} <button onClick={refresh}>Refresh</button>
    </p>
  )
}

Dowiedz się więcej o tym haku

Zastrzeżenie Jestem autorem tej biblioteki.

webNeat
źródło
1

W przypadku zwykłych komponentów opartych na klasie React, zapoznaj się z dokumentami React dla forceUpdateinterfejsu API pod tym adresem URL. Dokumenty wspominają, że:

Zwykle powinieneś unikać wszelkich zastosowań forceUpdate () i czytać tylko z this.props i this.state w render ()

Jednak w dokumentach wspomniano również, że:

Jeśli twoja metoda render () zależy od innych danych, możesz powiedzieć Reactowi, że komponent wymaga ponownego renderowania, wywołując forceUpdate ().

Tak więc, chociaż przypadki użycia forceUpdate mogą być rzadkie i nigdy z niego nie korzystałem, jednak widziałem go używanego przez innych programistów w niektórych starszych projektach korporacyjnych, nad którymi pracowałem.

Tak więc, aby uzyskać równoważną funkcjonalność dla komponentów funkcjonalnych, zapoznaj się z dokumentacją React dla HOOKSów pod tym adresem URL. W powyższym adresie URL można użyć haka „useReducer”, aby zapewnić plikforceUpdate funkcjonalności dla komponentów funkcjonalnych.

Poniżej znajduje się działający przykład kodu that does not use state or props, który jest również dostępny w CodeSandbox pod tym adresem URL

import React, { useReducer, useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  // Use the useRef hook to store a mutable value inside a functional component for the counter
  let countref = useRef(0);

  const [, forceUpdate] = useReducer(x => x + 1, 0);

  function handleClick() {
    countref.current++;
    console.log("Count = ", countref.current);
    forceUpdate(); // If you comment this out, the date and count in the screen will not be updated
  }

  return (
    <div className="App">
      <h1> {new Date().toLocaleString()} </h1>
      <h2>You clicked {countref.current} times</h2>
      <button
        onClick={() => {
          handleClick();
        }}
      >
        ClickToUpdateDateAndCount
      </button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

UWAGA: Alternatywne podejście wykorzystujące hak useState (zamiast useReducer) jest również dostępne pod tym adresem URL.

Alan CS
źródło