繁体   English   中英

React 的 useEffect 和 DOM 事件处理程序之间的执行顺序

[英]Execution order between React's useEffect and DOM event handler

我在下面编写了一个自定义上下文提供程序,它将应用程序设置保存为Context ,并将其保存在localStorage中(感谢 Alex Krush 的帖子)。

我添加了initialized标志以避免在安装组件后立即保存从localStorage获取的值( useEffect将在componentDidMount的时间运行并尝试将获取的值写入存储)。

import React, { useCallback, useEffect, useReducer, useRef } from 'react';

const storageKey = 'name';
const defaultValue = 'John Doe';

const initializer = (initialValue) => localStorage.getItem(storageKey) || initialValue;
const reducer = (value, newValue) => newValue;

const CachedContext = React.createContext();

const CachedContextProvider = (props) => {
  const [value, setValue] = useReducer(reducer, defaultValue, initializer);
  const initialized = useRef(false);

  // save the updated value as a side-effect
  useEffect(() => {
    if (initialized.current) {
      localStorage.setItem(storageKey, value);
    } else {
      initialized.current = true; // skip saving for the first time
    }
  }, [value]);

  return (
    <CachedContext.Provider value={[value, setValue]}>
      {props.children}
    </CachedContext.Provider>
  );
};

用法:

const App = (props) => {
  return <CachedContextProvider><Name name='Jane Doe' /></CachedContextProvider>;
}

const Name = (props) => {
  const [name, setName] = useContext(CachedContext);

  useEffect(() => {
    setName(props.name);
  }, [props.name]);
}

然后,我想让我的自定义上下文检测另一个 window 对目标存储所做的更改。 我将handleStorageEvent添加到CachedContextProvider以监听存储事件:

  // re-initialize when the storage has been modified by another window
  const handleStorageEvent = useCallback((e) => {
    if (e.key === storageKey) {
      initialized.current = false; // <-- is it safe???
      setValue(initializer(defaultValue));
    }
  }, []);

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.addEventListener('storage', handleStorageEvent);
      return () => {
        window.removeEventListener('storage', handleStorageEvent);
      };    
    }
  }, []);

我担心的是我是否可以安全地将initialized重置为false以避免写回获取的值。 我担心多进程设置中的以下情况:

  1. Window 1 运行setValue('Harry Potter')
  2. Window 2 次运行setValue('Harry Potter')
  3. Window 2 运行localStorage.setItem以响应更新value
  4. handleStorageEvent 1 中的 handleStorageEvent 检测到存储的变化并重新初始化其initializedvalue false'Harry Potter'
  5. Window 1 尝试运行localStorage.setItem ,但它什么也没做,因为 Window 2 的value已经设置为'Harry Potter' ,React 可能会判断没有任何变化。 因此, initialized将保持为false
  6. Window 1 运行setValue('Ron Weasley') 它更新value不保存它,因为已initialized === false 它有机会失去应用程序设置的值

我认为这与 React 的useEffect和 DOM 事件处理程序之间的执行顺序有关。 有人知道怎么做吗?

我可能会添加某种测试来查看在每种可能的情况下会发生什么。

但是,这是我的理论:在第 5 步中,window 1 不会尝试运行 localStorage.setItem(如您所说),因为 initialized 刚刚设置为 false。 相反,它会将 initialized 设置为 true。 因此,第 6 步应该按预期工作,这应该不是问题。

我和我的同事讨论了这个问题,最后找到了解决方案。 他指出新的 React Fiber 引擎不应该保证副作用的执行顺序,并建议在 state 中添加修订号

这是一个例子。 即使设置value未更改,递增的revision也将始终调用useEffect 订阅者从Provider获取state.value ,无需关心底层revision

import React, { useCallback, useEffect, useReducer, useRef } from 'react';

const storageKey = 'name';
const defaultValue = 'John Doe';

const orDefault(value) = (value) =>
  (typeof value !== 'undefined' && value !== null) ? value : defaultValue;

const initializer = (arg) => ({
  value: orDefault(localStorage.getItem(storageKey)),
  revision: 0,
});

const reducer = (state, newValue) => ({
  value: newValue,
  revision: state.revision + 1,
});

const useCachedValue = () => {
  const [state, setState] = useReducer(reducer, null, initializer);
  const initialized = useRef(false);

  // save the updated value as a side-effect
  useEffect(() => {
    if (initialized.current) {
      localStorage.setItem(storageKey, state.value);
    } else {
      // skip saving just after the (re-)initialization
      initialized.current = true;
    }
  }, [state]);

  // re-initialize when the storage has been modified by another window
  const handleStorageEvent = useCallback((e) => {
    if (e.key === null || e.key === storageKey) {
      initialized.current = false;
      setState(orDefault(e.newValue));
    }
  }, []);

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.addEventListener('storage', handleStorageEvent);
      return () => {
        window.removeEventListener('storage', handleStorageEvent);
      };
    }
  }, []);

  return [state.value, setState];
};

const Context = React.createContext();

const Provider = (props) => {
  const cachedValue = useCachedValue();
  return (
    <Context.Provider value={cachedValue}>
      {props.children}
    </Context.Provider>
  );
};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM