我该如何使用React hooks强制重新渲染组件?

221

考虑以下Hooks示例

   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() 方法来强制立即重新渲染组件。

Translated:

In class components, we can use the `this.forceUpdate()` method to force the component to be re-rendered immediately.

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

但我的问题是如何使用钩子立即强制上述功能组件进行重新渲染?


1
你能发布一个使用 this.forceUpdate() 的原始组件版本吗?也许有一种方法可以在不使用它的情况下实现相同的效果。 - Jacob
setCount 中的最后一行被截断了。当前状态下,setCount 的目的不清楚。 - Estus Flask
这只是在 this.forceUpdate() 之后的一个操作。我添加它只是为了解释我的问题中关于 this.forceUpdate() 的内容。 - Hemadri Dasari
1
说句闲话:我之所以摆脱不了这个问题,是因为我认为需要手动重新渲染,最后才意识到我只需要将一个外部变量移动到状态钩子中,并利用设置函数,就可以在没有重新渲染的情况下解决所有问题。并不是说它从来不需要用,但值得第三次和第四次查看以确定在您特定的用例中是否确实需要。 - Alexander Nied
20个回答

147

使用 useStateuseReducer 都可以实现这一点,因为 useState 在内部使用了 useReducer

const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => 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 }));
}

这与hooks一一对应,唯一的例外是被用作回调函数的函数最好被记忆化:

   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 }));
   }, []);

4
useCallback会记忆forceUpdate,所以在组件的生命周期内它保持不变,可以安全地作为prop传递。每次调用forceUpdate时,updateState({})会用新对象更新状态,因此会导致重新渲染。因此,是的,它在被调用时会强制更新。 - Estus Flask
4
所以,useCallback 部分并不是必须的。如果没有它也应该能正常工作。 - Dávid Molnár
1
@Andru 是的,状态只会更新一次,因为0 === 0。是的,数组也可以工作,因为它也是一个对象。任何不通过相等性检查的东西都可以使用,比如 updateState(Math.random()) 或者一个计数器。 - Estus Flask
@EstusFlask 实际上这个比较是使用 Object.is - 这与 Number.NaN 相等的自身标识比较不同(类似地,+0和-0也是不同的)。 - paul23
1
setStateforceUpdate之间的一个微不足道的区别是,forceUpdate跳过了shouldComponentUpdate的调用。但是使用hooks时,没有跳过React.memo的选项。 - Easwar
显示剩余2条评论

74

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>


71

通常情况下,您可以使用任何状态处理方法来触发更新。

使用TypeScript

codesandbox示例

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 中是一个虚拟的reducer,每次分派action时都返回一个新的对象。
{} (新对象) 是必需的,以便通过更改状态中的引用来触发更新。

PS: useState 只是在内部包装了 useReducer,因此使用reducer可以减少复杂性。来源

注意: 引用不稳定性

useState 中使用 .bind 会导致函数引用在渲染之间发生更改。
可以将其包装在 useCallback 中,如已在此答案中已经解释,但这样做就不再是一个 性感的一行代码™Reducer版本已经保持了引用平等(稳定)在渲染之间。如果你想将forceUpdate函数通过props传递给另一个组件,这一点非常重要。

纯JS

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

如果有条件地调用forceUpdate,那么这不会导致在渲染之间钩子被调用的次数不同吗?这将违反钩子的规则,并可能导致钩子访问错误的数据。 - user56reinstatemonica8
1
@user56reinstatemonica8 这基本上只是一个状态分配,触发渲染,没有什么奇怪的。 - Qwerty
4
否则我会得到一个类型错误,提示useState类型错误,因此我不得不使用以下typescript useState解决方法:const forceUpdate: () => void = React.useState({})[1].bind(null, {}); - SamB

43

正如其他人提到的那样,useState是有效的 - 这里是mobx-react-lite如何实现更新的方式 - 您可以做类似的事情。

定义一个新的钩子,useForceUpdate -

import { useState, useCallback } from 'react'

