我正在阅读一篇名为“ A Complete Guide to useEffect”的文章,并尝试实现“ Why useReducer Is the Cheat Mode of Hooks”部分的示例。
在该示例中,有一个Counter
组件借助useReducer
钩子定义状态(只是一个数字)。减速器仅处理一个动作-'tick'
,在该动作上,状态将以step
prop的值递增。 'tick'
动作是在useEffect
挂钩中设置的间隔函数中每秒分发一次。
这是该示例中的代码,并做了一些小的修改:
function Counter({ step }) {
const [count, dispatch] = React.useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === "tick") {
console.log(`Reducer: state=${state} and step=${step}`);
return state + step;
} else {
throw new Error(`Unknown action type: ${action.type}`);
}
}
React.useEffect(() => {
console.log("Create interval");
const id = setInterval(() => {
console.log("Dispatch");
dispatch({ type: "tick" });
}, 1000);
return () => {
console.log("Clear interval");
clearInterval(id);
};
}, [dispatch]);
return <h1>{count}</h1>;
}
function App() {
const [step, setStep] = React.useState(0);
return (
<>
<Counter step={step} />
<input
type="number"
value={step}
onChange={(e) => setStep(Number(e.target.value))}
/>
</>
);
}
我发现该示例适用于react@16.8.0-alpha.0
,不适用于react@16.8.0
及更高版本。运行代码时,步骤和计数器的初始值为0
。如果我等待3秒而不更改任何内容,然后增加步骤,则会得到以下输出:
Create interval
Dispatch
Reducer: state=0 and step=0
Dispatch
Reducer: state=0 and step=0
Dispatch
Reducer: state=0 and step=0
Reducer: state=0 and step=1
Reducer: state=1 and step=1
Reducer: state=2 and step=1
Dispatch
Reducer: state=3 and step=1
Reducer: state=3 and step=1
Dispatch
Reducer: state=4 and step=1
Dispatch
Reducer: state=5 and step=1
从日志中可以看到,reducer的执行比调度"tick"
动作要多。
通过从step
道具创建引用并使用useCallback
钩子对化简器进行了记忆化,使我能够按预期工作。
const stepRef = React.useRef(step);
React.useEffect(() => {
stepRef.current = step;
}, [step]);
const reducer = useCallback((state, action) => {
if (action.type === "tick") {
console.log(`Reducer: state=${state} and step=${stepRef.current}`);
return state + stepRef.current;
} else {
throw new Error(`Unknown action type: ${action.type}`);
}
}, []);
您可以在此处使用示例:
react@16.8.0-alpha.0
,则会按预期运行); 但是问题仍然存在。
react@16.8.0-alpha.0
钩中的react@16.8.0
钩中的哪些行为(useReducer
或step
)被认为是正确的?最后一个问题的答案应该与还原器正在重新创建这一事实有关。无论是在每个渲染器上,还是仅在useCallback
道具更改时,都没关系,因为使用[step]
钩子对化简器进行记忆并将exoplayer 2.11.8
作为依赖项数组传递并不能解决问题。有人对此有任何想法吗?
谢谢!
答案 0 :(得分:1)
useReducer
需要记住reducer
,以便能够说出组件是否需要重新渲染(需要它来计算状态并将其与先前的状态进行比较)。但是,由于您可以“交换”减速器,因此它可以访问的减速器可能已过时。因此,在化简器状态与上一个化简器状态相同的情况下,不是丢弃结果并将其命名为一天,React会存储结果以及用于计算结果的化简器,直到下一次重新渲染为止。然后检查减速器是否仍然相同。如果不是,则使用新的reducer重新计算状态。
您的示例是一个极端案例。它不应该那样工作,但是Ract不知道它何时确切应该丢弃陈旧的reducer状态更新-它总是等到下一次重新渲染以比较reducer并计算将在重新渲染中使用的最终状态。 / p>
这是一个描述它的简化示例:
最初step
是0
。 dispatch
运行3次。结果是
0 + 0 + 0 = 0
并正确显示。然后,当您单击按钮时,step
变为1
,但是dispatch
甚至不会触发一次。不过,由于使用了新创建的reducer重新运行了以前的所有操作,因此结果为3
。
function App() {
console.log("rendering");
const [step, setStep] = React.useState(0);
const [count, dispatch] = React.useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === "tick") {
console.log(`Reducer: state=${state} and step=${step}`);
return state + step;
}
}
React.useEffect(() => {
for(let i = 0; i < 3; i++) {
console.log("Dispatch");
dispatch({ type: "tick" });
}
}, []);
return (
<div>
<span>{count} </span>
<button
onClick={(e) => setStep(step + 1)}
>step +1</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.development.js"></script>
<div id="root"></div>
解决方案是使reducer变得纯净(应该从一开始就如此),然后将所有必需的数据传递到action.payload
中(如有需要,请ref
添加内容)。
function reducer(state, action) {
if (action.type === "tick") {
const { step } = action.payload;
console.log(`Reducer: state=${state} and step=${step}`);
return state + step;
}
}
function Counter({ step }) {
const [count, dispatch] = React.useReducer(reducer, 0);
const stepRef = React.useRef();
stepRef.current = step;
React.useEffect(() => {
console.log("Create interval");
const id = setInterval(() => {
console.log("Dispatch");
dispatch({ type: "tick", payload: { step: stepRef.current }});
}, 1000);
return () => {
console.log("Clear interval");
clearInterval(id);
};
}, [dispatch]);
return <span>{count} </span>;
}
function App() {
const [step, setStep] = React.useState(0);
return (
<div>
<Counter step={step} />
<input
type="number"
value={step}
onChange={(e) => setStep(Number(e.target.value))}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.development.js"></script>
<div id="root"></div>
答案 1 :(得分:0)
import React from "react";
import ReactDOM from "react-dom";
function App() {
const [step, setStep] = React.useState(0);
function Counter({ step }) {
const [count, dispatch] = React.useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === "tick") {
console.log(`Reducer: state=${state} and step=${step}`);
return step === 0 ? state : state + step;
} else {
throw new Error(`Unknown action type: ${action.type}`);
}
}
React.useEffect(() => {
console.log("Create interval");
const id = setInterval(() => {
console.log("Dispatch");
dispatch({ type: "tick" });
}, 1000);
return () => {
console.log("Clear interval");
clearInterval(id);
};
}, [dispatch]);
return <h1>{count}</h1>;
}
return (
<>
<Counter step={step} />
<input
type="number"
value={step}
onChange={(e) => setStep(Number(e.target.value))}
/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);