Состояние компонента является производным от его prop, и это prop происходит от асинхронного вызова API

#javascript #reactjs

#javascript #reactjs

Вопрос:

Я столкнулся с этой ситуацией недавно при разработке некоторой бизнес-логики. Вот упрощенный и надуманный пример из него

 function App() {
  const [myState, setMyState] = useState([])

  useEffect(() => {
    setTimeout(() => {
      setMyState((prevState) =>
        prevState.concat([
          {
            type: 'foo',
            enabled: true,
          },
          {
            type: 'bar',
            enabled: true,
          },
          {
            type: 'baz',
            enabled: true,
          },
        ])
      )
    }, 500)
  }, [])

  return (
    <div className="App">
      <ChildComponent propFromApp={myState} />
    </div>
  )
}
  

Я использовал setTimeout для моделирования вызова API, и он передал состояние ChildComponent .

 function ChildComponent({ propFromApp }) {
  const allTypes = propFromApp.map((item) => item.type)
  const [checkedItems, setCheckedItems] = useState(allTypes)

  return (
    <div>
      {allTypes.map((type, index) => (
        <div key={index}>
          <input
            type="checkbox"
            name={type}
            checked={checkedItems.includes(type)}
            onChange={() => {
              if (checkedItems.includes(type)) {
                setCheckedItems((prevState) =>
                  prevState.filter((prevType) => type !== prevType)
                )
              } else {
                setCheckedItems((prevState) => prevState.concat(type))
              }
            }}
          />
          <label htmlFor={type}>{type}</label>
        </div>
      ))}
    </div>
  )
}
  

И ChildComponent отвечает за рендеринг множества флажков на основе реквизита. Сначала он извлек массив type из prop, который вызывается allTypes , а затем установил свое собственное состояние, checkedItems используя это allTypes в качестве начального значения. таким образом, изначально проверяются все элементы. Причина, по которой я заставляю ChildComponent сохранять их как свое собственное состояние, заключается в том, что компонент может таким образом управлять ими с помощью флажка.

Однако, поскольку propFromApp изначально это пустой массив, состояние ChildComponent so checkedItems также инициализируется как пустой массив и не будет обновляться, когда реальные данные возвращаются из вызова API, что происходит через 500 мс после запуска обратного вызова в setTimeout .

Итак, затем я использую useEffect для обновления checkedItems при обновлении реквизита.

итак, теперь ChildComponent выглядит так

 
function ChildComponent({ propFromApp }) {
  const allTypes = propFromApp.map((item) => item.type)
  const [checkedItems, setCheckedItems] = useState(allTypes)

  useEffect(() => {
    setCheckedItems(allTypes)
  }, [allTypes])

  return (....)
  

Однако это привело к ошибке

Превышена максимальная глубина обновления. Это может произойти, когда компонент вызывает setState внутри useEffect, но useEffect либо не имеет массива зависимостей, либо одна из зависимостей изменяется при каждом рендеринге.

Я полагал, что это потому, что allTypes это не один из примитивных типов, поэтому проверка равенства всегда определяла бы, что allTypes отличается каждый раз, когда компонент повторно отображает. Итак, наконец, я должен изменить его на using useMemo , чтобы избежать проблемы. Теперь ChildComponent выглядит следующим образом

 function ChildComponent({ propFromApp }) {
  const allTypes = useMemo(() => propFromApp.map((item) => item.type), [
    propFromApp,
  ])
  const [checkedItems, setCheckedItems] = useState(allTypes)

  useEffect(() => {
    setCheckedItems(allTypes)
  }, [allTypes])

  return (....)
  

Теперь это решает проблему. но я не уверен, что это правильный способ сделать это или это хорошая практика. Кто-нибудь может предложить лучшую альтернативу или указать, где мой код недостаточно хорош?

Вот живой купол https://codesandbox.io/s/async-sky-3vl1r?file=/src/App.js

Ответ №1:

useMemo в этом контексте обычно проблема решается, но может и не решаться. useMemo может пересчитывать или не пересчитывать его значение на основе некоторой логики управления памятью, написанной людьми умнее меня. Поэтому вам нужно будет написать свой код, имея в виду, что ссылочный идентификатор сохраненного значения может измениться. Изменение ссылочного идентификатора означает, что массивы deps изменятся.

Его можно использовать useMemo для исправления бесконечного цикла. В большинстве случаев пересчет вызывает дополнительный повторный запуск и ничего больше.

Нехорошо useMemo влиять на логику приложения. В вашем примере пересчет будет сброшен checkedItems , в то время как пересчет не выполняется. Чтобы устранить проблему, отложите получение значений до тех пор, пока вам не понадобится производная.

   const [checkedItems, setCheckedItems] = useState(() => propFromApp.map((item) => item.type))

  useEffect(() => {
    const allTypes = propFromApp.map((item) => item.type)
    setCheckedItems(allTypes)
  }, [propFromApp])
  

В качестве альтернативы, если у вас много dep, может стать неоправданно обременительным столь тщательное управление dep. Затем вы можете явно отключить обратный вызов эффекта, если не произошло ожидаемое вами изменение.

   const prevPropFromApp = useRef(propFromApp)
  const propFromAppChanged = prevPropFromApp.current !== (prevPropFromApp.current = propFromApp)
  useEffect(() => {
    if (!propFromAppChanged) return;
    setCheckedItems(allTypes)
  }, [allTypes, propFromAppChanged])
  

Комментарии:

1. привет, спасибо за ответ. Я действительно думал о вашем первом решении, но для этого требуется дважды написать propFromApp.map((item) => item.type логику. Я не уверен, понимаю ли я, что вы имели в виду, говоря «использовать useMemo для изменения логики приложения нехорошо».. Вы имели в виду, что в моем случае я изменил логику приложения с помощью useMemo ? Также не могли бы вы привести пример изменения логики приложения с помощью useMemo ?

2. Пока требуемое вам преобразование является чистым, как отображение в вашем примере, вы можете просто написать вспомогательную функцию для него, чтобы избежать дублирования. Я действительно был неясен в своей формулировке, я отредактирую ответ. Я хотел сказать, что это не подходит для useMemo пересчета или не пересчитывания, имеющего какую-либо разницу в терминах логики приложения. В вашем примере это имеет значение (сброс или не сброс), и поэтому это не нормально и вызывает ошибки.

Ответ №2:

Просто предложение, но я бы пропустил useMemo и просто установил оператор If вокруг обоих ваших useEffects. Это гарантирует, что useEffect обновляется только тогда, когда ваши условия действительны.

Пример в приложении:

 useEffect(() => {

  if(!myState) {
    setTimeout(() => {...
  

Пример в дочернем:

 useEffect(() => {

    if(!checkedItems amp;amp; allTypes) {
      setCheckedItems(allTypes)
    }
  }, [allTypes, checkedItems])
  

Комментарии:

1. спасибо за ответ, но я не думаю, что ваше решение здесь правильное. checkedItems является массивом и ! checkedItems всегда будет возвращать false .