We have a code stack with lots of legacy jQuery components. We’re moving toward React, and one of the steps is wrapping jQuery with React.
However, managing the state of a non-React child component like this doesn’t seem to be something that is commonly covered for useEffects. es-lint/exhaustive-deps
doesn’t like any of my solutions. I’ve reviewed https://overreacted.io/a-complete-guide-to-useeffect/ and React docs, but I’m still not sure what the right answer is.
The naive function component looks like this:
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} />;
};
In theory, I could move the entire helper inside the useEffect, but this turns into a mess very quickly and I’d like to avoid that. Following various guides, I arrived at this solution, with a useRef
to store the useCallback
.
const renderJQueryHelper = useCallback(() => { ..., [props]);
const helperRef = useRef(renderJQueryHelper);
useEffect(() => {
setJQueryComp(helperRef.current());
...
This works for helper functions, and I’ve already used this elsewhere. But it doesn’t cover JQueryComp, which I need to be able to call destroy. It also doesn’t handle cases where I do want to run the heavy render helper more often, like if the jQuery component crashes, or if anything else is more complicated. I feel like I must be missing something.
I’ll include example implementation of JQueryComp, as well as how this looks in a class component where it seems much simpler.
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)} />;
}
}
2
Answers
This is what I ended up with.
I think both your initial solution and your "arrived-at" solution are very close to being correct. I don’t think the local state is necessary though, so I believe the
myJQueryComponent
component/object reference can be stored in another React ref.myJQueryComponent
object.useEffect
hook callback (analogous to a React class component’scomponentDidMount
lifecycle method) to instantiate themyJQueryComponent
and return a cleanup function (analogous to a class component’scomponentWillUnmount
lifecycle method) to destroy the currentmyJQueryComponent
object when the component unmounts.useEffect
hook to handle the component lifecycle where theprops
value changes over time and is used as a dependency to trigger updating themyJQueryComponent
object (analogous to a class component’scomponentDidUpdate
lifecycle method).On the off-hand chance that the above doesn’t quite work and React still needs a bit of a kick to know it should rerender the DOM is fully updated and repainted you can force a component rerender if necessary.
Example: