【问题标题】:useState hook can only set one object at the time and return the other object of the same array to initial stateuseState 钩子一次只能设置一个对象,并将同一个数组的另一个对象返回到初始状态
【发布时间】: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

【问题讨论】:

    标签: reactjs react-hooks state use-state


    【解决方案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 })
        )
      );
      

    【讨论】:

      猜你喜欢
      • 2022-11-15
      • 2022-10-08
      • 1970-01-01
      • 2018-07-28
      • 1970-01-01
      • 2021-09-03
      • 2021-06-30
      • 1970-01-01
      • 2018-12-29
      相关资源
      最近更新 更多