你能在不调用 setState 的情况下强制 React 组件重新渲染吗?

Can you force a React component to rerender without calling setState?

我有一个外部(组件)可观察对象,我想监听它的变化。当对象更新时,它会发出更改事件,然后我想在检测到任何更改时重新呈现组件。

在顶级 React.render 中这是可能的,但在一个组件中它不起作用(这有一定道理,因为 render 方法只是 returns 一个对象).

这是一个代码示例:

export default class MyComponent extends React.Component {

  handleButtonClick() {
    this.render();
  }

  render() {
    return (
      <div>
        {Math.random()}
        <button onClick={this.handleButtonClick.bind(this)}>
          Click me
        </button>
      </div>
    )
  }
}

单击按钮会在内部调用 this.render(),但这并不是真正导致渲染发生的原因(您可以在实际操作中看到这一点,因为 {Math.random()} 创建的文本不会更改)。但是,如果我简单地调用 this.setState() 而不是 this.render(),它工作正常。

所以我想我的问题是: React 组件需要 有状态才能重新渲染吗?有没有办法在不改变状态的情况下强制组件按需更新?

在 class 个组件中,您可以调用 this.forceUpdate() 强制重新渲染。

文档:https://facebook.github.io/react/docs/component-api.html

在函数组件中,没有 forceUpdate 的等价物,但您可以

当您希望两个不受关系(父子关系)约束的 React 组件进行通信时,建议使用 Flux 或类似的架构。

你要做的是监听observable component store 的变化,它保存了模型和它的接口,并将导致渲染变化的数据保存为stateMyComponent 中。当商店推送新数据时,您更改组件的状态,这会自动触发渲染。

通常你应该尽量避免使用 forceUpdate() 。来自文档:

Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). This makes your application much simpler and more efficient

forceUpdate 应该避免,因为它偏离了 React 的思维方式。 React 文档引用了何时可以使用 forceUpdate 的示例:

By default, when your component's state or props change, your component will re-render. However, if these change implicitly (eg: data deep within an object changes without changing the object itself) or if your render() method depends on some other data, you can tell React that it needs to re-run render() by calling forceUpdate().

但是,我想提出这样的想法,即使对象嵌套很深,forceUpdate 也是不必要的。通过使用不可变的数据源跟踪变化变得便宜;更改总是会产生一个新对象,因此我们只需要检查对对象的引用是否已更改。您可以使用库 Immutable JS 将不可变数据对象实现到您的应用程序中。

Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). This makes your component "pure" and your application much simpler and more efficient.forceUpdate()

更改所需元素的键 re-rendered 即可。通过状态在您的元素上设置 key 道具,然后当您想要更新设置状态以拥有新密钥时。

<Element key={this.state.key} /> 

然后发生变化,您重置密钥

this.setState({ key: Math.random() });

我要注意,这将替换键正在更改的元素。这可能有用的一个示例是当您有一个文件输入字段时,您希望在图像上传后重置该字段。

虽然 OP 问题的真正答案是 forceUpdate() 我发现此解决方案在不同情况下很有用。我还想指出,如果您发现自己在使用 forceUpdate,您可能需要检查您的代码,看看是否有其他方法可以做事。

注意 2019 年 1 月 9 日:

以上(更改键)将完全替换元素。如果您发现自己更新密钥以进行更改,则您的代码中的其他地方可能存在问题。在键中使用 Math.random() 将 re-create 元素与每个渲染。我不建议像这样更新密钥,因为 React 使用密钥来确定 re-render 事物的最佳方式。

实际上,forceUpdate() 是唯一正确的解决方案,因为 setState() 可能 不会 如果在 shouldComponentUpdate() 或者只是 returns false.

强制更新()

Calling forceUpdate() will cause render() to be called on the component, skipping shouldComponentUpdate(). more...

设置状态()

setState() will always trigger a re-render unless conditional rendering logic is implemented in shouldComponentUpdate(). more...


forceUpdate() 可以通过 this.forceUpdate()

从您的组件中调用


挂钩:


顺便说一句:你是在改变状态还是你的嵌套属性不传播?

So I guess my question is: do React components need to have state in order to rerender? Is there a way to force the component to update on demand without changing the state?

