React:使用 Hooks 为深度嵌套对象设置状态
Posted
技术标签:
【中文标题】React:使用 Hooks 为深度嵌套对象设置状态【英文标题】:React: Setting State for Deeply Nested Objects w/ Hooks 【发布时间】:2020-01-07 23:08:17 【问题描述】:我正在使用 React 中深度嵌套的状态对象。我的代码库要求我们尝试使用函数组件,因此每次我想更新嵌套对象中的键/值对时,我都必须使用钩子来设置状态。不过,我似乎无法理解更深层次的嵌套项目。我有一个带有 onChange 处理程序的下拉菜单。 . .inside onChange 处理程序是一个内联函数,用于直接设置正在更改的任何键/值对的值。
然而,我在每个内联函数中展开运算符之后使用的语法是错误的。
作为一种解决方法,我已将内联函数分解为它自己的函数,该函数在每次状态更改时重写整个状态对象,但这非常耗时且丑陋。我宁愿像下面这样内联:
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”是未定义的,大概是因为它们永远不会被每个选择器到达/改变它们的状态。我的预期输出将是与选择器当前 val 的值匹配的选定选项(在状态更改之后)。任何帮助将不胜感激。
【问题讨论】:
创建一个可重现的最小示例,以便我们测试问题。 ***.com/help/minimal-reproducible-example 【参考方案1】:我认为您应该使用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"/>
【讨论】:
这行得通!谢谢你。现在把它变成一个下拉列表。 . . 您需要将其作为内联函数吗?将其设置为独立函数并使用选项值作为参数调用它更容易,不是吗? 不一定。 . .但我需要能够运行带有事件的内联函数,而不是像他那样手动为其分配一个字符串值。现在想弄清楚。 . . 你知道如何运行你引用的同一个函数,同时从下拉列表中传递一个事件吗? 我添加了一个新示例。看看有没有帮助。【参考方案2】:另一种方法是使用useReducer hook
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"/>
【讨论】:
【参考方案3】:React 状态的主要规则是do not modify state directly。这包括保存在***状态对象中的对象,或保存在 them 中的对象等。因此,要修改嵌套对象并让 React 可靠地处理结果,您必须复制您更改的每一层. (是的,真的。下面有详细信息,带有文档链接。)
另外,当您根据现有状态更新状态时,最好使用状态设置器的回调版本,因为state updates may be asynchronous(我不知道他们为什么说“可能存在”,他们是异步的)和state updates are merged,所以使用旧的状态对象可能会导致过时的信息被放回状态。
考虑到这一点,让我们看看您的第二个更改处理程序(因为它比第一个更深),它需要更新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
来避免在子组件的道具未更改时重新渲染它们。 PureComponent
或任何实现 shouldComponentUpdate
并且在其 props 未更改时不会更新的组件也会发生同样的事情。
React.memo
/ PureComponent
/ shouldComponentUpdate
用于主要代码库(和抛光组件),以避免不必要的重新渲染。幼稚的不完整状态更新会在使用它们时给您带来麻烦,并且可能在其他时候也是如此。
【讨论】:
以上是关于React:使用 Hooks 为深度嵌套对象设置状态的主要内容,如果未能解决你的问题,请参考以下文章