ReactJS — автосохранение с несколькими входами — лучшие практики

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

function App(props) {
  
  const timer = React.useRef(null);  
  const [data, setData] = React.useState(props.inputData);

  React.useEffect(() => {
    clearTimeout(timer.current)
    timer.current = setTimeout(() => {
      console.log("Saving call...", data)
    }, 1000)
  }, [data])

  const inputChangeHandler = (e, type) => {
    if (type === "first") {
      setData({ ...data, first: e.target.value })
    } else if (type === "second") {
      setData({ ...data, second: e.target.value })
    } else if (type === "third") {
      setData({ ...data, third: e.target.value })
    }
  }

  return (
    <>
      <div className="inputFields">
        <input 
          defaultValue={data.first} 
          type="text" 
          onChange={(e) => inputChangeHandler(e, "first")} 
        />
        <input 
          defaultValue={data.second} 
          type="text" 
          onChange={(e) => inputChangeHandler(e, "second")} 
        />
        <input 
          defaultValue={data.third} 
          type="text" 
          onChange={(e) => inputChangeHandler(e, "third")} 
        />
      </div>
      <ChildComponent data={data} />
    </>
  )
}

Я читал о debounce, но моя реализация не сработала. Кто-нибудь сталкивался с такой же проблемой?

Ниже приведена моя реализация debounce с использованием lodash:

React.useEffect(() => {
  console.log("Saving call...", data)
}, [data])

const delayedSave = React.useCallback(_.debounce(value => setData(value), 1000), []);

const inputChangeHandler = (e, type) => {
  if (type === "first") {
    let obj = { ...data };
    obj.first = e.target.value;
    delayedSave(obj)
  } else if (type === "second") {
    let obj = { ...data };
    obj.second = e.target.value;
    delayedSave(obj)
  } else if (type === "third") {
    let obj = { ...data };
    obj.third = e.target.value;
    delayedSave(obj)
  }
}

Проблема с этим заключается в том, что если пользователь вводит сразу (до задержки в 1 секунду) с первого ввода на второй, он сохраняет только последний ввод пользователя.


person Christakitos    schedule 03.08.2020    source источник
comment
Не сработало каким образом? Вы получаете ошибки?   -  person Guy Incognito    schedule 03.08.2020
comment
@GuyIncognito никаких ошибок, я печатал сразу с первого ввода на второй и ждал автосохранения, а оно сохранило только последнее.   -  person Christakitos    schedule 03.08.2020
comment
Поскольку setData является асинхронным, данные не будут обновляться немедленно.   -  person Beyond    schedule 03.08.2020
comment
Рассматривали ли вы реализацию debounce с помощью lodash?   -  person Glen Carpenter    schedule 03.08.2020
comment
Да @GlenCarpenter, моя реализация была с lodash. Я попытаюсь воссоздать его в приведенном выше примере и отредактирую свой вопрос.   -  person Christakitos    schedule 03.08.2020


Ответы (2)


Проблема в вашей реализации заключается в том, что таймер устанавливается в замыкании (в useEffect) с использованием data вашего компонента, который был до запуска таймера. Вы должны запустить таймер после изменения data (или newData в моем предложении по реализации). Что-то вроде:

