我正在 React 中使用一个深度嵌套的状态对象。 我的代码库要求我们尝试坚持使用函数组件,因此每次我想更新该嵌套对象内的键/值对时,我都必须使用钩子来设置状态。 不过,我似乎无法获得更深的嵌套项目。我有一个带有onChange处理程序的下拉菜单。 。 。onChange 处理程序内部是一个内联函数,用于直接设置正在更改的任何键/值对的值。
但是,我在每个内联函数中的 spread 运算符之后使用的语法是错误的。
作为一种解决方法,我采用了将内联函数分解为其自己的函数,该函数在每次状态更改时重写整个状态对象,但这非常耗时且丑陋。 我宁愿像下面这样内联:
const [stateObject, setStateObject] = useState({
top_level_prop: [
{
nestedProp1: "nestVal1",
nestedProp2: "nestVal2"
nestedProp3: "nestVal3",
nestedProp4: [
{
deepNestProp1: "deepNestedVal1",
deepNestProp2: "deepNestedVal2"
}
]
}
]
});
<h3>Top Level Prop</h3>
<span>NestedProp1:</span>
<select
id="nested-prop1-selector"
value={stateObject.top_level_prop[0].nestedProp1}
onChange={e => setStateObject({...stateObject,
top_level_prop[0].nestedProp1: e.target.value})}
>
<option value="nestVal1">nestVal1</option>
<option value="nestVal2">nestVal2</option>
<option value="nestVal3">nestVal3</option>
</select>
<h3>Nested Prop 4</h3>
<span>Deep Nest Prop 1:</span>
<select
id="deep-nested-prop-1-selector"
value={stateObject.top_level_prop[0].nestprop4[0].deepNestProp1}
onChange={e => setStateObject({...stateObject,
top_level_prop[0].nestedProp4[0].deepNestProp1: e.target.value})}
>
<option value="deepNestVal1">deepNestVal1</option>
<option value="deepNestVal2">deepNestVal2</option>
<option value="deepNestVal3">deepNestVal3</option>
</select>
上面代码的结果给了我一个"nestProp1"和"deepNestProp1"是未定义的,大概是因为它们从未被达到/每个选择器更改了它们的状态。我的预期输出将是与选择器的当前值匹配的选定选项(在状态更改后(。 任何帮助将不胜感激。
我认为您应该使用setState
的功能形式,以便您可以访问当前状态并对其进行更新。
喜欢:
setState((prevState) =>
//DO WHATEVER WITH THE CURRENT STATE AND RETURN A NEW ONE
return newState;
);
看看这是否有帮助:
function App() {
const [nestedState,setNestedState] = React.useState({
top_level_prop: [
{
nestedProp1: "nestVal1",
nestedProp2: "nestVal2",
nestedProp3: "nestVal3",
nestedProp4: [
{
deepNestProp1: "deepNestedVal1",
deepNestProp2: "deepNestedVal2"
}
]
}
]
});
return(
<React.Fragment>
<div>This is my nestedState:</div>
<div>{JSON.stringify(nestedState)}</div>
<button
onClick={() => setNestedState((prevState) => {
prevState.top_level_prop[0].nestedProp4[0].deepNestProp1 = 'XXX';
return({
...prevState
})
}
)}
>
Click to change nestedProp4[0].deepNestProp1
</button>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
更新:带下拉菜单
function App() {
const [nestedState,setNestedState] = React.useState({
propA: 'foo1',
propB: 'bar'
});
function changeSelect(event) {
const newValue = event.target.value;
setNestedState((prevState) => {
return({
...prevState,
propA: newValue
});
});
}
return(
<React.Fragment>
<div>My nested state:</div>
<div>{JSON.stringify(nestedState)}</div>
<select
value={nestedState.propA}
onChange={changeSelect}
>
<option value='foo1'>foo1</option>
<option value='foo2'>foo2</option>
<option value='foo3'>foo3</option>
</select>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
React 状态的主要规则是不直接修改状态。这包括在顶级状态对象中保存的对象,或保存在其中的对象等。因此,要修改您的嵌套对象并让 React 可靠地处理结果,您必须复制您更改的每个图层。(是的,真的。详细信息如下,并附有文档链接。
另外,当您基于现有状态更新状态时,最好使用状态设置器的回调版本,因为状态更新可能是异步的(我不知道为什么他们在那里说"可能是",它们是异步的(并且状态更新被合并,因此使用旧的状态对象可能会导致过时的信息被放回状态。
考虑到这一点,让我们看一下您的第二个更改处理程序(因为它比第一个更深入(,它需要更新stateObject.top_level_prop[0].nestprop4[0].deepNestProp1
。为了正确地做到这一点,我们必须复制我们正在修改的最深的对象(stateObject.top_level_prop[0].nestprop4[0]
(及其所有父对象;其他对象可以重复使用。这就是:
stateObject
top_level_prop
top_level_prop[0]
top_level_prop[0].nestprop4
top_level_prop[0].nestprop4[0]
那是因为它们都是通过改变top_level_prop[0].nestprop4[0].deepNestProp1
来"改变"的。
所以:
onChange={({target: {value}}) => {
// Update `stateObject.top_level_prop[0].nestprop4[0].deepNestProp1`:
setStateObject(prev => {
// Copy of `stateObject` and `stateObject.top_level_prop`
const update = {
...prev,
top_level_prop: prev.top_level_prop.slice(), // Or `[...prev.top_level_prop]`
};
// Copy of `stateObject.top_level_prop[0]` and `stateObject.top_level_prop[0].nextprop4`
update.top_level_prop[0] = {
...update.top_level_prop[0],
nextprop4: update.top_level_prop[0].nextprop4.slice()
};
// Copy of `stateObject.top_level_prop[0].nextprop4[0]`, setting the new value on the copy
update.top_level_prop[0].nextprop4[0] = {
...update.top_level_prop[0].nextprop4[0],
deepNestProp1: value
};
return update;
});
}}
不要复制树中没有更改的其他对象,因为渲染它们的任何组件都不需要重新渲染,但我们需要复制要更改的最深的对象及其所有父对象。
围绕这一点的尴尬是尽可能保持useState
使用状态对象的原因之一。
但我们真的必须这样做吗?
是的,让我们看一个例子。下面是一些不执行必要复制的代码:
const {useState} = React;
const ShowNamed = React.memo(
({obj}) => <div>name: {obj.name}</div>
);
const Example = () => {
const [outer, setOuter] = useState({
name: "outer",
middle: {
name: "middle",
inner: {
name: "inner",
},
},
});
const change = () => {
setOuter(prev => {
console.log("Changed");
prev.middle.inner.name = prev.middle.inner.name.toLocaleUpperCase();
return {...prev};
});
};
return <div>
<ShowNamed obj={outer} />
<ShowNamed obj={outer.middle} />
<ShowNamed obj={outer.middle.inner} />
<input type="button" value="Change" onClick={change} />
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
请注意,即使状态已更改,单击按钮似乎也没有执行任何操作(除了记录"已更改"(。这是因为传递给ShowName
的对象没有改变,所以ShowName
没有重新渲染。
这是一个执行必要更新的方法:
const {useState} = React;
const ShowNamed = React.memo(
({obj}) => <div>name: {obj.name}</div>
);
const Example = () => {
const [outer, setOuter] = useState({
name: "outer",
middle: {
name: "middle",
inner: {
name: "inner",
},
},
});
const change = () => {
setOuter(prev => {
console.log("Changed");
const update = {
...prev,
middle: {
...prev.middle,
inner: {
...prev.middle.inner,
name: prev.middle.inner.name.toLocaleUpperCase()
},
},
};
return update;
});
};
return <div>
<ShowNamed obj={outer} />
<ShowNamed obj={outer.middle} />
<ShowNamed obj={outer.middle.inner} />
<input type="button" value="Change" onClick={change} />
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
该示例使用React.memo
来避免在子组件的 props 未更改时重新呈现子组件。同样的事情发生在PureComponent
或任何实现shouldComponentUpdate
并且在其道具未更改时不会更新的组件。
React.memo
/PureComponent
/shouldComponentUpdate
用于主要代码库(和抛光组件(,以避免不必要的重新渲染。天真的不完整状态更新会在使用它们时咬你,也可能在其他时候
另一种方法是使用 useReducer 钩
子
const App = () => {
const reducer = (state, action) =>{
return {...state, [action.type]: action.payload}
}
const [state, dispatch] = React.useReducer(reducer,{
propA: 'foo1',
propB: 'bar1'
});
const changeSelect = (prop, event) => {
const newValue = event.target.value;
dispatch({type: prop, payload: newValue});
}
return(
<React.Fragment>
<div>My nested state:</div>
<div>{JSON.stringify(state)}</div>
<select
value={state.propA}
onChange={(e) => changeSelect('propA', e)}
>
<option value='foo1'>foo1</option>
<option value='foo2'>foo2</option>
<option value='foo3'>foo3</option>
</select>
<select
value={state.propB}
onChange={(e) => changeSelect('propB', e)}
>
<option value='bar1'>bar1</option>
<option value='bar2'>bar2</option>
<option value='bar3'>bar3</option>
</select>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>