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 包含&lt;FormsByAudience audienceId=id books=books /&gt;

因此,在单个渲染中,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 而不是父级中导入和调用 FormsByAudiencerenderByAudience 创建一个有时永远不会被使用的 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 钩子一次只能设置一个对象,并将同一个数组的另一个对象返回到初始状态的主要内容,如果未能解决你的问题,请参考以下文章

反应钩子useState()的真正奇怪的行为

React Native:如何从循环中获取值到钩子 useState

在 useState 钩子中反应设置状态

React useState钩子无限期运行 - 无限循环

使用钩子从数组中删除对象(useState)

使用 useState() 钩子测试功能组件时设置状态