Визуализация дочерних компонентов вызывает нежелательные отключения

#reactjs #lifecycle

Вопрос:

Я работаю над приложением для планирования. На главной странице есть таблица, в которой строками являются проекты, столбцами-недели. Каждая ячейка — это рабочая нагрузка человека для соответствующего проекта и недели. В верхней части таблицы есть строка, в которой указана общая оставшаяся доступность за каждую неделю. Это выглядит так :

Скриншот приложения

Когда пользователь вводит рабочую нагрузку в поле ввода, значение автоматически сохраняется на сервере после небольшой задержки (отмена). Затем вызывается обратный вызов, чтобы запустить обновление итоговых данных за неделю в верхней части таблицы.

Вот иерархия компонентов для этой страницы:

 WorkPlanPage
 - ProjectWithWorkloads
   - WorkloadInput
 

Есть 2 проблемы с кодом, которые я покажу ниже :

  1. При вызове обратного вызова обновляется список выполняемых рабочих нагрузок WorkPlanPage , что запускает повторную визуализацию всех дочерних компонентов. При этом WorkloadInput все s размонтируются и монтируются заново. Если бы кто-то находился в процессе редактирования (очень быстрым) пользователем, редактирование и связанный с ним вызов на сервер были бы потеряны (отмена отмены отменяется).
  2. Я не могу предотвратить повторное отображение всех ячеек, потому что обратный вызов обновляется каждый раз при изменении рабочей нагрузки. Таким образом, запоминание не будет работать, потому что функция обратного вызова обновляется при каждом обновлении рабочей нагрузки. Эта проблема взаимоотношений родителей и детей с обратным вызовом кажется очень простой, и все же я не могу придумать способ избежать повторного отображения всех детей, что может быть проблемой, когда детей много (50 проектов, 15 недель), поэтому 750 полей ввода.

Вот код для 3 компонентов, содержащий только соответствующие биты:

Страница рабочего плана

 
const WorkPlanPage = () => {

  // ...

  const { data: leaves } = useQuery(
     ['leaves', { employeeId: employeeId, start: firstWeek, end: lastWeek }],
     () => requestEmployeeLeaves(employeeId, firstWeek, lastWeek
     { refetchOnWindowFocus: false, initialData: [], enabled: (employeeId > 0) amp;amp; (currentDate !== undefined) }
   );

  // this callback changes every time allWorkloads changes. The callback changes
  // allWorkloads which in turn triggers the re-rendering of all the children.
  const handleWorkloadChange = useCallback((updated: Workload) => {
    if (allWorkloads === undefined) {
      return;
    }
    // first remove the workload we're about to update
    let newWorkloads = allWorkloads.filter(workload => {
      return (
        workload.project !== updated.project ||
        workload.employee !== updated.employee ||
        workload.date !== updated.date
      );
    });
    newWorkloads.push(updated);

    queryClient.setQueryData(
      ['workloads', { employeeId: employeeId, start: firstWeek, end: lastWeek }],
      newWorkloads);
  }, [allWorkloads, employeeId, firstWeek, lastWeek, queryClient]);

 // ...

  return (
    <div>
      <div>{/* Other parts of the page omitted */}</div>

      <WeeklyAvailabilies
        workloads={allWorkloads ? allWorkloads : []}
        leaves={leaves ? leaves : []}
        weeks={weeks} />

       {displayedProjects?.map((project: Project) => (
         <ProjectWithWorkload
           key={project.id   "-"   employeeId}
           employeeId={employeeId}
           project={project}
           weeks={weeks}
           workloads={workloadsForProjects.get(project.id)}
           onWorkloadChange={handleWorkloadChange}
         />
       ))}
      </div>
    </div>
  );
}
 

ProjectWithWorkloads (rows)

 const ProjectWithWorkload = ({
 employeeId,
 project,
 weeks,
 workloads,
 onWorkloadChange
}) => {

  return (
    <div>
      <div>{/* Removed project header */}</div>
      <div>
        {workloads.map((workload: Workload) => (
          <div key={workload.project   workload.date}>
            <WorkloadInput
              project={project}
              workload={workload}
              onWorkloadChange={onWorkloadChanged}
            />
          </div>
        ))}
      </div>
    </div>
  );
}
 

Вход рабочей нагрузки (ячейка)

 const WorkloadInput = ({ project, workload, onWorkloadChange }) => {

  const [effort, setEffort] = useState<string>("");

  // ...

  const handleEffortChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const valueAsString = event.currentTarget.value;
    const valueAsNumber = parseFloat(valueAsString);

    setEffort(event.currentTarget.value);

    if (valueAsString === "") {
      debouncedSaveWorkload(0);
    } else if (!isNaN(valueAsNumber)) {
      debouncedSaveWorkload(valueAsNumber);
    }
  };

  const debouncedSaveWorkload = useMemo(() => 
    _.debounce(saveWorkload, 500), [saveWorkload]
  );

  // cancel debounce on unmounting
  useEffect(() => {
    return () => {
      debouncedSaveWorkload.cancel();
    }
  }, [debouncedSaveWorkload]);

  const saveWorkload = useCallback((newEffort: number) => {
    updateWorkload({
      ...workload, effort: newEffort 
    }).then((updatedWorkload) => {
      onWorkloadChange(updatedWorkload); // where the callback is called
    });
  };

  return (
    <div>
      <input
        type="text"
        value={workload}
        onChange={handleEffortChange}
      />
    </div>
  );
};

 

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

1. Для оптимизации вы хотите только повторно отобразить a cell или весь row ? И, судя по всему, в настоящее время он обновляет все строки, это правильно?

2. @MwamiTovi Я ожидал бы, что только строка, в которой была обновлена рабочая нагрузка, должна быть повторно отображена. Среди этой строки я ожидал бы, что только измененная ячейка будет повторно отображена. Любое повторное отображение других ячеек в настоящее время запускает отмену функции debounceSaveWorkload отмены . Это приведет к потере предстоящих сохранений на сервере. Это происходит, когда пользователь редактирует рабочую нагрузку в течение недели и быстро обновляет рабочую нагрузку на следующем этапе до завершения первого сетевого цикла.

3. @MwamiTovi И, кстати, меня не совсем беспокоит кажущееся отсутствие оптимизации. Меня больше беспокоит то, что полная повторная визуализация отменяет отмену во всех других ячейках с неожиданным эффектом, который я описал. Просто так получилось, что если только измененная клетка перерисовывается, я убиваю двух зайцев одним выстрелом. Но этого не может произойти (afai может видеть), потому handleWorkloadChange что обратный вызов меняется после каждого обновления , а также массив рабочей нагрузки WorkPlanPage , запускающий весь повторный рендеринг.

4. Хммм…. вы довольно хорошо объяснили, какова ваша цель здесь. Изучите его более внимательно и вернетесь с некоторыми предложениями.

5. Ждать… Если onWorkloadChange в cell компоненте есть на самом деле handleWorkloadChange … почему бы не сделать handleWorkloadChange так, чтобы это полностью обрабатывалось внутри cell компонента? Таким образом, компонент Parent и rows должен быть исключен из любых повторных визуализаций. Вы уже пробовали это сделать? И посмотрите на исключение allWorkloads (которое , я полагаю, должно быть каким-то state , верно?) handleWorkloadChange из cell компонента. Таким образом, вместо этого вы можете сохранить allWorkloads родительский компонент, но его следует изменять только в том случае, если вам нужно повторно отобразить Parent его .