useState 钩子一次只能设置一个对象,并将同一数组的另一个对象返回到初始状态

Ndx*_*Ndx 8 state reactjs react-hooks use-state

我有这种形式的数据:

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" }
        ]
      }]
Run Code Online (Sandbox Code Playgroud)

表格是嵌套的。对于每本书,都有评论家和读者,并且会根据 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>
Run Code Online (Sandbox Code Playgroud)

这是根据受众 ID 呈现的表单

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>
  );
Run Code Online (Sandbox Code Playgroud)

最后,对于每个读者/评论家,都有一个表格可以输入。

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>
  );
Run Code Online (Sandbox Code Playgroud)

问题

每次我填写评论家/读者的字段时,它都会为该对象设置状态,但也会为其余对象设置初始状态。它只设置焦点所在的表单状态,不保留其他表单的值

我不知道是什么导致了这个问题。

这是一个重现问题的沙箱https://codesandbox.io/s/project-u0m5n

Cer*_*nce 3

哇,这真是太棒了。最终的问题源于您调用以下组件的事实:

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)}
Run Code Online (Sandbox Code Playgroud)

对于每一个不同的评论家(和读者):

props.audiences.critics.map((c) => (
    <div key={c.id}>
        <div>{c.name}</div>
        {props.shouldRenderCritics &&
            props.criticsChildren &&
            cloneElement(props.criticsChildren, { criticId: c.id })}
    </div>
))}
Run Code Online (Sandbox Code Playgroud)

其中props.criticsChildren包含<FormsByAudience audienceId={id} books={books} />.

因此,在单个 render内部,变量有许多单独的绑定critics。您没有critics针对整个应用程序或特定受众的单一内容;你有多个critics数组,每个批评者一个。设置一个批评家critics数组的状态FormsByAudience不会导致其他critics批评家的 React 元素关闭的其他数组发生更改。

为了解决这个问题,考虑到critics数组是从创建的props.bookscritics状态应该放在使用书籍的同一级别附近,并且绝对不要放在嵌套映射器函数的组件内。然后将状态和状态设置者传递给孩子们。

完全相同的事情也适用于readers国家。

这是一个最小的实时堆栈片段,说明了这个问题:

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)}
Run Code Online (Sandbox Code Playgroud)
props.audiences.critics.map((c) => (
    <div key={c.id}>
        <div>{c.name}</div>
        {props.shouldRenderCritics &&
            props.criticsChildren &&
            cloneElement(props.criticsChildren, { criticId: c.id })}
    </div>
))}
Run Code Online (Sandbox Code Playgroud)

为了使 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 }} />;
  };
Run Code Online (Sandbox Code Playgroud)

向下传递dataAndSetters直到您到达

export default function FormsByAudience(props: FormsByAudienceProps) {
  const { critics, setCritics, readers, setReaders } = props.dataAndSetters;
Run Code Online (Sandbox Code Playgroud)

现场演示:

https://codesandbox.io/s/project-forked-woqhf


如果我可以提供一些其他建议,以显着提高您的代码质量:

  • 进行更改Recap,以便FormsByAudience导入并调用它,Recap而不是在父级中调用。renderByAudience创建一个有时永远不会被使用的 React 元素真的很奇怪(而且很难一眼理解) 。

  • Recap评论家和读者已经转变了。这可能是无意的。这:

    <h5>Readers</h5>
      {!isEmpty(props.audiences.critics) &&
        props.audiences.critics.map((c) => (
    
    Run Code Online (Sandbox Code Playgroud)

    可能应该是

    <h5>Readers</h5>
      {props.audiences.readers.map((c) => (
    
    Run Code Online (Sandbox Code Playgroud)

    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);
    
    Run Code Online (Sandbox Code Playgroud)

    是不必要的。只需从父组件传递读者/评论家索引critics[criticIdx],然后用于找到有问题的评论家。

  • 在 中hook.ts,这个:

    const byAudience = books
      .map((b) => b.audiences)
      .flat()
      .filter((bk) => [1].includes(bk.audienceId));
    
    Run Code Online (Sandbox Code Playgroud)

    简化为

    const byAudience = books
      .flatMap(b => b.audiences)
      .filter(bk => bk.audienceId === 1);
    
    Run Code Online (Sandbox Code Playgroud)

    和这个:

    byAudience.forEach((el) => {
      el.readers.map((r) => {
        return readersToSet.push({ feedback: "", rate: "", readerId: r.id });
      });
      return readersToSet;
    });
    
    Run Code Online (Sandbox Code Playgroud)

    没有意义,因为forEach忽略其返回值,并且.map仅应在从回调返回值以构造新数组时使用。使用flatMaponbyAudience或将其推入for..of循环。

    const readersToSet = byAudience.flatMap(
      el => el.readers.map(
        r => ({ feedback: "", rate: "", readerId: r.id })
      )
    );
    
    Run Code Online (Sandbox Code Playgroud)