英文:
How to save to context in custom hook on unmount in React?
问题
我有以下简化的自定义钩子:
function useSpecialState(defaultValue, key) {
const { stateStore, setStateStore } = useContext(StateStoreContext);
const [state, setState] = useState(
stateStore[key] !== undefined ? stateStore[key] : defaultValue
);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
useEffect(() => {
return () => {
setStateStore((prevStateStore) => ({
...prevStateStore,
[key]: stateRef.current,
}));
};
}, []);
return [state, setState];
}
目标是在卸载时保存到上下文,但是这段代码不起作用。将 state
放在负责保存到上下文的 useEffect
的依赖数组中不是一个好的解决方案,因为那样会在每次状态更改时保存,这是不必要的。
上下文:
const StateStoreContext = createContext({
stateStore: {},
setStateStore: () => {},
});
父组件:
function StateStoreComponent(props) {
const [stateStore, setStateStore] = useState({});
return (
<StateStoreContext.Provider value={{ stateStore, setStateStore }}>
{props.children}
</StateStoreContext.Provider>
);
}
英文:
I have the following simplified custom hook:
function useSpecialState(defaultValue, key) {
const { stateStore, setStateStore } = useContext(StateStoreContext);
const [state, setState] = useState(
stateStore[key] !== undefined ? stateStore[key] : defaultValue
);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
useEffect(() => {
return () => {
setStateStore((prevStateStore) => ({
...prevStateStore,
[key]: stateRef.current,
}));
};
}, []);
return [state, setState];
}
The goal would be to save to a context on unmount, however, this code does not work. Putting state
in the dependency array of the useEffect
which is responsible for saving to context would not be a good solution, because then it would be saved on every state change, which is grossly unnecessary.
The context:
const StateStoreContext = createContext({
stateStore: {},
setStateStore: () => {},
});
The parent component:
function StateStoreComponent(props) {
const [stateStore, setStateStore] = useState({});
return (
<StateStoreContext.Provider value={{ stateStore, setStateStore }}>
{props. Children}
</StateStoreContext.Provider>
);
}
答案1
得分: 4
TL;DR
你的代码是正确的,技术上是正确的,观察到的行为是由于在非生产环境中React.StrictMode
组件双重挂载组件导致的。换句话说,你部署的正常生产环境中,代码和逻辑应该按照你的期望工作。这是所有预期的行为。
解释
你的代码是正确的,技术上是正确的。之所以看起来不起作用是因为你在React的StrictMode
组件中渲染应用程序,它在非生产环境中执行额外的行为。具体来说,在这种情况下,它是为了检查Ensuring Reusable State或Fixing bugs found by re-running Effects而双重挂载组件。
严格模式也可以帮助发现Effects中的错误。
每个Effect都有一些设置代码和可能有一些清理代码。
通常情况下,React在组件挂载(添加到屏幕上)时调用设置,并在组件卸载(从屏幕上移除)时调用清理。然后,如果它的依赖关系在上一次渲染之后发生了变化,React会再次调用清理和设置。当启用Strict Mode时,在开发中,React还会为每个Effect运行一个额外的设置+清理周期。这可能会感到意外,但它有助于发现手动难以捕捉的细微错误。
在React.StrictMode
组件内渲染的任何组件并使用自定义的useSpecialState
钩子将被挂载、卸载并运行第二个useEffect
钩子的清理函数,这将更新上下文中的状态,然后再次挂载组件。
以下是一个小示例,切换使用useSpecialState
钩子的相同组件的挂载,其中只有一个组件在React.StrictMode
组件中挂载。请注意,"Component A"在每次挂载和卸载时都会更新上下文状态,而"Component B"仅在卸载时更新上下文状态。
步骤:
- 应用程序挂载,上下文渲染为0
- 切换A挂载:观察挂载/卸载/挂载,状态更新为A,上下文渲染为1
- 切换B挂载:观察挂载,无状态更新
- 切换A卸载:观察卸载,状态更新为A,上下文渲染为2
- 切换B卸载:观察卸载,状态更新为B,上下文渲染为3
沙盒代码:
const MyComponent = ({ label }) => {
const [count, setCount] = useSpecialState(0, "count" + label);
return (
<>
<h1>Component{label}</h1>
<div>Count: {count}</div>
<button type="button" onClick={() => setCount((c) => c + 1)}>
+
</button>
</>
);
};
export default function App() {
const [mountA, setMountA] = useState(false);
const [mountB, setMountB] = useState(false);
return (
<StateStoreComponent>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>
<button type="button" onClick={() => setMountA((mount) => !mount)}>
{mountA ? "Unmount" : "Mount"} A
</button>
<button type="button" onClick={() => setMountB((mount) => !mount)}>
{mountB ? "Unmount" : "Mount"} B
</button>
</div>
<StrictMode>{mountA && <MyComponent label="A" />}</StrictMode>
{mountB && <MyComponent label="B" />}
</div>
</StateStoreComponent>
);
}
英文:
TL;DR
The code you have is fine and technically correct, the observed behavior is caused by the React.StrictMode
component double-mounting components in non-production builds. In other words, the code & logic should behave as you expect in normal production builds you deploy. This is, or should be, all expected behavior.
Explanation
The code you have is fine and technically correct. The reason it appears that it is not working is because you are rendering the app within the React StrictMode
component which executes additional behavior in non-production builds. Specifically in this case it's the double-mounting of components as part of React's check for Ensuring Reusable State or Fixing bugs found by re-running Effects if you prefer the current docs.
> Strict Mode can also help find bugs in Effects.
>
> Every Effect has some setup code and may have some cleanup code.
> Normally, React calls setup when the component mounts (is added to the
> screen) and calls cleanup when the component unmounts (is removed from
> the screen). React then calls cleanup and setup again if its
> dependencies changed since the last render.
>
> When Strict Mode is on, React will also run one extra setup+cleanup
> cycle in development for every Effect. This may feel surprising, but
> it helps reveal subtle bugs that are hard to catch manually.
Any component rendered within a React.StrictMode
component and using the custom useSpecialState
hook will be mounted, unmounted and run the second useEffect
hook's cleanup function which will update the state in the context, and then mount again the component.
Here's a small demo toggling the mounting of identical components that use the useSpecialState
hook, where only one of them is mounted within a React.StrictMode
component. Notice that "Component A" updates the context state each time when it is mounted and unmounted, while "Component B" updates the context state only when it unmounts.
Steps:
- App mounts, context render 0
- Toggle A mounted: observe mount/unmount/mount, state update A, context render 1
- Toggle B mounted: observe mount, no state update
- Toggle A unmounted: observe unmount, state update A, context render 2
- Toggle B unmounted: observe unmount, state update B, context render 3
Sandbox Code:
const MyComponent = ({ label }) => {
const [count, setCount] = useSpecialState(0, "count" + label);
return (
<>
<h1>Component{label}</h1>
<div>Count: {count}</div>
<button type="button" onClick={() => setCount((c) => c + 1)}>
+
</button>
</>
);
};
export default function App() {
const [mountA, setMountA] = useState(false);
const [mountB, setMountB] = useState(false);
return (
<StateStoreComponent>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>
<button type="button" onClick={() => setMountA((mount) => !mount)}>
{mountA ? "Unmount" : "Mount"} A
</button>
<button type="button" onClick={() => setMountB((mount) => !mount)}>
{mountB ? "Unmount" : "Mount"} B
</button>
</div>
<StrictMode>{mountA && <MyComponent label="A" />}</StrictMode>
{mountB && <MyComponent label="B" />}
</div>
</StateStoreComponent>
);
}
答案2
得分: 1
我尝试了您的useSpecialState
钩子,它似乎按预期正常工作。
从我的理解,您需要在组件卸载时保存上一个状态值到上下文中。
我创建了一个子组件来increment
计数器,另一个toggle
按钮用于挂载和卸载CounterComponent
,同时保持上一次卸载时的计数器值。
以下是我尝试的方式。可工作的CodeSandbox链接
import React, {
useState,
useRef,
useEffect,
useContext,
createContext
} from "react";
import "./styles.css";
const StateStoreContext = createContext({
stateStore: {},
setStateStore: () => {}
});
function StateStoreComponent(props) {
const [stateStore, setStateStore] = useState({});
return (
<StateStoreContext.Provider value={{ stateStore, setStateStore }}>
{props.children}
</StateStoreContext.Provider>
);
}
function useSpecialState(defaultValue, key) {
const { stateStore, setStateStore } = useContext(StateStoreContext);
const [state, setState] = useState(
stateStore[key] !== undefined ? stateStore[key] : defaultValue
);
const stateRef = useRef(state);
useEffect(() => {
console.log("child state change ---> ");
stateRef.current = state;
}, [state]);
useEffect(() => {
console.log("mount ---> ");
return () => {
console.log("un-mount <--- ");
console.log("prevStateStore ", stateStore);
console.log("persistState :: ", key, stateRef.current);
setStateStore((prevStateStore) => ({
...prevStateStore,
[key]: stateRef.current
}));
};
}, []);
return [state, setState];
}
const CounterComponent = () => {
const [counter, setCounter] = useSpecialState(0, "counter");
return (
<>
<h1>Hello CodeSandbox Counter: {counter}</h1>
<button onClick={() => setCounter((prev) => prev + 1)}>increment</button>
<br />
<br />
<br />
</>
);
};
export default function App() {
const [visible, setVisible] = useSpecialState(true, "visible");
return (
<div className="App">
<StateStoreComponent>
{visible && <CounterComponent />}
<button onClick={() => setVisible(!visible)}>toggle</button>
</StateStoreComponent>
</div>
);
}
英文:
I tried your useSpecialState
hook and It seems to work fine as expected.
From what I understand that you needed to save the last state value in the context when the component un-mounts.
I created a child component that increment
the counter and another toggle
button which mounts and un-mounts that CounterComponent
while persisting the counter value from the last un-mount.
Here is how I tried it. Working codesandbox
import React, {
useState,
useRef,
useEffect,
useContext,
createContext
} from "react";
import "./styles.css";
const StateStoreContext = createContext({
stateStore: {},
setStateStore: () => {}
});
function StateStoreComponent(props) {
const [stateStore, setStateStore] = useState({});
return (
<StateStoreContext.Provider value={{ stateStore, setStateStore }}>
{props.children}
</StateStoreContext.Provider>
);
}
function useSpecialState(defaultValue, key) {
const { stateStore, setStateStore } = useContext(StateStoreContext);
const [state, setState] = useState(
stateStore[key] !== undefined ? stateStore[key] : defaultValue
);
const stateRef = useRef(state);
useEffect(() => {
console.log("child state change ---> ");
stateRef.current = state;
}, [state]);
useEffect(() => {
console.log("mount ---> ");
return () => {
console.log("un-mount <--- ");
console.log("prevStateStore ", stateStore);
console.log("persistState :: ", key , stateRef.current);
setStateStore((prevStateStore) => ({
...prevStateStore,
[key]: stateRef.current
}));
};
}, []);
return [state, setState];
}
const CounterComponent = () => {
const [counter, setCounter] = useSpecialState(0, "counter");
return (
<>
<h1>Hello CodeSandbox Counter: {counter}</h1>
<button onClick={() => setCounter((prev) => prev + 1)}>increment</button>
<br />
<br />
<br />
</>
);
};
export default function App() {
const [visible, setVisible] = useSpecialState(true, "visible");
return (
<div className="App">
<StateStoreComponent>
{visible && <CounterComponent />}
<button onClick={() => setVisible(!visible)}>toggle</button>
</StateStoreComponent>
</div>
);
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论