У нас есть стек кода с множеством устаревших компонентов 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 активно развивается, и новые версии языка регулярно включают новые функции и улучшения.
Я думаю, что и ваше первоначальное решение, и ваше «пришедшее» решение очень близки к правильному. Однако я не думаю, что локальное состояние необходимо, поэтому считаю, что ссылка на компонент/объект myJQueryComponent
может храниться в другой ссылке React.
myJQueryComponent
.useEffect
обратный вызов перехвата (аналог метода componentDidMount
жизненного цикла компонента класса React), чтобы создать экземпляр myJQueryComponent
и вернуть функцию очистки (аналогично методу componentWillUnmount
жизненного цикла компонента класса) для уничтожения текущего myJQueryComponent
объект, когда компонент размонтируется.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.
}, []);