Как правильно работать с данными из React Context в useEffect / useCallback-hook - PullRequest
0 голосов
/ 18 января 2020

Я использую контекст React для хранения данных и обеспечения функциональности для изменения этих данных.

Теперь я пытаюсь преобразовать компонент класса в функциональный компонент, используя React Hooks.

Хотя в классе все работает как положено, я не могу заставить его работать в функциональном компоненте.

Поскольку код моих приложений немного сложнее, я создал этот небольшой пример ( JSFiddle link ), которая позволяет воспроизвести проблему:

Сначала контекст, который одинаков как для класса, так и для функционального компонента:

const MyContext = React.createContext();

class MyContextProvider extends React.Component {
    constructor (props) {
        super(props);

        this.increase = this.increase.bind(this);
        this.reset = this.reset.bind(this);

        this.state = {
            current: 0,
            increase: this.increase,
            reset: this.reset
        }
    }

    render () {
        return (
            <MyContext.Provider value={this.state}>
                {this.props.children}
            </MyContext.Provider>
        );
    }

    increase (step) {
        this.setState((prevState) => ({
            current: prevState.current + step
        }));
    }

    reset () {
        this.setState({
            current: 0
        });
    }
}

Теперь, вот компонент Class, который работает просто отлично:

class MyComponent extends React.Component {
    constructor (props) {
        super(props);

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

    componentDidMount () {
        setInterval(this.increaseByOne, 1000);
    }

    render () {
        const count = this.context;

        return (
            <div>{count.current}</div>
        );
    }

    increaseByOne () {
        const count = this.context;

        if (count.current === 5) {
            count.reset();
        }
        else {
            count.increase(1);
        }
    }
}
MyComponent.contextType = MyContext;

Ожидаемый результат - он считается до 5 с интервалом в одну секунду, а затем снова начинается с 0.

А вот преобразованный функциональный компонент:

const MyComponent = (props) => {
    const count = React.useContext(MyContext);

    const increaseByOne = React.useCallback(() => {
        console.log(count.current);

        if (count.current === 5) {
            count.reset();
        }
        else {
            count.increase(1);
        }
    }, []);

    React.useEffect(() => {
        setInterval(increaseByOne, 1000);
    }, [increaseByOne]);

    return (
        <div>{count.current}</div>
    );
}

Вместо сброса счетчика на 5 он возобновляет счет.

Проблема в том, что count.current в строке if (count.current === 5) { всегда 0, так как я t не использует последнее значение.

Единственный способ заставить это работать - настроить код следующим образом:

const MyComponent = (props) => {
    const count = React.useContext(MyContext);

    const increaseByOne = React.useCallback(() => {
        console.log(count.current);

        if (count.current === 5) {
            count.reset();
        }
        else {
            count.increase(1);
        }
    }, [count]);

    React.useEffect(() => {
        console.log('useEffect');

        const interval = setInterval(increaseByOne, 1000);

        return () => {
            clearInterval(interval);
        };
    }, [increaseByOne]);

    return (
        <div>{count.current}</div>
    );
}

Теперь обратный вызов increaseByOne воссоздается при каждом изменении контекста, что также означает, что эффект вызывается каждую секунду.
В результате он очищает интервал и устанавливает новый при каждом изменении контекста (вы можете увидеть это в консоль браузера).
Это может работать в этом небольшом примере, но оно изменило исходную логику c и имеет намного больше накладных расходов.

Мое приложение не использует интервал, но оно слушает для события. Удаление прослушивателя событий и добавление его позже, означало бы, что я могу потерять некоторые события, если они будут запущены между удалением и привязкой слушателя, что выполняется асинхронно с помощью React.

Есть кто-то Идея, как это должно реагировать, чтобы решить эту проблему без изменения общего лога c?

Я создал скрипку, чтобы поиграться с кодом выше:
https://jsfiddle.net/Jens_Duttke/78y15o9p/

Ответы [ 2 ]

1 голос
/ 18 января 2020

Первое решение состоит в том, чтобы поместить данные, изменяющиеся во времени, в useRef, чтобы они были доступны по ссылке, а не по закрытию (равно как вы получаете доступ к фактическому this.state в версии на основе классов)

const MyComponent = (props) => {
  const countByRef = React.useRef(0);
    countByRef.current = React.useContext(MyContext);

    React.useEffect(() => {
        setInterval(() => {
          const count = countByRef.current;

          console.log(count.current);

          if (count.current === 5) {
                count.reset();
          } else {
            count.increase(1);
          }
      }, 1000);
    }, []);

    return (
        <div>{countByRef.current.current}</div>
    );
}

Другое решение состоит в том, чтобы изменить reset и increase, чтобы разрешить функциональный аргумент, а также это возможно с помощью setState и useState.

Тогда это будет

useEffect(() => {
  setInterval(() => {
    count.increase(current => current === 5? 0: current + 1);
  }, 1000);
}, [])

PS также надеемся, что вы не пропустили функцию очистки в своем реальном коде:

useEffect(() => {
 const timerId = setInterval(..., 1000);
 return () => {clearInterval(timerId);};
}, [])

в противном случае у вас будет утечка памяти

1 голос
/ 18 января 2020

Если функции increaseByOne не нужно знать фактическое значение count.current, вы можете избежать его воссоздания. В контексте создайте новую функцию с именем is, которая проверяет, равно ли current значение:

is = n => this.state.current === n;

И используйте эту функцию в функции increaseByOne:

if (count.is(5)) {
    count.reset();
}

Пример:

const MyContext = React.createContext();

class MyContextProvider extends React.Component {
  render() {
    return (
      <MyContext.Provider value={this.state}>
        {this.props.children}
      </MyContext.Provider>
    );
  }

  increase = (step) => {
    this.setState((prevState) => ({
      current: prevState.current + step
    }));
  }

  reset = () => {
    this.setState({
      current: 0
    });
  }

  is = n => this.state.current === n;

  state = {
    current: 0,
    increase: this.increase,
    reset: this.reset,
    is: this.is
  };
}

const MyComponent = (props) => {
  const { increase, reset, is, current } = React.useContext(MyContext);

  const increaseByOne = React.useCallback(() => {
    if (is(5)) {
      reset();
    } else {
      increase(1);
    }
  }, [increase, reset, is]);

  React.useEffect(() => {
    setInterval(increaseByOne, 1000);
  }, [increaseByOne]);

  return (
    <div>{current}</div>
  );
}

const App = () => (
  <MyContextProvider>
    <MyComponent />
  </MyContextProvider>
);

ReactDOM.render( <
  App / > ,
  document.querySelector("#app")
);
body {
  background: #fff;
  padding: 20px;
  font-family: Helvetica;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="app"></div>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...