Рендеринг компонента 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)} />;
  }
}

🤔 А знаете ли вы, что...
JavaScript активно развивается, и новые версии языка регулярно включают новые функции и улучшения.


1
50
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} />;
};

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

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.
}, []);