Рендеринг компонента jQuery с помощью React: как написать useEffects?

У нас есть стек кода с множеством устаревших компонентов jQuery. Мы движемся к React, и одним из шагов является объединение jQuery с React.

Однако управление состоянием дочернего компонента, не относящегося к React, похоже, не является чем-то, что обычно рассматривается в useEffects. es-lint/exhaustive-deps не нравится ни одно из моих решений. Я просмотрел https://overreacted.io/a-complete-guide-to-useeffect/ и документацию React, но до сих пор не уверен, какой ответ правильный.

Наивный функциональный компонент выглядит так:

const MyReactFunctionComponent = (props) => {
  const element = useRef(null);
  const [JQueryComp, setJQueryComp] = useState(null);

  const renderJQueryHelper = () => {
    // Not 1-1 props match, lot of transformation and helper functions
    const JQueryProps = { ...props };
    return new myJQueryComponent(JQueryProps, element.current);
  };

  useEffect(() => {
    // only heavy render on first mount
    setJQueryComp(renderJQueryHelper());
    return () => {
      JQueryComp.destroy();
    };
  }, []); // warn: missing deps 'JQueryComp' and 'renderJQueryHelper'
  
  // call update on every reRender, comp diffs the props itself.
  if (JQueryComp) {
    JQueryComp.update(props);
  }

  return <div ref = {element} />;
};

