如何强制一个功能性的React组件进行渲染?

245

我有一个函数组件,想要强制它重新渲染。

我该如何做?
由于没有实例 this,我不能调用 this.forceUpdate()


1
不,无状态组件没有状态。请使用类代替。 - TryingToImprove
4
您是指“无状态组件”而不是“函数式组件”吗? - Chris
3
为了更新一个无状态组件,需要更改传入的props。 - fungusanthrax
2
除了props,您还可以使用hook useState,当它发生变化时组件将被重新渲染。 - Hamid Shoja
13个回答

406
你现在可以使用React hooks来使用useReducer(简短回答)。
const [, forceUpdate] = useReducer(x => x + 1, 0);

来自React FAQ

使用方法:

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

  return (
    <div onClick={forceUpdate}>
      Click me to refresh
    </div>
  );
}

使用 useState(更明确的答案)

使用 React hooks,你现在可以在函数组件中调用 useState()

useState() 将返回一个包含两个元素的数组:

  1. 一个值,表示当前的状态。
  2. 它的 setter。使用它来更新这个值。

通过 setter 更新值将会强制重新渲染你的函数组件
就像 forceUpdate 一样:

import React, { useState } from 'react';

//create your forceUpdate hook
function useForceUpdate(){
    const [value, setValue] = useState(0); // integer state
    return () => setValue(value => value + 1); // update state to force render
    // A function that increment  the previous state like here 
    // is better than directly setting `setValue(value + 1)`
}

function MyComponent() {
    // call your hook here
    const forceUpdate = useForceUpdate();
    
    return (
        <div>
            {/*Clicking on the button will force to re-render like force update does */}
            <button onClick={forceUpdate}>
                Click to re-render
            </button>
        </div>
    );
}

你可以在这里找到一个演示

上面的组件使用了一个自定义的钩子函数(useForceUpdate),它使用了 React 的状态钩子 useState。它增加了组件状态的值,从而告诉 React 重新渲染组件。


编辑

在这个答案的旧版本中,代码片段使用了一个布尔值,并在forceUpdate()中进行了切换。现在我已经编辑了我的答案,代码片段使用了一个数字而不是布尔值。

为什么?(你会问我)

因为曾经有一次,我的forceUpdate()被连续两次调用,来自两个不同的事件,因此它将布尔值重置为原始状态,导致组件从未渲染。

这是因为在useState的setter函数(这里是setValue)中,React会将前一个状态与新状态进行比较,只有状态不同才会进行渲染。


15
那个页面上没有任何关于如何使用 Hooks 调用 forceUpdate 的信息。 - jdelman
1
目前来看,你是正确的,因为即使钩子还没有发布,你仍然可以在beta中使用它。但是一旦它们发布了,没有理由类组件会更好。使用钩子使代码比类组件更清晰,就像下面的reactconf视频所示。无论如何,问题是这是否可能。由于钩子,答案现在从“否”变为“是”。https://www.youtube.com/watch?v=wXLf18DsV-I - Yairopro
2
嗨@DanteTheSmith。 "顶层"的意思是钩子不能从条件或循环内部调用,就像你说的那样。但我可以告诉你,你可以从另一个函数内部调用它们。这意味着创建一个自定义钩子。正如Dan Abramov在React conf中展示React hooks时所示,这是在功能组件之间共享逻辑的最清洁和最佳方法:https://youtu.be/dpw9EHDh2bM?t=2753 - Yairopro
5
是的,它确实进行了比较。我们谈论的是useState钩子,而不是类中的setState,除非你实现了shouldUpdate方法,否则它确实不会进行比较。请查看我发布的相同演示,但使用静态值作为setState,它不会再次渲染:https://codesandbox.io/s/determined-rubin-8598l - Yairopro
4
一个空对象也能达到同样的效果。 - Snowmanzzz
显示剩余20条评论

91

官方FAQ 现在建议这样做,如果你真的需要这样做:

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

  function handleClick() {
    forceUpdate();
  }

54
你可以使用以下代码将你的代码缩短7个字节,并且不会创建未使用的变量:const [, forceUpdate] = useReducer(x => x + 1, 0);。请注意,这不会改变原始意思。 - Konstantin Smolyanin
12
更短的 const forceUpdate = useReducer(x => x + 1, 0)[1] - vsync

78

更新 React v16.8(2019年2月16日发布)

自从React 16.8发布了hooks以来,函数组件具备了持久状态的能力。有了这个能力,你现在可以模拟一个forceUpdate