其他答案试图说明您如何可以,但重点是您不应该。即使是更改密钥的 hacky 解决方案也没有抓住要点。 React 的强大之处在于放弃了手动管理什么时候应该渲染的控制权,而是只关心你自己应该如何映射输入。然后提供输入流。

如果您需要手动强制重新渲染,几乎可以肯定您做错了什么。

我通过以下

避免了forceUpdate

WRONG WAY : do not use index as key

this.state.rows.map((item, index) =>
   <MyComponent cell={item} key={index} />
)

CORRECT WAY : Use data id as key, it can be some guid etc

this.state.rows.map((item) =>
   <MyComponent item={item} key={item.id} />
)

因此,通过进行此类代码改进,您的组件将 UNIQUE 并自然呈现

您可以通过几种方式完成:

1.使用forceUpdate()方法:

使用forceUpdate()方法时可能会出现一些问题。一个例子是它忽略了 shouldComponentUpdate() 方法,并且无论 shouldComponentUpdate() returns 是否为 false 都会重新渲染视图。因此,应尽可能避免使用 forceUpdate()。

2。将 this.state 传递给 setState() 方法

下面的代码行解决了前面例子中的问题:

this.setState(this.state);

实际上所有这一切都是用触发重新渲染的当前状态覆盖当前状态。这仍然不一定是最好的做事方式,但它确实克服了您在使用 forceUpdate() 方法时可能遇到的一些故障。

使用 hooks 或 HOC 任你选择

使用 hooksHOC(高阶组件)模式,您可以在商店更改时自动更新。这是一种没有框架的非常轻量级的方法。

useStore 处理商店更新的挂钩方式

interface ISimpleStore {
  on: (ev: string, fn: () => void) => void;
  off: (ev: string, fn: () => void) => void;
}

export default function useStore<T extends ISimpleStore>(store: T) {
  const [storeState, setStoreState] = useState({store});
  useEffect(() => {
    const onChange = () => {
      setStoreState({store});
    }
    store.on('change', onChange);
    return () => {
      store.off('change', onChange);
    }
  }, []);
  return storeState.store;
}

withStores HOC 处理商店更新

export default function (...stores: SimpleStore[]) {
  return function (WrappedComponent: React.ComponentType<any>) {
    return class WithStore extends PureComponent<{}, {lastUpdated: number}> {
      constructor(props: React.ComponentProps<any>) {
        super(props);
        this.state = {
          lastUpdated: Date.now(),
        };
        this.stores = stores;
      }

      private stores?: SimpleStore[];

      private onChange = () => {
        this.setState({lastUpdated: Date.now()});
      };

      componentDidMount = () => {
        this.stores &&
          this.stores.forEach((store) => {
            // each store has a common change event to subscribe to
            store.on('change', this.onChange);
          });
      };

      componentWillUnmount = () => {
        this.stores &&
          this.stores.forEach((store) => {
            store.off('change', this.onChange);
          });
      };

      render() {
        return (
          <WrappedComponent
            lastUpdated={this.state.lastUpdated}
            {...this.props}
          />
        );
      }
    };
  };
}

SimpleStore class

import AsyncStorage from '@react-native-community/async-storage';
import ee, {Emitter} from 'event-emitter';

interface SimpleStoreArgs {
  key?: string;
  defaultState?: {[key: string]: any};
}

export default class SimpleStore {
  constructor({key, defaultState}: SimpleStoreArgs) {
    if (key) {
      this.key = key;
      // hydrate here if you want w/ localState or AsyncStorage
    }
    if (defaultState) {
      this._state = {...defaultState, loaded: false};
    } else {
      this._state = {loaded: true};
    }
  }
  protected key: string = '';
  protected _state: {[key: string]: any} = {};
  protected eventEmitter: Emitter = ee({});
  public setState(newState: {[key: string]: any}) {
    this._state = {...this._state, ...newState};
    this.eventEmitter.emit('change');
    if (this.key) {
      // store on client w/ localState or AsyncStorage
    }
  }
  public get state() {
    return this._state;
  }
  public on(ev: string, fn:() => void) {
    this.eventEmitter.on(ev, fn);
  }
  public off(ev: string, fn:() => void) {
    this.eventEmitter.off(ev, fn);
  }
  public get loaded(): boolean {
    return !!this._state.loaded;
  }
}

使用方法

在钩子的情况下:

// use inside function like so
const someState = useStore(myStore);
someState.myProp = 'something';

在 HOC 的情况下:

// inside your code get/set your store and stuff just updates
const val = myStore.myProp;
myOtherStore.myProp = 'something';
// return your wrapped component like so
export default withStores(myStore)(MyComponent);

确定 像这样将您的商店导出为单例以获得全局变化的好处:

class MyStore extends SimpleStore {
  public get someProp() {
    return this._state.someProp || '';
  }
  public set someProp(value: string) {
    this.setState({...this._state, someProp: value});
  }
}
// this is a singleton
const myStore = new MyStore();
export {myStore};

这种方法非常简单,适合我。我还在大型团队中工作,使用 Redux 和 MobX,发现它们也很好,但只是有很多样板文件。我个人喜欢我自己的方法,因为我总是讨厌大量的代码,而这些代码在您需要时可以很简单。

我发现最好避免使用 forceUpdate()。强制重新渲染的一种方法是添加 render() 对临时外部变量的依赖,并在需要时更改该变量的值。

这是一个代码示例:

class Example extends Component{
   constructor(props){
      this.state = {temp:0};

      this.forceChange = this.forceChange.bind(this);
   }

   forceChange(){
      this.setState(prevState => ({
          temp: prevState.temp++
      })); 
   }

   render(){
      return(
         <div>{this.state.temp &&
             <div>
                  ... add code here ...
             </div>}
         </div>
      )
   }
}

需要强制重新渲染时调用this.forceChange()。

ES6 - 我包括了一个对我有帮助的例子:

在 "short if statement" 中,您可以像这样传递空函数:

isReady ? ()=>{} : onClick

这似乎是最短的方法。

()=>{}

我们可以使用 this.forceUpdate() 如下。

       class MyComponent extends React.Component {



      handleButtonClick = ()=>{
          this.forceUpdate();
     }


 render() {

   return (
     <div>
      {Math.random()}
        <button  onClick={this.handleButtonClick}>
        Click me
        </button>
     </div>
    )
  }
}

 ReactDOM.render(<MyComponent /> , mountNode);

即使您使用 setState 重新渲染组件,DOM 中的元素 'Math.random' 部分也只会更新。

这里的所有答案都是正确的,补充了理解的问题..正如我们所知,在不使用 setState({}) 的情况下重新渲染组件是通过使用 forceUpdate()。

上面的代码使用 setState 运行,如下所示。

 class MyComponent extends React.Component {



             handleButtonClick = ()=>{
                this.setState({ });
              }


        render() {
         return (
  <div>
    {Math.random()}
    <button  onClick={this.handleButtonClick}>
      Click me
    </button>
  </div>
)
  }
 }

ReactDOM.render(<MyComponent /> , mountNode);

另一种方法是调用 setStateA​​ND 保存状态:

this.setState(prevState=>({...prevState}));

forceUpdate(); 方法可以,但建议使用 setState();

为了完成您所描述的内容,请尝试 this.forceUpdate()。

只是另一个支持已接受答案的回复:-)

React 不鼓励使用 forceUpdate(),因为它们通常采用非常 "this is the only way of doing it" 的函数式编程方法。这在很多情况下都很好,但是许多 React 开发人员都有 OO 背景,并且通过这种方法,听一个可观察对象是完全可以的。

如果这样做,您可能知道必须在可观察 "fires" 时重新渲染,因此,您应该使用 forceUpdate(),这实际上是 shouldComponentUpdate() 的优点这里不涉及。