Теоретически я мог бы переместить весь помощник внутрь useEffect, но это очень быстро превращается в беспорядок, и мне бы хотелось этого избежать. Следуя различным руководствам, я пришел к этому решению с useRef для хранения useCallback.

  const renderJQueryHelper = useCallback(() => { ..., [props]);

  const helperRef = useRef(renderJQueryHelper);

  useEffect(() => {
    setJQueryComp(helperRef.current());
    ...

Это работает для вспомогательных функций, и я уже использовал это где-то еще. Но он не распространяется на JQueryComp, который мне нужен для уничтожения. Он также не обрабатывает случаи, когда я хочу чаще запускать помощник тяжелого рендеринга, например, если происходит сбой компонента jQuery или если что-то еще более сложное. Я чувствую, что, должно быть, что-то упускаю.

Я приведу пример реализации JQueryComp, а также то, как это выглядит в компоненте класса, где это кажется намного проще.

const myJQueryComponent = (props, element) => {
  const $element = $(element);
  $element.addClass('my-JQuery-component');

  const initialize = () => {
    // lots of JQuery code here, attaching containers, event listeners, etc.
    // eventually renders other JQuery components
  };

  const render = () => {
    if ($element.children().length > 0) {
      $element.trigger('JQuery_COMP_UPDATE', props);
    } else {
      initialize();
    }
  };

  this.update = _.debounce((newProps) => {
    if (newProps.type !== props.type) {
      this.destroy();
    }

    if (!_.isEqual(newProps, props)) {
      props = newProps;
      render();
    }
  }, 100);

  this.destroy = () => {
    $element.trigger('JQuery_COMP_DESTROY').empty();
  };

  render();
};

class MyReactClassComponent extends React.Component {
  renderJQueryHelper() {
    // Not 1-1 props match, lot of transformation and helper functions
    const JQueryProps = {...props}
    return new myJQueryComponent(JQueryProps, this.element);
  }

  componentDidMount() {
    this.JQueryComp = renderJQueryHelper();
  }

  componentDidUpdate() {
    if (!this.JQueryComp) {
      // JQuery comp crashed?
      this.JQueryComp = renderJQueryHelper
    }

    this.JQueryComp.update(this.props);
  }

  componentWillUnmount() {
    if (this.JQueryComp) {
      this.JQueryComp.destroy();
    }
  }

  render() {
    return <div ref = {(element) => (this.element = element)} />;
  }
}
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
0
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, что и ваше первоначальное решение, и ваше «пришедшее» решение очень близки к правильному. Однако я не думаю, что локальное состояние необходимо, поэтому считаю, что ссылка на компонент/объект myJQueryComponent может храниться в другой ссылке React.

  1. Создайте вторую ссылку React для хранения ссылки на объект myJQueryComponent.
  2. Используйте монтирование (пустой массив зависимостей, чтобы эффект выполнялся ровно один раз) useEffect обратный вызов перехвата (аналог метода componentDidMount жизненного цикла компонента класса React), чтобы создать экземпляр myJQueryComponent и вернуть функцию очистки (аналогично методу componentWillUnmount жизненного цикла компонента класса) для уничтожения текущего myJQueryComponent объект, когда компонент размонтируется.
  3. Используйте второй хук useEffect для обработки жизненного цикла компонента, где значение props меняется со временем и используется в качестве зависимости для запуска обновления объекта myJQueryComponent (аналогично методу жизненного цикла componentDidUpdate компонента класса).
const MyReactFunctionComponent = (props) => {
  const elementRef = useRef(null);
  const jQueryCompRef = useRef();

  useEffect(() => {
    const jQueryProps = { ...props };

    jQueryCompRef.current = new myJQueryComponent(
      JQueryProps,
      elementRef.current
    );

    return () => {
      jQueryCompRef.current.destroy();
    };
    // NOTE: mounting effect only
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    jQueryCompRef.current.update(props);
  }, [props]);
  
  return <div ref = {element} />;
};

Если есть вероятность, что вышеописанное не совсем работает, и React все еще нужно немного поработать, чтобы понять, что он должен выполнить повторную отрисовку, DOM полностью обновлен и перерисован, при необходимости вы можете принудительно выполнить повторную отрисовку компонента.

Пример:

const useForceRerender = () => {
  const [, setState] = useState(false);

  // useCallback is used to memoize a stable callback
  return useCallback(() => setState(c => !c), []);
};
const MyReactFunctionComponent = (props) => {
  const elementRef = useRef(null);
  const jQueryCompRef = useRef();

  const forceRerender = useForceRerender();

  useEffect(() => {
    const jQueryProps = { ...props };

    jQueryCompRef.current = new myJQueryComponent(
      JQueryProps,
      elementRef.current
    );

    return () => {
      jQueryCompRef.current.destroy();
    };
    // NOTE: mounting effect only
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    jQueryCompRef.current.update(props);
    forceRerender();
  }, [forceRerender, props]);
  
  return <div ref = {element} />;
};

Ого, тебе действительно разрешено делать такие вещи? Эээ, действительно ли рендеринг такого компонента jQuery достаточно необычен, чтобы заслуживать такого нарушения правил React? Мне действительно сказали не игнорировать исчерпывающие сведения. Думаю, я понимаю переход на useRef в этом случае, нам не нужны функции useState, и ясно, когда я буду использовать каждую из них, но useForceRerender кажется, что это может быть огромным анти-шаблоном. Стоит ли использовать это только в случаях, очень похожих на этот?

Isaiah Shiner 28.08.2024 17:59

@IsaiahShiner Какие правила React здесь нарушаются? Вы имеете в виду использование пустого массива зависимостей для запуска эффекта ровно один раз? Правила проверки React-hooks — это лишь самый широкий набор, который обычно дает желаемый результат, но то, что вы передаете в массиве зависимостей, контролирует, когда срабатывает эффект. Если вы хотите, чтобы он запускался только один раз, передайте пустой массив.

Drew Reese 28.08.2024 18:05

@IsaiahShiner Да, я полностью согласен с тем, что использование любого вида «принудительного повторного рендеринга» является общим антишаблоном React, но возможность делать это полезна в крайних случаях, подобных этому, когда вы напрямую работаете с библиотекой, отличной от React. это работает вне фреймворка React. Это «аварийный люк», вдохновленный методом forceUpdate старого компонента класса React. Jquery и React имеют довольно ортогональные подходы к управлению/потоку пользовательского интерфейса, они как масло и вода, поэтому иногда необходимо некоторое «ручное управление».

Drew Reese 28.08.2024 18:06

@IsaiahShiner В старых устаревших документах это описано немного лучше, чем в текущих документах, Legacy.reactjs.org/docs/…, прокрутите немного вниз, чтобы найти примечание в желтом поле, которое разъясняет это как вариант использования.

Drew Reese 28.08.2024 18:19

Да, да, это имеет гораздо больше смысла. Документы React отталкивают пользователей от аварийных люков, потому что большинству пользователей не следует их использовать. И они не так много упоминают о jQuery или других библиотеках, потому что случаев очень много. И хорошо обратиться к старым документам, я привык сразу переходить к новым документам, но у старых может быть другая точка зрения. Думаю, теперь я хочу сохранить набор инструментов для аварийных выходов и четко понимать, о чем думает React и что именно означает нарушение правил. Спасибо за помощь!

Isaiah Shiner 28.08.2024 18:51

@IsaiahShiner Добро пожаловать, рад помочь. Я надеюсь, что мой ответ здесь поможет решить ваш вопрос/проблему.

Drew Reese 28.08.2024 18:57

Я, вероятно, немного добавлю свой собственный ответ, чтобы дать четкий контекст того, что именно я решил сделать.

Isaiah Shiner 28.08.2024 19:04

Вот что у меня получилось.

const elementRef = useRef(null);
const jQueryCompRef = useRef(null);

// This ends up being redeclared every render, but that's fine.
const jQueryProps = someComplexHelperFunction(props);

useEffect(() => {
  if (!jQueryCompRef.current && elementRef.current) {
    jQueryCompRef.current = new myJQueryComponent(jQueryProps, elementRef.current);
  }
  // Turns out, I do want this to run every frame. If the component crashes,
  // this will attempt to recreate it.
}, [jQueryProps]);

useEffect(() => {
  return () => {
    jQueryCompRef.current?.destroy();
  };
  // But, I only want to destroy it on unmount. Otherwise you get screen flashing.
}, []);

Другие вопросы по теме