useState 钩子一次只能设置一个对象,并将同一个数组的另一个对象返回到初始状态
Posted
技术标签:
【中文标题】useState 钩子一次只能设置一个对象,并将同一个数组的另一个对象返回到初始状态【英文标题】:useState hook can only set one object at the time and return the other object of the same array to initial state 【发布时间】:2021-06-05 03:13:32 【问题描述】:我有这种形式的数据:
books = [
id: 1,
name: "book-1",
audiences: [
audienceId: 1,
name: "Cat A",
critics: [
id: 5, name: "Jack" ,
id: 45, name: "Mike" ,
id: 32, name: "Amanda"
],
readers: [
id: 11, fullName: "Mike Ross" ,
id: 76, fullName: "Natalie J" ,
id: 34, fullName: "Harvey Spectre"
]
]
表单是嵌套的。每本书都有评论家和读者,会根据 AudienceId 的值来渲染各个表单
<div className="App">
books.map((book, index) => (
<div key=book.id>
<h1>Books index + 1</h1>
book.audiences.map((au) => (
<div key=au.audienceId>
<h3>au.name</h3>
<Recap
audiences=au
shouldRenderCritics=au.audienceId === 2
shouldRenderReaders=au.audienceId === 1
criticsChildren=renderByAudience(au.audienceId)
readersChildren=renderByAudience(au.audienceId)
/>
</div>
))
</div>
))
</div>
这是根据 AudienceId 呈现的表单
return (
<div className="root">
<div>
props.audienceId === 1 && (
<A
setReader=setReader(readerIdx)
reader=selectedReader as IreaderState
/>
)
</div>
<div>
props.audienceId === 2 && (
<B
setCritic=setCritic(criticIdx)
critic=selectedCritic as IcriticState
/>
)
</div>
</div>
);
最后,对于每个读者/评论家,都有一个表格可以输入。
export default function A(props: Aprops)
const handleChange = (
event: ChangeEvent< value: number | string; name: string >
) =>
const name, value = event.target;
const r = ...props.reader ;
r[name] = value;
props.setReader(r);
;
return (
<div>
<TextField
name="rate"
type="number"
value=get(props.reader, "rate", "")
onChange=handleChange
/>
<TextField
name="feedback"
value=get(props.reader, "feedback", "")
onChange=handleChange
/>
</div>
);
问题
每次我填写评论家/读者字段时,它都会为该对象设置状态,但也会为其余对象设置初始状态。它只设置焦点所在的表单状态,不保留其他表单的值
我不知道是什么导致了问题。
这里是一个沙盒重现问题https://codesandbox.io/s/project-u0m5n
【问题讨论】:
【参考方案1】:哇,这太棒了。最终问题源于您调用以下组件这一事实:
export default function FormsByAudience(props: FormsByAudienceProps)
const [critics, setCritics] = useCritics(props.books);
const setCritic = (index: number) => (critic: IcriticState) =>
const c = [...critics];
c[index] = critic;
setCritics(c);
;
// ...
// in render method:
setCritic=setCritic(criticIdx)
对于每个不同的评论家(和读者):
props.audiences.critics.map((c) => (
<div key=c.id>
<div>c.name</div>
props.shouldRenderCritics &&
props.criticsChildren &&
cloneElement(props.criticsChildren, criticId: c.id )
</div>
))
其中props.criticsChildren
包含<FormsByAudience audienceId=id books=books />
。
因此,在单个渲染中,critics
变量有很多单独的绑定。对于整个应用程序或给定的受众,您没有一个 critics
;您有多个 critics
数组,每个评论者一个。在FormsByAudience
中为一个评论家的critics
数组设置状态不会导致其他评论家的React 元素关闭的其他critics
数组发生变化。
为了解决这个问题,鉴于 critics
数组是从 props.books
创建的,critics
状态应该放置在使用书籍的同一级别附近,并且绝对不在嵌套映射器函数。然后将 state 和 state setter 传递给孩子。
同样的事情也适用于readers
状态。
这里有一个最小的实时堆栈片段来说明这个问题:
const people = ['bob', 'joe'];
const Person = (name, i) =>
const [peopleData, setPeopleData] = React.useState(people.map(() => 0));
console.log(peopleData.join(','));
const onChange = e =>
setPeopleData(
peopleData.map((num, j) => j === i ? e.target.value : num)
)
;
return (
<div>
<div>name</div>
<input type="number" value=peopleData[i] onChange=onChange />
</div>
);
;
const App = () =>
return people.map(
(name, i) => <Person key=i ... name, i />
);
;
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>
为了使传递的 props 更易于键入和阅读,您可以考虑将 4 个变量(2 个数据和 2 个 setter)合并到一个对象中。
export default function App()
const [critics, setCritics] = useCritics(books);
const [readers, setReaders] = useReaders(books);
const dataAndSetters = critics, setCritics, readers, setReaders ;
const renderByAudience = (id: number) =>
return <FormsByAudience audienceId=id ... books, dataAndSetters />;
;
向下传递dataAndSetters
,直到到达
export default function FormsByAudience(props: FormsByAudienceProps)
const critics, setCritics, readers, setReaders = props.dataAndSetters;
现场演示:
https://codesandbox.io/s/project-forked-woqhf
如果我可以提供一些其他建议,以显着提高您的代码质量:
更改 Recap
以便在 Recap
而不是父级中导入和调用 FormsByAudience
。 renderByAudience
创建一个有时永远不会被使用的 React 元素真的很奇怪(而且一目了然很难理解)。
Recap
让评论家和读者转了转。这可能是无意的。这个:
<h5>Readers</h5>
!isEmpty(props.audiences.critics) &&
props.audiences.critics.map((c) => (
应该是
<h5>Readers</h5>
props.audiences.readers.map((c) => (
(在映射数组之前不需要检查isEmpty
)
整体
const selectedReader =
readers.find((r) => r.readerId === (props.readerId as number)) || ;
const readerIdx = readers.indexOf(selectedReader as IreaderState);
const selectedCritic =
critics.find((r) => r.criticId === (props.criticId as number)) || ;
const criticIdx = critics.indexOf(selectedCritic as IcriticState);
是不必要的。只需从父组件向下传递 reader/critic index,然后使用 critics[criticIdx]
找到相关的评论家。
在hook.ts
,这个:
const byAudience = books
.map((b) => b.audiences)
.flat()
.filter((bk) => [1].includes(bk.audienceId));
简化为
const byAudience = books
.flatMap(b => b.audiences)
.filter(bk => bk.audienceId === 1);
还有这个:
byAudience.forEach((el) =>
el.readers.map((r) =>
return readersToSet.push( feedback: "", rate: "", readerId: r.id );
);
return readersToSet;
);
没有意义,因为forEach
忽略了它的返回值,并且.map
应该只在从回调返回一个值以构造一个新数组时使用。要么在 byAudience
上使用 flatMap
,要么在 for..of
循环内推入。
const readersToSet = byAudience.flatMap(
el => el.readers.map(
r => ( feedback: "", rate: "", readerId: r.id )
)
);
【讨论】:
以上是关于useState 钩子一次只能设置一个对象,并将同一个数组的另一个对象返回到初始状态的主要内容,如果未能解决你的问题,请参考以下文章