Как заставить компонент перерисовать с помощью хуков в React?

Рассматривая ниже пример хуков

   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>
        );
     }

По сути, мы используем метод this.forceUpdate(), чтобы заставить компонент немедленно повторно выполнить рендеринг в компонентах класса React, как показано в примере ниже.

    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>
        }
 }

Но мой вопрос: Как я могу заставить вышеуказанный функциональный компонент немедленно рендериться с помощью ловушек?

20 ответов

Это возможно с useState или же useReducer, поскольку useState использования useReducer внутренне:

let [, updateState] = React.useState();
...
updateState();

forceUpdate не предназначен для использования при обычных обстоятельствах, только в тестировании или других нерешенных случаях. Эта ситуация может быть решена более общепринятым способом.

setCount пример неправильно использованного forceUpdate, setState является асинхронным по соображениям производительности и не должен быть вынужден быть синхронным только потому, что обновления состояний не выполнялись правильно. Если состояние зависит от ранее установленного состояния, это должно быть сделано с помощью функции обновления,

Если вам нужно установить состояние на основе предыдущего состояния, прочтите об аргументе средства обновления ниже.

<...>

Как состояние, так и реквизиты, полученные функцией обновления, гарантированно будут актуальными. Вывод средства обновления поверхностно объединен с состоянием.

setCount может не быть иллюстративным примером, потому что его назначение неясно, но это относится к функции обновления:

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

Официальное решение React Hooks FAQ дляforceUpdate:

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

Рабочий пример

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

  return (
    <div>
      <button onClick={forceUpdate}>Force update</button>
      <p>Forced update {_} times</p>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js" integrity="sha256-vMEjoeSlzpWvres5mDlxmSKxx6jAmDNY4zCt712YCI0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.1/umd/react-dom.production.min.js" integrity="sha256-QQt6MpTdAD0DiPLhqhzVyPs1flIdstR4/R7x4GqCvZ4=" crossorigin="anonymous"></script>
<script>var useReducer = React.useReducer</script>
<div id="root"></div>

Как правило, можно использовать любой подход к обработке состояния, при котором требуется запускать обновление.

С TypeScript

codeandbox пример

useState

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

useReducer

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

как индивидуальный крючок

Просто оберните любой подход, который вы предпочитаете, вот так

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

Как это работает?

"Запустить обновление" означает сообщить движку React, что какое-то значение изменилось и что он должен повторно отрендерить ваш компонент.

[, setState] от useState()требуется параметр. Избавляемся от него привязав свежий объект{}.
() => ({}) в useReducer - это фиктивный редуктор, который возвращает новый объект каждый раз при отправке действия.
{} (свежий объект) необходим, чтобы он запускал обновление, изменяя ссылку в состоянии.

PS: useState просто обертывает useReducerвнутренне. источник

ПРИМЕЧАНИЕ. Использование.bind с useState вызывает изменение ссылки на функцию между отрисовками. Можно обернуть его внутри useCallback, как уже объяснялось здесь, но тогда это не будет привлекательнымоднострочником ™. Версия Reducerуже поддерживает ссылочное равенство между рендерами. Это важно, если вы хотите передать функцию forceUpdate в props.

простой JS

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

Как уже упоминали другие, useState работает - вот как MOBX-реагировать-Lite реализует обновления - вы можете сделать что-то подобное.

Упрощенный код выглядит так:

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

в другом месте -

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

См. https://github.com/mobxjs/mobx-react-lite/blob/master/src/utils.ts и https://github.com/mobxjs/mobx-react-lite/blob/master/src/useObserver.ts

Альтернатива ответу @MinhKha:

Это может быть намного чище с useReducer:

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

Применение:forceUpdate() - очиститель без параметров

Вы можете просто определить useState вот так:

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

И использование: forceUpdate(n => !n)

Надеюсь на эту помощь!

Простой код

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

Использование:

forceUpdate();

Желательно, чтобы ваш компонент зависел только от состояния и реквизита, и он будет работать как положено, но если вам действительно нужна функция для принудительного повторного рендеринга компонента, вы можете использовать useState подключите и вызовите функцию, когда это необходимо.

пример

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/react@16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.production.min.js"></script>

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

Возможный вариант - принудительное обновление только определенного компонента с помощью key. Обновление ключа запускает рендеринг компонента (который раньше не обновлялся)

Например:

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

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

...
<DataTable
    key={tableKey}
    data={tableData}/>

Вы можете (ab) использовать обычные хуки для принудительного повторного рендеринга, воспользовавшись тем фактом, что React не печатает логические значения в коде 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>
)

