How to use React useEffect hook with non-React API

Mulberg (Andrei C)
Mar 16 · 5 min read
Photo by Ryoji Iwata on Unsplash

Integrating code that uses a different paradigm from React into a React app may be tricky sometimes, especially when using useEffect API.

In this article, I want to share with you some gotchas that I ran into when working with the useEffect hook, and patterns that may be useful for handling these situations.

It is assumed that you know the basics of the React hooks API. If not, here is the link to the official documentation https://reactjs.org/docs/hooks-intro.html

Setup

First, let’s take a look at our simple React app:

import React from "react";

function App() {
const [counter, setCounter] = React.useState(0);

function increment() {
setCounter(state => state + 1);
}

return (
<React.Fragment>
<div>{counter}</div>
<button onClick={increment}>Increment</button>
</React.Fragment>
);
}

export default App;

Now, imagine that there is a non-React third-party library that we want to use:

function fakeCharts() {
let isInit = false;
let chartContainer;
// We can only subscribe to events in the init method
function init(container, eventListeners) {
if (isInit) {
// fakeCharts can be initialized only once

throw new Error("FakeCharts is already initialized");
}

isInit = true;
chartContainer = container;
// Set event listeners
Object.keys(eventListeners).forEach(eventName => {
chartContainer.addEventListener(
eventName,
eventListeners[eventName]
);
});
}

return { init };
}

export default fakeCharts;

Problem

We want to print the current value of the counter variable every time a fake chart element is clicked. To encapsulate FakeCharts we wrap it in a React component <FakeChartsWrapper />:

import React from "react";
import fakeCharts from "./fakeCharts";
const chart = fakeCharts();function FakeChartsWrapper({ onClickFakeCharts }) {
const containerRef = React.useRef();
React.useEffect(() => {
chart.init(containerRef.current, {
click: onClickFakeCharts
});
}, [onClickFakeCharts]);

return <div ref={containerRef}>Click me</div>;
}
import React from "react";
import FakeChartsWrapper from "./FakeChartsWrapper";

function App() {
const [counter, setCounter] = React.useState(0);

function increment() {
setCounter(state => state + 1);
}

function onClickFakeCharts() {
console.log(`FakeCharts: The counter value is ${counter}`);
}


return (
<React.Fragment>
<div>{counter}</div>
<button onClick={increment}>Increment</button>
<FakeChartsWrapper onClickFakeCharts={onClickFakeCharts} />
</React.Fragment>
);
}

The problem with that implementation is that as soon as the Increment button is clicked, a “FakeCharts is already initialized” error will be thrown. Let’s figure out why that is happening with a simple example:

function createFunction() { 
function a() { return 'Hello!'; }
return a;
}
const b = createFunction();
const c = createFunction();
console.log(b === c); // false

In the example above, despite the function a() having the same signature, a unique instance of a() is created every time createFunction() is called. Similarly, every time a functional React component is rendered it creates new instances of all the functions that are defined inside of this component (unless they are not memoized).

Therefore, every time the counter value changes, a new instance of onClickFakeCharts() is created. And that triggers a React.useEffect to be re-applied.

A naive solution to that would be to omit onClickFakeCharts() from the dependencies of useEffect:

React.useEffect(() => {
chart.init(containerRef.current, {
click: onClickFakeCharts
});
}, []);

That will guarantee that this effect is applied only once, after the component is mounted. However, there is a bug: after counter is incremented, every time FakeChartsWrapper is clicked only the initial value of counter is displayed:

> click "Increment"
> counter is incremented and is equal to 1
FakeCharts: The counter value is 0
> click "Increment"
> counter is incremented and is equal to 2
FakeCharts: The counter value is 0
> click "Increment"
> counter is incremented and is equal to 3
FakeCharts: The counter value is 0

That is not the expected behaviour. Back to the simple example:

function createFunction(state) { 
function a() { console.log(state); }
return a;
}
const b = createFunction(0);
const c = createFunction(1);
console.log(b === c); // false
b(); // 0
c(); // 1

