It has been approximately one year since React v16.8 was released, marking the introduction of Hooks. Yet, there are still people used to React class components who still haven't experienced the full potential of this new feature, along with functional components, including myself. The aim of this article is to summarize and encompass the most distinguishable features of the class components, and respectively show their alternatives when using React hooks.
Functional components
Before we start with Hooks examples, we will shortly discuss functional components in case you aren't familiar. They provide an easy way to create new units without the need of creating a new class and extending React.Component
.
Note: Keep in mind that functional components have been part of React since it's creation.
Here is a very simple example of a functional component:
const Element = () => (
<div className="element">
My Element
</div>
);
And just like class components, we can access the properties. They are provided as the first argument of the function.
const Element = ({ text }) => (
<div className="element">
{text}
</div>
);
However, these type of components--while very convenient for simple UI elements--used to be very limited in terms of life cycle control, and usage of state. This is the main reason they had been neglected until React v16.8.
Component state
Let's take a look at the familiar way of how we add state to our object-oriented components. The example will represent a component which renders a space scene with stars; they have the same color. We are going to use few utility functions for both functional and class components.
createStars(width: number): Star[]
- Creates an array with the star objects that are ready for rendering. The number of stars depends on the window width.renderStars(stars: Star[], color: string): JSX.Element
- Builds and returns the actual stars markup.logColorChange(color: string)
- Logs when the color of the space has been changed.
and some less important like calculateDistancesAmongStars(stars: Star[]): Object
.
We won't implement these. Consider them as black boxes. The names should be sufficient enough to understand their purpose.
Note: You may find a lot of demonstrated things unnecesary. The main reason I included this is to showcase the hooks in a single component.
And the example:
Class components
class Space extends React.Component {
constructor(props) {
super(props);
this.state = {
stars: createStars(window.innerWidth)
};
}
render() {
return (
<div className="space">
{renderStars(this.state.stars, this.props.color)}
</div>
);
}
}
Functional components
The same can be achieved with the help of the first React Hook that we are going to introduce--useState
. The usage is as follows: const [name, setName] = useState(INITIAL_VALUE)
. As you can see, it uses array destructuring in order to provide the value and the set function:
const Space = ({ color }) => {
const [stars, setStars] = useState(createStars(window.innerWidth));
return (
<div className="space">
{renderStars(stars, color)}
</div>
);
};
The usage of the property is trivial, while setStars(stars)
will be equivalent to this.setState({ stars })
.
Component initialization
Another prominent limitation of functional components was the inability to hook to lifecycle events. Unlike class components, where you could simply define the componentDidMount
method, if you want to execute code on component creation, you can't hook to lifecycle events. Let's extend our demo by adding a resize listener to window
which will change the number of rendered stars in our space when the user changes the width of the browser:
Class components
class Space extends React.Component {
constructor(props) { ... }
componentDidMount() {
window.addEventListener('resize', () => {
const stars = createStars(window.innerWidth, this.props.color);
this.setState({ stars });
});
}
render() { ... }
}
Functional components
You may say: "We can attach the listener right above the return statement", and you will be partially correct. However, think about the functional component as the render
method of a class component. Would you attach the event listener there? No. Just like render
, the function of a functional component could be executed multiple times throughout the the lifecycle of the instance. This is why we are going to use the useEffect
hook.
It is a bit different from componentDidMount
though--it incorporates componentDidUpdate
, and componentDidUnmount
as well. In other words, the provided callback to useEffect
is executed on every update. Anyhow, you can have certain control with the second argument of useState
- it represents an array with the values/dependencies which are monitored for change. If they do, the hook is executed. In case the array is empty, the hook will be executed only once, during initialization, since after that there won't be any values to be observed for change.
const Space = ({ color }) => {
const [stars, setStars] = useState(createStars(window.innerWidth));
useEffect(() => {
window.addEventListener('resize', () => {
const stars = createStars(window.innerWidth, color);
setStars(stars);
});
}, []); // <-- Note the empty array
return (
...
);
};
Component destruction
We added an event listener to window
, so we will have to remove it on component unmount in order to save us from memory leaks. Respectively, that'll require keeping a reference to the callback:
Class components
class Space extends React.Component {
constructor(props) { ... }
componentDidMount() {
window.addEventListener('resize', this.__resizeListenerCb = () => {
const stars = createStars(window.innerWidth, this.props.color);
this.setState({ stars });
});
}
componentDidUnmount() {
window.removeEventListener('resize', this.__resizeListenerCb);
}
render() { ... }
}
Functional component
For the equivalent version of the class component, the useEffect
hook will execute the returned function from the provided callback when the component is about to be destroyed. Here's the code:
const Space = ({ color }) => {
const [stars, setStars] = useState(createStars(window.innerWidth));
useEffect(() => {
let resizeListenerCb;
window.addEventListener('resize', resizeListenerCb = () => {
const stars = createStars(window.innerWidth, color);
setStars(stars);
});
return () => window.removeEventListener('resize', resizeListenerCb);
}, []); // <-- Note the empty array
return (
...
);
};
An important remark
It is worth mentioning that, when you work with event listeners or any other methods that defer the execution in the future of a callback/function, you should take into account that the state provided to them is not mutable.
Taking the window
listener we use in our demo as an example; if we used thestars
state inside the callback, we would get the exact value at the moment of the definition (callback), which means that, when the callback is executed, we risk having a stale state.
There are various ways to handle that, one of which is to re-register the listener every time the stars are changed, by providing the stars
value to the observed dependency array of useEffect
.
Changed properties
We already went through useEffect
in the sections above. Now, we will briefly show an example of componentDidUpdate
. Let's say that we want to log the occurances of color change to the console:
Class components
class Space extends React.Component {
...
componentDidUpdate(prevProps) {
if (this.props.color !== prevProps.color) {
logColorChange(this.props.color);
}
}
...
}
Functional components
We'll introduce another useEffect
hook:
const Space = ({ color }) => {
...
useEffect(() => {
logColorChange(color);
}, [color]); // <-- Note that this time we add `color` as observed dependency
...
};
Simple as that!
Changed properties and memoization
Just as an addition to the example above, we will quickly showcase useMemo
; it provides an easy way to optimize your component when you have to perform a heavy calculation only when certain dependencies change:
const result = useMemo(() => expensiveCalculation(), [color]);
References
Due to the nature of functional components, it becomes hard to keep a reference to an object between renders. With class components, we can simply save one with a class property, like:
class Space extends React.Component {
...
methodThatIsCalledOnceInALifetime() {
this.__distRef = calculateDistancesAmongStars(this.state.stars);
}
...
}
However, here is an example with a functional component that might look correct but it isn't:
const Space = ({ color }) => {
...
let distRef; // Declared on every render.
function thatIsCalledOnceInALifetime() {
distRef = caclulateDistancesAmongStars(stars);
}
...
};
As you can see, we won't be able to preserve the output object with a simple variable. In order to do that, we will take a look at yet another hook named useRef
, which will solve our problem:
const Space = ({ color }) => {
...
const distRef = useRef();
function thatIsCalledOnceInALifetime() {
// `current` keeps the same reference
// throughout the lifetime of the component instance
distRef.current = caclulateDistancesAmongStars(stars);
}
...
}
The same hook is used when we want to keep a reference to a DOM element.
Conclusion
Hopefully this should give you a starting point when it comes to using React Hooks for the things that you are already used to doing with class components. Obviously, there are more hooks to explore, including the definition of custom ones. For all of that, you can head to the official docs. Give them a try and experience the potential of functional React!