react-tidy есть специальный хук только для этого, называемый 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>
  )
}

Узнать больше об этом крючке

Отказ от ответственности Я автор этой библиотеки.

Однострочное решение:

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

useState возвращает пару значений: текущее состояние и функция, которая его обновляет - состояние и сеттер, здесь мы используем только сеттер для принудительного повторного рендеринга.

Моя вариация forceUpdate не через counter а скорее через объект:

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

Потому как {} !== {} каждый раз.

Решение в одной строке:

const [,forceRender] = useReducer((s) => s+1, 0)

Вы можете узнать об useReducer здесь. https://reactjs.org/docs/hooks-reference.html

      const useForceRender = () => {
  const [, forceRender] = useReducer(x => (x + 1) % 2, 0)
  return forceRender
}

использование

      function Component () {
  const forceRender = useForceRender() 
  useEffect(() => {
    // ...
    forceRender()
  }, [])

Это будет отображать зависимые компоненты 3 раза (массивы с одинаковыми элементами не равны):

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

setMsg(["test"])
setMsg(["test"])
setMsg(["test"])

Есть много способов принудительного повторного рендеринга в Hook.

Для меня простой способ с useState() и подсказка значений эталонного объекта.

      const [, forceRender] = useState({});

// Anywhre
forceRender({});

Codesandbox Пример

Немного поздно для вечеринки, но я заметил, что в большинстве (всех) ответов пропущена часть, где вы можете передать обратный вызов методу жизненного цикла forceUpdate.

Согласно исходному коду реакции , этот обратный вызов имеет такое же поведение, как и в методе setState — он выполняется после обновления.

Следовательно, наиболее правильная реализация будет такой:

Для обычных компонентов на основе React Class см. React Docs для forceUpdateapi по этому URL-адресу. В документах упоминается, что:

Обычно вы должны стараться избегать любого использования forceUpdate() и читать только из this.props и this.state в render()

Однако в документации также упоминается, что:

Если ваш метод render() зависит от некоторых других данных, вы можете сообщить React, что компоненту требуется повторный рендеринг, вызвав forceUpdate().

Итак, хотя варианты использования forceUpdate может быть редкостью, и я никогда не использовал его, однако я видел, как его использовали другие разработчики в некоторых устаревших корпоративных проектах, над которыми я работал.

Итак, для получения эквивалентной функциональности для функциональных компонентов обратитесь к React Docs for HOOKS по этому URL-адресу. По указанному выше URL-адресу можно использовать ловушку useReducer для предоставленияforceUpdate функциональность для функциональных компонентов.

Образец рабочего кода that does not use state or propsприведен ниже, который также доступен в CodeSandbox по этому 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);

ПРИМЕЧАНИЕ. Альтернативный подход с использованием ловушки useState (вместо useReducer) также доступен по этому URL-адресу.

Я работал с массивом и заметил эту проблему. Однако вместо явногоforceUpdateЯ нашел другой подход — деконструировать массив и установить для него новое значение с помощью этого кода:

          setRoutes(arr => [...arr, newRoute]); // add new elements to the array
    setRouteErrors(routeErrs => [...routeErrs]); // the array elements were changed

Мне показалось очень интересным, что установка даже копии массива не вызовет хук. Я предполагаю, что React делает поверхностное сравнение

Другие вопросы по тегам