function App(props) {
  const [data, setData] = React.useState(props.inputData);
  const { current } = React.useRef({ data, timer: null });

  const inputChangeHandler = (e, type) => {
    current.data = { ...current.data, [type]: e.target.value };

    if(current.timer) clearTimeout(current.timer);

    current.timer = setTimeout(() => {
      current.timer = null;
      setData(current.data);
      console.log("Saving...", current.data);
    }, 1000);
  }

  return (
    <>
      <input defaultValue={data.first} type="text" onChange={(e) => inputChangeHandler(e, "first")} />
      <input defaultValue={data.second} type="text" onChange={(e) => inputChangeHandler(e, "second")} />
      <input defaultValue={data.third} type="text" onChange={(e) => inputChangeHandler(e, "third")} />
    </>
  );
}
person Daniele Ricci    schedule 06.08.2020
comment
Спасибо, что ответили мне @Daniele, единственная проблема с этим заключается в том, что компонент повторно отображается при каждом нажатии клавиши, и я пытаюсь этого избежать. - person Christakitos; 06.08.2020
comment
Просто удалите setData(newData) из inputChangeHandler @Christakitos - person Daniele Ricci; 06.08.2020
comment
В приведенном выше коде вы используете newData={...data etc}, если вы удаляете setData каждый раз, когда вводите другое поле, он снова получит начальные значения. Это можно исправить с помощью refs, но дело в том, что мне также нужно, чтобы состояние обновлялось. Извините, я не указал это в своем описании. - person Christakitos; 06.08.2020
comment
Я не уверен, что с приведенным выше кодом поля сохраняют свои исходные значения @Christakitos, вы уверены, что в вашем коде нет value={data.first} в трех тегах <input>? Если это так, я предлагаю вам прочитать о неконтролируемых компонентах и прилагаемой ссылке о контролируемых компонентах. Vs неконтролируемый ввод; по своей природе контролируемый компонент перерисовывает при каждом нажатии клавиши - person Daniele Ricci; 06.08.2020
comment
Мои входы имеют defaultValue = {data.first} и так далее, если быть точным, но помимо этого, если вы меняете состояние при каждом нажатии kerystroke, не означает ли это, что компонент будет повторно отображаться? Мне интересно, можно ли обновлять состояние в функции тайм-аута, например, каждые 1 с после ввода. - person Christakitos; 07.08.2020
comment
Да, это возможно @Christakitos, но если вы обновите состояние повторного рендеринга компонента, вот как работает React.js... чтобы дать мне понять, что на самом деле происходит в вашем коде, это может быть хорошей идеей если вы обновите код в своем вопросе как свой реальный код. - person Daniele Ricci; 07.08.2020
comment
Я обновил свой код выше @Daniele, это именно то, что происходит в моем реальном коде. У меня есть компонент, который содержит 2 или более входных данных, значения по умолчанию которых исходят от родительского компонента. Этот компонент имеет дочерний компонент, который действует как предварительный просмотр входных данных. Я пытаюсь автоматически сохранить эти данные с задержкой в ​​​​1 с, а также обновить состояние с этой задержкой без повторного рендеринга при каждом нажатии клавиши. - person Christakitos; 07.08.2020
comment
Пожалуйста, попробуйте мой обновленный ответ @Christakitos; имейте в виду, что при обновлении состояния (после тайм-аута 1 с) React.js повторно отображает компонент. - person Daniele Ricci; 08.08.2020
comment
Я использовал это решение до @Daniele, и основная проблема с этим заключается в том, что если вы вводите первый ввод и сразу второй (до задержки 1 с), ваше обновленное состояние содержит только последние входные данные. Я использовал refs, чтобы обойти это (когда я сохранил второе состояние ввода, я также обновил состояние для двух других с их ref.current.value), но мне не очень нравится решение ref, - person Christakitos; 08.08.2020
comment
Вы правы @Christakitos, я не учел это в основной проблеме. Пожалуйста, проверьте мой обновленный ответ - person Daniele Ricci; 08.08.2020
comment
Спасибо, @Daniele, я ценю ваш вклад. Мне понравилась твоя идея. - person Christakitos; 08.08.2020

Вы должны установить идентификатор таймера равным нулю внутри обработчика таймера.

React.useEffect(()=>{
    clearTimeout(timer.current)
    timer.current = setTimeout(()=>{
      timer.current = null;
      console.log("Saving...",data)
    },1000)
},[data])
person Beyond    schedule 03.08.2020
comment
Не могли бы вы объяснить, что вы думаете об этом @Beyond? Недостаточно ли clearTimeout? - person Christakitos; 03.08.2020