React:状态的状态

  • 本文关键字:状态 React reactjs
  • 更新时间 :
  • 英文 :


React Hook "useState"不能在回调中调用。React Hooks必须在React函数组件或自定义React Hook函数中调用

我理解为什么我得到这个信息,但同时我认为我想要的应该是可以实现的。下面是代码:

import React, { useState } from "react";
import "./styles.css";
export default function App() {
const children = useState([]);
return (
<div>
{children.map((child, i) => (<Child child={child} />))}
<button
onClick={() => { children[1]([...children, useState(0)]); }}
>
Add
</button>
</div>
);
}
function Child(props) {
const [state, setState] = props.child;
return (
<div>
<input
type="range"
min="1"
max="255"
value={state}
onChange={(e) => setState(e.target.value)}
></input>
</div>
);
}

我希望每个<Child>完全控制其状态,而不必声明updateChild(i : Index, data)类型的函数,然后在列表中找到正确的子节点,等等。因为这不能很好地扩展深度嵌套的视图层次结构。

是否有一种方法可以同时使用:

  • 将状态保留在父级(单一真实源)
  • 允许子节点改变自己的状态

简而言之,我想要实现这一点,但要使用多个子组件。

为了说明您当前方法的问题以及为什么React不会让您这样做,这里是useState的伪实现,具有与真实版本大致相同的行为(我已经将触发重新渲染作为练习留给用户,它不支持功能更新,但这里重要的是显示Parent组件的底层状态)。

// Approximation of useState
let parentStateIndex;
const parentState = [];
const useState = (defaultValue) => {
const i = parentStateIndex;
if (parentState.length === i) {
parentState.push(defaultValue);
}
parentStateIndex += 1;
return [parentState[i], (value) => parentState[i] = value];
}
const Parent = () => {
parentStateIndex = 0;  // hack required to reset on each "render"
const [state, setState] = useState([]);
return (
<>
{state.map((child) => (
<Child child={child} />
)}
<button
onClick={() => setState([...state, useState(0)])}
>
Add
</button>
</>
);
};
const Child = ({ child }) => {
const [state, setState] = child;
return (
<input
type="range"
min="0"
max="255"
value={state}
onChange={({ target }) => setState([target.value, setState])}
/>
);
};

(注意,关于父状态的一些知识已经悄悄进入了子状态-如果只有setState(target.value),那么[value, setter]对将被value取代,其他东西将开始爆炸。但我认为这种方式可以更好地说明下面发生的事情。

第一次呈现父元素时,传递给useState的新数组被添加到状态:

parentState = [
[
[],
(value) => parentState[0] = value
],
];

到目前为止一切顺利。现在假设单击了Add按钮。再次调用useState,更新状态以添加第二个项目,并将新项目添加到第一个项目:

parentState = [
[
[
[0, (value) => parentState[1] = value]
],
(value) => parentState[0] = value,
],
[
0,
(value) => parentState[1] = value,
],
];

这似乎也起作用了,但是现在当子值更新为例如1时会发生什么?

parentState = [
[
[
[0, /* this gets called: */ (value) => parentState[1] = value],
],
(value) => parentState[0] = value,
],
[
/* but this gets changed: */ 1,
(value) => parentState[1] = value,
],
];

状态的第二部分被更新,它完全改变了它的类型,但第一部分仍然保持旧的值。

当父元素重新呈现时,useStateis只被调用一次,所以父元素状态中的第二项是不相关的;子节点获取旧值parentState[0][0],仍然是[0, () => ...]

当Add按钮再次被点击时,因为这是useState第二次在这个渲染中被调用,现在我们得到了用于第一个子的新值,作为第二个子:

parentState = [
[
[
[0, (value) => parentState[1] = value],
[1, (value) => parentState[1] = value],
],
(value) => parentState[0] = value,
],
[
1,
(value) => parentState[1] = value,
],
];

And,正如你所看到的,对第一个子节点或第二个子节点的修改都指向相同的值;它们实际上不会出现在任何地方,直到再次单击Add按钮,它们突然成为第三个子元素的值。

有关钩子如何工作以及为什么调用顺序如此重要的更多信息,请参见Dan Abramov的<为什么React钩子依赖于调用顺序?>


那么什么有效呢?为了使子进程能够正确地更新父进程的状态,它至少需要setter和自己的索引:

const Parent = () => {
const [state, setState] = useState([]);
return (
<>
{state.map((child, index) => (
<Child child={child} index={index} setState={setState} />
)}
<button
onClick={() => setState([...state, 0])}
>
Add
</button>
</>
);
};
const Child = ({ child, index, setState }) => {
return (
<input
type="range"
min="0"
max="255"
value={child}
onChange={({ target }) => setState((oldState) => {
return oldState.map((oldValue, i) => i === index
? target.value
: oldValue);
})}
/>
);
};

但这意味着父进程不再控制自己的状态,而子进程必须知道父进程的所有状态——这两个组件非常紧密地耦合在一起,所以它们之间的边界显然是在错误的地方(也许根本不应该存在)。


那么正确的解决方案是什么?这是一件你不想做的事,让孩子"打电话回家";使用已更新的值,并让父进程相应地更新其状态。这使得子进程与父进程的细节解耦,其实现也相应简单:

const Parent = () => {
const [state, setState] = useState([]);
const onChange = (newValue, index) => setState((oldState) => {
return oldState.map((oldValue, i) => i === index
? newValue
: oldValue);
};
return (
<>
{state.map((child, index) => (
<Child child={child} onChange={(value) => onChange(value, index)} />
)}
<button
onClick={() => setState([...state, 0])}
>
Add
</button>
</>
);
};
const Child = ({ child, onChange }) => {
return (
<input
type="range"
min="0"
max="255"
value={child}
onChange={({ target: { value } }) => onChange(value)}
/>
);
};

最新更新