功能组件中useEffect()中的useState()

时间:2019-09-11 09:45:44

标签: javascript reactjs

我试图将基于类的组件更改为功能组件,并在尝试读取附加于useEffect()的处理函数中的状态属性时遇到麻烦。

在基于类的组件内部,我将在componentDidMount内附加处理程序方法,并且该处理程序可以访问状态。

渲染的输出在两种类型的组件中均显示正确的值!

但是当我需要在处理程序中使用state属性的当前值来计算新的状态值时,我会在读取状态属性时遇到麻烦 内部功能组件。

我创建了以下示例来演示该问题:https://codesandbox.io/s/peaceful-wozniak-l6w2c(打开控制台并滚动)

如果您单击两个组件都可以正常工作:

  • 控制台输出状态属性counter的当前值
  • 增加状态属性counter

如果仅滚动基于类的组件,则可以:

  • 控制台输出状态属性position的当前值
  • 更新状态属性position

但是功能组件的控制台输出仅返回我 初始状态属性值。

基于类的组件

import React, { Component } from "react";

export default class CCTest extends Component {
  constructor(props) {
    super(props);
    this.state = { position: 0, counter: 0 };
    this.handleScroll = this.handleScroll.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }

  handleScroll(e) {
    console.log(
      "class.handleScroll()",
      this.state.position
    );
    this.setState({ position: document.body.getBoundingClientRect().top });
  }

  handleClick() {
    console.log("[class.handleClick]", this.state.counter);
    this.setState(prevState => ({ counter: prevState.counter + 1 }));
  }

  componentDidMount() {
    window.addEventListener("scroll", this.handleScroll);
  }

  render() {
    return (
      <div
        style={{
          backgroundColor: "orange",
          padding: "20px",
          cursor: "pointer"
        }}
        onClick={this.handleClick}
      >
        <strong>class</strong>
        <p>
          position: {this.state.position}, counter: {this.state.counter}
        </p>
      </div>
    );
  }
}

功能组件

import React from "react";

export default prop => {
  const [position, setPosition] = React.useState(0);
  const [counter, setCounter] = React.useState(0);

  React.useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
    // eslint-disable-next-line
  }, []);

  const handleScroll = () => {
    console.log(
      "function.handleScroll()",
      position
    );
    setPosition(document.body.getBoundingClientRect().top);
  };

  const handleClick = () => {
    console.log("[function.handleClick]", counter);
    setCounter(counter + 1);
  };

  return (
    <div
      style={{ backgroundColor: "green", padding: "20px", cursor: "pointer" }}
      onClick={handleClick}
    >
      <strong>function</strong>
      <p>
        position: {position}, counter: {counter}
      </p>
    </div>
  );
};

每个小提示都会帮助<3

2 个答案:

答案 0 :(得分:4)

每次渲染组件时,都会创建一个新的handleScroll函数,该函数可以通过组件闭包访问当前状态。但是这种关闭不会在每个渲染器上都得到更新。而是创建了一个新的handleScroll函数,该函数可以查看新值。

问题是,当您这样做时:

window.addEventListener("scroll", handleScroll);

您将函数的 current 版本绑定到事件,并且该事件将始终在那时读取值,而不是新值。通常,useEffect将再次运行,并且将使用该函数的新实例,但是您要使用空数组作为第二个参数来防止它发生。

仅在这些情况下会有棉绒警告,但您已将其禁用:

// eslint-disable-next-line

如果删除此空数组,则可以删除禁用linter规则,它将正常运行。

其他选项

如果每次都不能更改功能(例如,使用非反应库或出于性能原因),则有其他选择:

  • 您可以使用ref代替状态。引用会发生突变,而不是在每个渲染上都创建一个新引用。这样,即使您不更新功能,该功能的先前版本也可以读取并修改当前值。

  • 您可以使用useReducer。调度功能永远不会改变,因此,如果您的useEffect依赖它,它将不会重新运行。 reducer可以访问先前的值和操作,因此即使依赖于先前的值,它也可以计算新状态。

答案 1 :(得分:0)

由于@Anxo的帮助,我最终放弃了useState并改而使用useRecuder

这是我针对最初问题中特定示例的功能组件

import React from 'react';

const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_POSITION':
      return {
        ...state,
        position: document.body.getBoundingClientRect().top
      };
    case 'INCREMENT_COUNTER':
      return {
        ...state,
        counter: state.counter + 1
      };
    default:
      throw new Error('chalupa batman');
  }
};

const initialState = {
  position: 0,
  counter: 0
};

const FCTest = props => {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  React.useEffect(() => {
    window.addEventListener('scroll', () => {
      dispatch({ type: 'UPDATE_POSITION' });
    });
    return () => {
      window.removeEventListener(
        'scroll',
        dispatch({ type: 'UPDATE_POSITION' })
      );
    };
    // eslint-disable-next-line
  }, []);

  return (
    <div
      style={{ backgroundColor: 'green', padding: '20px', cursor: 'pointer' }}
      onClick={() => dispatch({ type: 'INCREMENT_COUNTER' })}
    >
      <strong>function</strong>
      <p>
        position: {state.position}, counter: {state.counter}
      </p>
    </div>
  );
};

export default FCTest;