export function useForceUpdate() {
  const [, setTick] = useState(0);
  const update = useCallback(() => {
    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.tshttps://github.com/mobxjs/mobx-react-lite/blob/master/src/useObserver.ts


3
据我对hooks的理解,这可能行不通,因为useForceUpdate会在每次函数重新渲染时返回一个新的函数。为了让forceUpdateuseEffect中正常工作,它应该返回useCallback(update)。请参阅https://kentcdodds.com/blog/usememo-and-usecallback。 - Martin Ratinaud
谢谢,@MartinRatinaud - 是的,如果没有使用 useCallback 可能会导致内存泄漏 - 已修复。 - Brian Burns

32

@MinhKha's 解答的替代方案:

使用 useReducer 可以让代码更加简洁:

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

使用方法: forceUpdate() - 清理不带参数


16

您可以简单地定义useState:

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

使用方法:forceUpdate(n => !n)

希望这有所帮助!


13
如果在每次渲染中调用forceUpdate的次数为偶数,则会失败。 - Izhaki
2
只需不断增加该值即可。 - Gary
3
这种做法容易出错,应该予以删除或编辑。 - slikts

14

最好只让你的组件依赖于状态和属性,这样它就可以按预期工作。但是如果你真的需要一个函数来强制重新渲染组件,你可以使用 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>


好的,但为什么React在早期版本中组件使用setState重新渲染时,还要引入this.forceUpdate()呢? - Hemadri Dasari
1
@Think-Twice 我个人从未使用过它,现在我也想不出一个好的用例,但我猜它可以作为那些非常特殊的情况下的紧急备选方案。"通常情况下,你应该尽量避免使用 forceUpdate(),并且只在 render() 函数中从 this.propsthis.state 中读取数据。" - Tholle
1
同意。在我的经验中,我甚至从未使用过它,但我知道它的工作原理,所以只是试图了解如何在Hooks中完成相同的事情。 - Hemadri Dasari
1
@Tholle 我现在想不出一个好的用例,我有一个,如果状态不是由React控制怎么办。我不使用Redux,但我假设它必须执行某种forceupdate。我个人使用代理来维护状态,然后组件可以检查prop更改,然后更新。似乎效率非常高。例如,我的所有组件都由代理控制,这个代理由SessionStorage支持,因此即使用户刷新网页,甚至下拉菜单等的状态也会保持不变。换句话说:我根本不使用状态,一切都由props控制。 - Keith
@Tholle 好的,但如果我想重新渲染组件,而又没有从父组件传递更改后的属性的可能性呢? - Matley

14

简单的代码

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

使用:

forceUpdate();

1
为了避免TypeScript错误(例如,useReducer(bool => !bool, true)[1]),建议提供一个初始化值。但是除此之外...这个解决方案真的很好!我觉得这个解决方案比官方文档中的示例要更好,因为它可以避免无用状态值无限增加。 - Philzen

9

通过使用 key,强制仅对特定组件进行更新是一种可能的选项。更新键会触发组件的重新渲染(之前未能更新)。

例如:

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

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

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

1
如果状态值与重新渲染的要求之间存在1:1的关系,那么这通常是最清晰的方法。 - jaimefps
1
这是一个简单的解决方案。 - Priyanka V
请注意,这将完全重新挂载<DataTable />组件,而不是重新渲染它。 - bryanph
这是最简单的解决方案!我的一个组件在子组件更改状态后不想重新渲染...实际上,这是我第一次遇到这样的问题。 - Alexander Cherednichenko

6

您可以通过利用React在JSX代码中不打印布尔值这一事实,滥用普通的hooks来强制重新渲染。

// 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>
)


1
在这种情况下,当setBoolean更改两次时,子组件React.useEffect可能无法识别更新。 - alma
据我所知,React将每个布尔更新都视为重新渲染页面的原因,即使布尔值很快又切换回来。话虽如此,React当然不是一个标准,它在这种情况下的工作方式是未定义的,并且可能会发生变化。 - Fergie
1
我不知道。出于某种原因,我被这个版本吸引了。它让我感到愉悦 :-)。此外,它感觉很纯粹。我的JSX依赖发生了一些变化,所以我重新渲染。在我看来,它的不可见性并没有影响这一点。 - Adrian Bartholomew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接