React Hook “Gotcha’s”

A quick overview of where I’ve gotten hung up while using React Hooks.

Paul Sherman
3 min readNov 4, 2018

I’ve been playing around with React Hooks a little bit (both with Curi and React Router). The experience has mostly been smooth, but there are a few places where I have gotten hung up:

General Note: React Hooks are experimental, only released in alpha, and are subject to changing. This article (originally published Nov. 4, 2018) may quickly become outdated.

Testing Effects

Problem:

Effects are functions that are run after React finishes rendering.

function MyComponent() {
React.useEffect(() => {
console.log("rendered MyComponent");
});
return <div>My Component</div>;
}

Unfortunately, testing them doesn’t appear to be straightforward. The effect has a low priority and will not be called until after a render has been flushed.

“Solution”:

The useLayoutEffect hook is roughly equivalent to a React class’s componentDidMount and componentDidUpdate functions. Substituting useEffect with useLayoutEffect gets the tests to pass.

function MyComponent() {
React.useLayoutEffect(() => {
console.log("rendered MyComponent");
});
return <div>My Component</div>;
}

This is a “solution”, not a solution, since useEffect is probably a better choice for many implementations. Once testing useEffect is easier, I will hopefully be able to switch back to it.

Functions as State

Problem:

I had a component with a bit of state. It sets up an observer that gets passed a function, which I want to store in state. Unfortunately, every time I tried storing the function, it would be called immediately.

function MyComponent() {
const [fn, setFn] = React.useState();
React.useLayoutEffect(() => {
return observe(newFn => {
// setting the new function will call the function
setFn(newFn);
});
});
return fn;
}

Solution:

If you want to store a function as state, you need to pass a function that returns the function to store (called “functional updates”). This was mostly an issue for me because I forgot that the “set” function returned by useState can be given a function.

function MyComponent() {
const [fn, setFn] = React.useState();
React.useLayoutEffect(() => {
return observe(newFn => {
// pass a function that returns the new function
setFn(() => newFn);
});
});
return fn;
}

This one was really my fault and I’m sure that the final documentation will make it more obvious how to store functions in state.

Setting State in Unmounted Components

Problem:

If you attempt to set a component’s state after it has unmounted, you will get an error message. This problem can crop up in asynchronous event handlers, which do not have a great way of knowing whether or not the component is still mounted.

function MyComponent() {
const [valid, setValid] = React.useState(true);
function validate(event) {
validateEvent(event)
.then(result => {
setValid(result);
});
}
return <Thing valid={valid} onChange={validate} />;
}

With a class component, you can use componentWillUnmount to set an instance variable and “guard” setState calls.

class Guarded extends React.Component {
state = { valid: true}
validate = event {
validateEvent(event)
.then(result => {
if (!this.removed) {
this.setState({ value: result });
}
});
}
render() {
return (
<Thing valid={this.state.valid} onChange={this.validate} />
);
}
componentWillUnmount() {
this.removed = true;
}
}

Solution:

Edit (Nov. 6): Instead of using state to store if a component is mounted, this should be done with a ref. Refs are closer to instance variables and don’t cause a re-render.

You can a ref and an effect to track if a component is mounted. An effect whose second argument is an empty array ([]) will only call its return method when the component unmounts. We can use this to set an “unmounted” state.

function MyComponent() {
const [valid, setValid] = React.useState(true);
const mounted = React.useState(true);
React.useEffect(() => {
return () => {
mounted.current = false;
}
}, []
function validate(event) {
validateEvent(event)
.then(result => {
if (mounted.current) {
setValid(result);
}
});
}
return <Thing valid={valid} onChange={validate} />;
}

--

--