The function a() encloses the local scope of createFunction(). The same way, onClickFakeCharts() encloses the scope of App(including the props, the state, and the other functions). So, any time and instance of onClickFakeCharts() is invoked, it uses a “snapshot” of the counter value.

When chart.init() is called, an instance of onClickFakeCharts() has counter == 0 in its scope. And the new instances of onClickFakeCharts() will never be set as an event handler, because useEffect does not have onClickFakeCharts() in the dependency list. So, every time FakeChartsWrapper is clicked, the initial value of counter will be displayed.

Lying to React about dependencies has bad consequences. Intuitively, this makes sense, but I’ve seen pretty much everyone who tries useEffect with a mental model from classes try to cheat the rules.

https://overreacted.io/a-complete-guide-to-useeffect/#dont-lie-to-react-about-dependencies

This problem is not limited to functions using React hooks. A class component may contain the same error

class FakeChartsWrapper extends React.Component {
componentDidMount() {
chart.init(containerRef.current, {
click: this.props.onClickFakeCharts
});
}
}

Solution

In case of subscribing to events it might be preferable to re-subscribe to an event with a new listener every time an instance of the handler function is changed:

/* Every time "eventHandlerFn" changes, unsubscribe from the mouseup event, and subscribe again with the new "eventHandlerFn" */React.useEffect(() => {
target.addEventListener("mouseup", eventHandlerFn);

return function cleanup() {
target.removeEventListener("mouseup", eventHandlerFn);
}
}, [eventHandlerFn]);

However, we may prefer not to do it that way, either for performance reasons or, as in the case with FakeCharts, because of some limitations.

For a class component it is easy to solve with a method that is bound to this

class FakeChartsWrapper extends React.Component {
constructor() {
this.handleClickFakeCharts = this.handleClickFakeCharts.bind(this);
}

handleClickFakeCharts() {
this.props.onClickFakeCharts()
}
componentDidMount() {
chart.init(containerRef.current, {
click: this.handleClickFakeCharts
});

That way, even if onClickFakeCharts() changes, handleClickFakeCharts() remains the same as well as this inside of it. Therefore, it will always call the most recent instance of the onClickFakeCharts() prop.

However, there is no this in a functional component. But we can achieve the same result with React.useRef

function FakeChartsWrapper({ onClick }) {
const containerRef = React.useRef();
const onClickRef = React.useRef(onClick);
React.useEffect(() => {
onClickRef.current = onClick;
}, [onClick]);
React.useEffect(() => {
const handler = () => onClickRef.current();
containerRef.current.addEventListener("click", handler);
}, []); // <--- This hook is called only once
return <button ref={containerRef}>Click me</button>;
}

useRef returns a mutable ref object whose .current property is initialized to the passed argument (onClick). The returned object will persist for the full lifetime of the component.
https://reactjs.org/docs/hooks-reference.html#useref

Every time after onClick is changed, the first hook updates the value of onClickRef.current . And the second hook creates an event handler. Since onClickRef is immutable, we don’t need to add it to the list of the hook dependencies. And every time handler is called, it calls the latest value of current property of onClickRef

Conclusion

When dealing with new tools it is important to get a deeper understanding of best practices around it, as well as fundamentals behind it (such as the rules of the language). The official documentation is your best friend. React hooks, for instance, is the same javascript that uses the same logic when it comes to the scope and context. But the mental model around hooks (and useEffect specifically) is different from working with class-based lifecycle hooks.

Thus, trying to cut corners, hack the solution or apply an old mental model to a new tool, may lead to hard-to-find bugs in your code.

Links

Build Galvanize

A window to the product, design, and engineering teams at…

Thanks to Alex Zherdev and Gwen Rossen

Mulberg (Andrei C)

Written by

Build Galvanize

A window to the product, design, and engineering teams at Galvanize, an enterprise SaaS company

More From Medium

More from Build Galvanize

More from Build Galvanize

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade