需要更清楚的说明如何避免使用React钩子进行无限重新渲染

时间:2020-04-29 22:36:06

标签: reactjs react-hooks

对React钩子不太熟练,以前使用过很多类组件,希望您能原谅。

当前代码会导致无限次重新渲染,我想我理解为什么-整个函数体都在重新渲染时被调用。

const NavTabs = () => {
  const classes = useStyles();

  const [categories, setCategories] = React.useState();
  const axiosPromise = getRequest(consts.categoriesURL);

  axiosPromise.then(data => {
    setCategories(data.value);
  })

  return (
    <div className={classes.root}>
      <AppBar position="static">
      </AppBar>
      {categories && <DynamicTabs categories={categories}/>}
    </div>
  );
}

我想我可以做类似if (!categories) { const axiosPromise [...]之类的事情,即只有在尚未填充类别时才执行http请求。我猜这也可以通过useEffect解决吗?还是将挂钩包装在内部函数中?

我猜我真正的问题是-为什么React会重新渲染整个函数体?它不应该只渲染return函数吗?那么使用将在每个渲染器上重新运行的钩子有什么意义呢?

与类组件相比-函数主体中的代码是否不应该与类组件中的构造函数代码相同,而返回函数是否​​应与render方法等效?

3 个答案:

答案 0 :(得分:2)

我想我可以执行if(!categories){const axiosPromise [...]等操作,即仅在尚未填充类别时才执行http请求。我想这也可以通过useEffect解决吗?还是将挂钩包装在内部函数中?

是的,useEffect是前往此处的方法。发出请求并将结果设置为state是副作用,在这种情况下,只应运行一次。我们可以使用useEffect轻松实现。

我猜我真正的问题是-为什么React会重新渲染整个函数体?它不应该只渲染return函数吗?那么使用将在每个渲染器上重新运行的钩子有什么意义呢?

React无法拆分js函数,而只能重新呈现返回值。该函数是原子函数,必须完成。这就是钩子的作用。运行钩子时使用React控件,以便它可以执行有趣的操作,例如批处理状态更新,忽略过时的效果并优先处理诸如动画之类的高优先级工作。

与类组件相比-函数主体中的代码是否不应该与类组件中的构造函数代码相同,而返回函数是否​​应与render方法等效?

功能组件等效于类组件的render方法。它们的调用方式类似。所有其他生命周期方法都由钩子代替。

我建议react docs是一个很好的起点,而Dan Abramov拥有great deep dive on hooks

答案 1 :(得分:1)

是的,getRequest在每个渲染周期都被调用,这会设置一些状态并触发重新渲染。最好将它放在带有依赖项数组的效果挂钩中。您定义的依赖项将决定何时可以调用getRequest

为什么React会重新渲染整个函数体?

需要运行整个函数主体才能确定返回值。

然后使用将在每个渲染器上重新运行的钩子有什么意义?

挂钩在每个渲染器上运行,其定义顺序相同,但是取决于依赖项可能不会调用回调。钩子使功能组件具有如此高的生存能力和组件生命周期的感觉,几乎等同于功能上基于类的组件。在大多数情况下,您可以将基于类的组件完全转换为功能组件,而不会删除任何功能。

与类组件相比-函数主体中的代码是否不应该与类组件中的构造函数代码相同,而返回函数是否​​应与render方法等效?

将整个功能组件定义 认为是基于类的render函数,可以包含一些逻辑并返回计算出的JSX以呈现到DOM。

示例解决方案:

const NavTabs = () => {
  const classes = useStyles();

  const [categories, setCategories] = React.useState(); // <-- no initial state!

  useEffect(() => {
    getRequest(consts.categoriesURL).then(data => {
      setCategories(data.value); // <-- will update state and trigger render
    });
  }, []); // <-- empty dependency is run once on component mount

  return (
    <div className={classes.root}>
      <AppBar position="static">
      </AppBar>
      {categories && <DynamicTabs categories={categories}/>}
    </div>
  );
}

答案 2 :(得分:1)

要回答“为什么反应正在运行整个功能”,答案是javascript函数是这样工作的:您总是必须运行整个程序,它们不会停在中间*。如果您习惯对组件进行分类,那么我了解您在这里的想法:我没有构造器部分和渲染部分吗?答案是:如果您正在使用功能组件,那不是真的。您只有渲染。但是钩子是魔术,它们让您假装有两个部分。

挂钩知道何时调用它们,并且假设您始终以相同的顺序调用它们,则可以跟踪render函数外部的状态。所以工作的方式有点像这样:

  1. React检测功能组件,并为该组件创建或重新使用现有的渲染上下文。这是挂钩信息所在的地方。
  2. React调用您的功能组件,它开始运行。
  3. 您可以在函数组件中调用钩子。这些检查当前渲染上下文是什么,并从该上下文保存/获取相关信息。从某种意义上说,渲染上下文是一个“全局”变量。
  4. 您可以在函数中执行任何其他操作,并最终返回组件树(JSX)或null。
  5. 然后反应(最终)更新DOM以匹配您返回的内容,并将更改保存到渲染上下文中,以便下次调用render时,它可以重新使用上下文。

魔术是渲染上下文可以用钩子做一些奇特的事情,例如只运行一次,总是从钩子或任何其他数目的事物返回相同的值。但是从某种意义上说,组件“类”成为了内部反应的渲染上下文,钩子知道如何访问。

这是在类组件中实现的useState钩子的示例:(您永远不需要这样做,但这是钩子如何工作的示例)。

class FakeHook extends React.Component {
  constructor(...args) {
    super(...args)
    this.state = {}
    this.useStateCalls = 0
  }
  
  useState(defaultValue){
    const currentRenderContext = this.state
    let value = defaultValue
    const currentStateKey = `useState${this.useStateCalls}`
    if (currentStateKey in currentRenderContext) value = currentRenderContext[currentStateKey]
    this.useStateCalls++
    
    return[value, (newValue) => this.setState({[currentStateKey]: newValue})]
  }
  
  render(){
    this.useStateCalls = 0
    let [fooState, setFoo] = this.useState("foo default")
    let [barState, setBar] = this.useState("bar default")
    
    return(
      <dl>
        <dt>Foo state</dt>
        <dd>
          <strong>Value:</strong>
          <div>{fooState}</div>
          <button onClick={(event) => {event.preventDefault(); setFoo(`foo updated at ${new Date().toLocaleString()}`)}}>Update Foo</button>
        </dd>
        <dt>Bar state</dt>
        <dd>
          <strong>Value:</strong>
          <div>{barState}</div>
          <button onClick={(event) => {event.preventDefault(); setBar(`bar updated at ${new Date().toLocaleString()}`)}}>Update Bar</button>
        </dd>
        <dt>Render context state:</dt>
        <dd><pre>{JSON.stringify(this.state)}</pre></dd>
      </dl>
    )
  }
  
}

ReactDOM.render(<FakeHook/>, document.getElementById('main'))
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main id=main>loading or error occurred...</main>

请注意,状态是根据在render内调用钩子的顺序存储的。在实际的挂钩中,渲染上下文存储在this.state之外的其他位置,但是挂钩知道如何获取它,因此您并不在乎。而且,这只是一个例子,实际的钩子工作原理略有不同,但是概念是相同的。

*:async函数和生成器不会一次全部运行,而是返回一个特殊对象,该对象使函数分多个步骤运行,在await或{{1}上等待或暂停}。

相关问题