React 的 useEffect 和 DOM 事件处理程序之间的执行顺序
Execution order between React's useEffect and DOM event handler
我在下面编写了一个自定义上下文提供程序,它将应用程序设置保存为 Context
并将其保存在 localStorage
中(感谢 Alex Krush 的 post)。
我添加了 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
以避免写回获取的值。我担心多进程设置中的以下情况:
- Window 1 运行s
setValue('Harry Potter')
- Window2运行秒
setValue('Harry Potter')
- Window 2 运行s
localStorage.setItem
响应 value
上的更新
handleStorageEvent
in Window 1 检测到存储的变化并重新初始化它的 initialized
和 value
为 false
和 'Harry Potter'
- Window 1 尝试 运行
localStorage.setItem
,但它什么也没做,因为 value
已经被 Window 设置为 'Harry Potter'
2而 React 可能会判断没有变化。结果,initialized
将保留为 false
- Window 1 运行s
setValue('Ron Weasley')
。它更新 value
但 不保存它 因为 initialized === false
。有几率丢失应用设置的值
我认为这与React的useEffect
和DOM事件处理程序之间的执行顺序有关。有谁知道正确的做法吗?
我可能会添加某种测试以查看在每种可能的情况下会发生什么。
但是,这是我的理论:在第 5 步中,window 1 不会尝试 运行 localStorage.setItem(如您所说),因为 initialized 只是设置为 false。它会将初始化设置为 true。因此,第 6 步应该按预期工作,这应该不是问题。
我和同事讨论了这个问题,终于找到了解决办法。他指出新的React Fiber引擎不应该保证side-effects的执行顺序,建议在state中增加一个revision number
这是一个例子。即使集合 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>
);
};
我在下面编写了一个自定义上下文提供程序,它将应用程序设置保存为 Context
并将其保存在 localStorage
中(感谢 Alex Krush 的 post)。
我添加了 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
以避免写回获取的值。我担心多进程设置中的以下情况:
- Window 1 运行s
setValue('Harry Potter')
- Window2运行秒
setValue('Harry Potter')
- Window 2 运行s
localStorage.setItem
响应value
上的更新
handleStorageEvent
in Window 1 检测到存储的变化并重新初始化它的initialized
和value
为false
和'Harry Potter'
- Window 1 尝试 运行
localStorage.setItem
,但它什么也没做,因为value
已经被 Window 设置为'Harry Potter'
2而 React 可能会判断没有变化。结果,initialized
将保留为false
- Window 1 运行s
setValue('Ron Weasley')
。它更新value
但 不保存它 因为initialized === false
。有几率丢失应用设置的值
我认为这与React的useEffect
和DOM事件处理程序之间的执行顺序有关。有谁知道正确的做法吗?
我可能会添加某种测试以查看在每种可能的情况下会发生什么。
但是,这是我的理论:在第 5 步中,window 1 不会尝试 运行 localStorage.setItem(如您所说),因为 initialized 只是设置为 false。它会将初始化设置为 true。因此,第 6 步应该按预期工作,这应该不是问题。
我和同事讨论了这个问题,终于找到了解决办法。他指出新的React Fiber引擎不应该保证side-effects的执行顺序,建议在state中增加一个revision number
这是一个例子。即使集合 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>
);
};