React Components: From Class to Function
How to convert your existing class component into a function component.
Why Bother?
Class components aren’t going anywhere any time soon. Support is planned for the foreseeable future by the React team. So why bother converting your current Class components to Function components? Well, in a word, hooks. Hooks are only supported in Function components, use them in a class and you’ll be met with an Invalid Hook Call Warning.
Hooks can only be called inside the body of a function component.
Since hooks were released back with React 16.8, they’ve taken over the React ecosystem. From Redux, to React Router, to Apollo GraphQL, to React DnD, hooks are everywhere and for good reason. Prior to hooks, most of these packages leveraged Higher-Order Components (HOC). These worked by injecting additional props into your component. When using just a few HOC, this is completely manageable. However, when you add more and more, this quickly becomes a maintainability nightmare. The origin of props becomes less and less clear, and when debugging you’ll quickly run into HOC hell.
Class Component
Prior to hooks, Class components offered features that weren’t possible with Function components. If you needed local state or lifecycle methods, you had no choice but to use a Class component. Let's consider the following:
The Pokemon List component lists the first 151 Pokemon and allows the user to filter the list based on their search. Let's break this down piece by piece.
The first thing we do is initialize the state with pokemon set to an empty array and search to an empty string.
state = {
pokemon: [],
search: ''
};
In the constructor, we create a ref and make a call to the fetchData function. In fetchData we make a GET request to the pokeapi and store the results in state.
fetchData = () => {
fetch('https://pokeapi.co/api/v2/pokemon?limit=151')
.then(result => result.json())
.then(({ results }) => this.setState({ pokemon: results}))
.catch(e => console.error(e));
}
In the render method, we map over the pokemon creating our list.
{pokemonList.map(pokemon => <p key={pokemon.name}>{pokemon.name}</p>)}
Looking at the input component, we can see that the ref we created in the constructor is being used here.
<input id="search" ref={this.ref} onChange={this.onChange}></input>
We can now use the ref to automatically focus the input for the user in the componentDidMount method.
componentDidMount() {
this.ref.current.focus();
}
The input component is also passed an onChange function. In the onChange, we update the value for search in state with the user’s input.
onChange = (e) => {
this.setState({ search: e.target.value.toLowerCase() });
}
Finally, we use the value for search to filter the pokemon.
const { pokemon, search } = this.state;
const pokemonList = pokemon.filter(
pokemon => pokemon.name.includes(search)
);
Function Component
With the power of hooks, we can refactor this into a Function.
Let’s take a look at the refactored component.
Right away we get started with hooks. In fact, the first five things we do are all leveraging hooks. Its good practice to put all your hooks toward the top of a Function component to avoid breaking the rules of hooks. Lets first look at useState.
const [pokemon, setPokemon] = useState([]);
const [search, setSearch] = useState('');
Instead of a single state, we have two states. One for pokemon, and the other for search. The useState hook receives the initial value as an argument; an empty array for pokemon and an empty string for search. What gets returned from useState is an array. The first item is the current value in state. The second item is a function to update the value in state. We’re leveraging array destructuring to easily assign the consts.
For the ref, this is super easy. Simply call useRef and assign the result to a const.
const ref = useRef();
Next is the useEffect hook. When I first started using hooks, useEffect was the most intimidating one to start using. However, after some practice, you’ll see it’s actually a great alternative to componentDidMount/Update. Just like useState, we can have as many useEffects as we’d like. Let’s look at the first one.
useEffect(() => {
fetchData();
}, []);
useEffect is a function that receives two arguments. The first is the effect or function, that you’d like called. The second argument is a dependencies array. In the dependencies array, you put any values, that when changed, should trigger your effect to be ran. In this example, the array is empty. This is because we only want the effect to be ran once when the component mounts. In this way, the useEffect is very similar to componentDidMount. Note the way the useEffect is structured. You’d be forgiven for thinking you could refactor the effect to be a one-liner.
useEffect(fetchData, []);
This will result in a type error. The only valid return type for an effect, is a cleanup function.
In the next useEffect, we focus our input component.
useEffect(() => {
ref.current.focus();
}, [ref]);
Here we put the ref in the dependencies array. We do this because in the case where the ref changes, we would like to re-run our effect.
The last piece is updating state for pokemon and search.
const fetchData = () => {
fetch('https://pokeapi.co/api/v2/pokemon?limit=151')
.then(result => result.json())
.then(({ results }) => setPokemon(results))
.catch(e => console.error(e));
}const onChange = (e) => {
setSearch(e.target.value.toLowerCase());
}
Instead of calling setState, we call the appropriate update function. For pokemon we pass the response from the api to setPokemon, and for search, we pass the user input to setSearch. With good naming, it should make using state much easier than before.
Conclusion
Should you stop everything you're doing and immediately start converting all your class components to functions? Probably not; but if you were planning to refactor a Class component anyway, why not try making it a function? I think in the long run, you’ll be happy you did.
References
- Planned Class Support in React
- Invalid Hook Call Documentation
- React 16.8 Release Notes
- Redux Hooks Documentation
- React Router Hooks Documentation
- Apollo GraphQL Hooks Documentation
- React DnD Hooks Documentation
- React Higher-Order Components Documentation
- Rules of Hooks
- Array Destructuring MDN
- React useEffect With Cleanup Function Documentation