像 MobX 这样采用 OO 方法的工具实际上是在表面下进行的(实际上 MobX 直接调用 render()

有几种方法可以重新渲染您的组件:

最简单的解决方案是使用 forceUpdate() 方法:

this.forceUpdate()

另一种解决方案是在状态中创建未使用的密钥(nonUsedKey) 并通过更新此 nonUsedKey 来调用 setState 函数:

this.setState({ nonUsedKey: Date.now() } );

或重写所有当前状态:

this.setState(this.state);

道具改变也提供组件重新渲染。

您可以使用 forceUpdate() 进行更多详细信息检查 (forceUpdate())。

forceUpdate(),但每次我听到有人谈论它时,都会跟进说你不应该使用它。

为了完整起见,您也可以在功能组件中实现:

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

或者,作为可重复使用的挂钩:

const useForceUpdate = () => {
  const [, updateState] = useState();
  return useCallback(() => updateState({}), []);
}
// const forceUpdate = useForceUpdate();

参见:

请注意,使用强制更新机制仍然是一种不好的做法,因为它违背了反应心态,因此仍应尽可能避免。

在 2021 年和 2022 年,这是 the official way 强制更新 React 功能组件。

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

  function handleClick() {
    forceUpdate();
  }

我知道 OP 是针对 class 组件的。但是这个问题是在 2015 年提出的,现在 hooks 可用,许多人可能会在功能组件中搜索 forceUpdate。这一点是给他们的。

编辑 2022 年 4 月 18 日

强制更新组件通常是不好的做法。

一些可能导致需要使用强制更新的事情。

  • 在必须使用的地方不使用状态变量 - 本地、redux、上下文。
  • 您尝试访问并期望 update/change 的状态对象的字段在对象或数组中嵌套得太深。甚至 Redux 建议维护平面对象或数组。如果一个复杂对象中只有一个字段值发生变化,React 可能无法识别状态对象发生了变化,因此它不会更新组件。保持状态平坦简单。
  • 您的列表项上的键,如另一个答案中所述。事实上,这也可能导致其他意外行为。我看到列表中的项目被重复呈现(重复),因为键不相同或键完全丢失。始终要求后端团队尽可能发送唯一的 ID!避免对键使用数组索引。不要尝试在前端使用 nanoid、uuid 或 random 创建唯一的 id。因为每次组件更新时使用上述方法创建的 id 都会更改(提供给列表的键需要是静态的并且在每次渲染时都相同)。创建唯一 ID 通常是后端问题。尽量不要将该需求带到前端。前端的职责只是绘制后端 return 的数据,而不是即时创建数据。
  • 如果您的 useEffect、useCallback 依赖数组没有设置正确的值。使用 ESLint 来帮助您解决这个问题!此外,这是 React 中内存泄漏的最大原因之一。在 return 回调中清理您的状态和事件侦听器以避免内存泄漏。因为这种内存泄漏非常难以调试。
  • 始终关注控制台。它是您工作中最好的朋友。解决控制台中显示的警告和错误可以解决很多令人讨厌的事情 - 您甚至没有意识到的错误和问题。

我记得有几件事我做错了。如果有帮助..

使用 useEffect 作为 componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合,如 React 文档中所述。

要表现得像 componentDidMount,您需要像这样设置 useEffect:

  useEffect(() => console.log('mounted'), []);

第一个参数是将基于第二个参数触发的回调,第二个参数是一个值数组。如果第二个参数中的任何值发生变化,您在 useEffect 中定义的回调函数将被触发。

然而,在我展示的示例中,我传递了一个空数组作为我的第二个参数,并且永远不会更改,因此回调函数将在组件安装时调用一次。

那种总结useEffect。如果您有一个参数而不是空值,例如:

 useEffect(() => {

  }, [props.lang]);

这意味着每次props.lang改变时,你的回调函数都会被调用。 useEffect 不会真正重新渲染您的组件,除非您在该回调函数中管理可能触发重新渲染的某些状态。

如果你想触发重新渲染,你的渲染函数需要有一个你正在更新的状态 useEffect

例如,在这里,渲染函数首先显示英语作为默认语言,在我的使用效果中,我在 3 秒后更改了该语言,因此渲染被重新渲染并开始显示“西班牙语”。

function App() {
  const [lang, setLang] = useState("english");

  useEffect(() => {
    setTimeout(() => {
      setLang("spanish");
    }, 3000);
  }, []);

  return (
    <div className="App">
      <h1>Lang:</h1>
      <p>{lang}</p>
    </div>
  );
}

bruh 如果我不关心持久化本地状态或任何数据,我只是 location.reload(),这会重新加载页面,显然 re-renders 组件