function App() {
  const [, forceUpdate] = React.useReducer(o => !o);
  console.log("render");
  return (
    <div>
      <button onClick={forceUpdate}>Force Render</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"/>

请注意,这种方法应该重新考虑,在大多数情况下,当您需要强制更新时,您可能做错了一些事情。

在 React 16.8.0 之前

不行,无状态函数组件只是普通的函数,返回 JSX,你无法访问 React 的生命周期方法,因为你没有继承自 React.Component。

把函数组件看作类组件中的 render 方法的一部分。


13
那不是强制重新渲染,那只是正常的渲染。当你想要强制渲染时,通常是在没有新的 propsstate 变化时运行渲染方法的情况下,例如。你不能强制执行无状态组件上的渲染函数,因为无状态组件上没有 render 函数。无状态组件不会 extends React.Component,它们只是返回 jsx 的普通函数。 - Sagiv b.g
3
感谢您对“当你需要强制更新时,你可能正在做一些错误的事情”的认可 - 我知道这是情况,但这促使我再次仔细查看我的useEffect钩子。 - Methodician

18

最简单的方法

如果你想强制重新渲染,可以添加一个虚拟状态来触发重新渲染。

const [rerender, setRerender] = useState(false);

...
setRerender(!rerender);     //whenever you want to re-render

这将确保重新渲染,您可以在任何地方调用setRerender(!rerender),以便您随时进行重新渲染 :)


3
请参见另一个答案评论线程,了解此方法无法正常工作的情况(称为两次场景)。 - GreenAsJade

11

我使用了一个名为use-force-update的第三方库来强制重新渲染我的React函数组件,非常好用。只需在项目中导入该包并像这样使用即可。

import useForceUpdate from 'use-force-update';

const MyButton = () => {

  const forceUpdate = useForceUpdate();

  const handleClick = () => {
    alert('I will re-render now.');
    forceUpdate();
  };

  return <button onClick={handleClick} />;
};

9
为了让您点击更省事,useForceUpdate使用了其他答案中提到的useCallback。这个库只是一个实用工具库,可以帮您省去一些打字。 - asyncwait

5

最佳实践-每次渲染时不要创建多余的变量:

const forceUpdateReducer = (i) => i + 1

export const useForceUpdate = () => {
  const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
  return forceUpdate
}

使用方法:

const forceUpdate = useForceUpdate()

forceUpdate()

2

如果您已经在函数组件中有一个状态,并且不想更改它,但需要重新渲染组件,您可以模拟一个状态更新,这将重新渲染组件。

const [items,setItems] = useState({
   name:'Your Name',
   status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}

这将保持状态不变,并让React认为状态已经更新。


1
如果您使用版本<16.8的函数组件,则一个解决方法是直接调用相同的函数,如下所示:
import React from 'react';

function MyComponent() {
    const forceUpdate = MyComponent();
    
    return (
        <div>
            <button onClick={forceUpdate}>
                Click to re-render
            </button>
        </div>
    );
}

但是如果您将某些属性传递给它,这将会破坏它。在我的情况下,我只是将接收到的相同属性传递给了rerender函数。


1

这些都没有给我一个令人满意的答案,所以最终我使用了 key 属性、useRef 和一些随机 ID 生成器(例如 shortid)来得到我想要的结果。

基本上,我想让某个聊天应用程序在某人第一次打开应用时自动播放。因此,我需要完全控制何时以及何种方式更新答案,并且希望使用异步等待的便利性。

示例代码:

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// ... your JSX functional component, import shortid somewhere

const [render, rerender] = useState(shortid.generate())

const messageList = useRef([
    new Message({id: 1, message: "Hi, let's get started!"})
])

useEffect(()=>{
    async function _ () {
      await sleep(500)
      messageList.current.push(new Message({id: 1, message: "What's your name?"}))
      // ... more stuff
      // now trigger the update
      rerender(shortid.generate())
   } 
   _()
}, [])

// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div> 

事实上,这也使我能够滚动类似于聊天消息的内容。
const waitChat = async (ms) => {
    let text = "."
    for (let i = 0; i < ms; i += 200) {
        if (messageList.current[messageList.current.length - 1].id === 100) {
            messageList.current = messageList.current.filter(({id}) => id !== 100)
        }
        messageList.current.push(new Message({
            id: 100,
            message: text
        }))
        if (text.length === 3) {
            text = "."
        } else {
            text += "."
        }
        rerender(shortid.generate())
        await sleep(200)
    }
    if (messageList.current[messageList.current.length - 1].id === 100) {
        messageList.current = messageList.current.filter(({id}) => id !== 100)
    }
}

在React的内置hooks中,如useEffect中使用await从来都不是一个好主意。此外,在第一个代码片段中,'await'不在'async'函数中,因此该代码将无法工作。如果您有一些任意的加载器或插件可以启用它,请务必提及,因为这不是默认配置。 - Josh Merlino
1
更新了一下,包括如何在useEffect中使用async/await的微不足道的示例。不幸的是,无论您个人的偏好如何,使用async/await在useEffect中通常都有非常好的用例。 - danieltan95

1

如果您在组件中添加一个属性(prop)和一个状态(state),则可以不使用提供的钩子(hooks)来完成此操作:

const ParentComponent = props => {
  const [updateNow, setUpdateNow] = useState(true)

  const updateFunc = () => {
    setUpdateNow(!updateNow)
  }

  const MyComponent = props => {
    return (<div> .... </div>)
  }

  const MyButtonComponent = props => {
    return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
  }

  return (
    <div> 
      <MyComponent updateMe={updateNow} />
      <MyButtonComponent updateFunc={updateFunc}/>
    </div>
  )
}

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