Lately, I have been doing a lot of refactoring work, and I have come to observe some patterns when converting class components to their functional equivalent in React.
Let's take a look at an example class component and my approach to converting it to a functional one.
Example
class MyComponent extends React.Component {
constructor (props) {
super (props)
this.state = {
count: props.initialCount,
countB: 0,
showComponent: false
}
}
increment () {
this.setState({
count: this.state.count + 1
})
}
render() {
const {showComponent} = this.state
return <div>
<span>Hello World</span>
{showComponent && <MyComponent2 />}
</div>;
}
}
export default MyComponent
While a bit contrived the example above gives us a good baseline for most of the class components that I have come across.
Step 1 - Convert class declaration into a function declaration and export it directly
export function MyComponent () {
constructor (props) {
super (props)
this.state = {
count: props.initialCount,
countB: 0,
showComponent: false
}
}
increment () {
this.setState({
count: this.state.count + 1
})
}
render() {
const {showComponent} = this.state
return <div>
<span>Hello World</span>
{showComponent && <MyComponent2 />}
</div>;
}
}
As can be seen above I converted the first line to reflect a function and also converted the component to a named
export (personal preference).
Step 2 - Extract state into hooks
There are two main ways to go about it. Either we extract each state property into a React.useState
hook, or we extract the entire state into a React.useState
hook or we create a single React.useState
hook. In this case I am going to with the latter
export function MyComponent ({initialCount}) {
const [state, setState] = React.useState({
count: initialCount,
countB: 0,
showComponent: false
})
increment () {
this.setState({
count: this.state.count + 1
})
}
render() {
const {showComponent} = this.state
return <div>
<span>Hello World</span>
{showComponent && <MyComponent2 />}
</div>;
}
}
Two things happened above.
- First I extracted the state into a
React.useState
hook. This has the benefit of needing minimal changes to the components in terms of state as the acessibility of state happens using thestate
object as in class components - I destructured the props variable directly into the function arguments
Step 3 - Declare class methods to be functions
export function MyComponent ({initialCount}) {
const [state, setState] = React.useState({
count: initialCount,
countB: 0,
showComponent: false
})
function increment () {
this.setState({
count: this.state.count + 1
})
}
render() {
const {showComponent} = this.state
return <div>
<span>Hello World</span>
{showComponent && <MyComponent2 />}
</div>;
}
}
This should be an intiuitive one. Essentially we add the keyword function
to every class method declaration
Step 4 - Remove the render method
export function MyComponent ({initialCount}) {
const [state, setState] = React.useState({
count: initialCount,
countB: 0,
showComponent: false
})
function increment () {
this.setState({
count: this.state.count + 1
})
}
const {showComponent} = this.state
return <div>
<span>Hello World</span>
{showComponent && <MyComponent2 />}
</div>;
}
As we know (or not) functional components don't have a render method. So I removed it.
Step 5 - Update setState to include previous state
export function MyComponent ({initialCount}) {
const [state, setState] = React.useState({
count: initialCount,
countB: 0,
showComponent: false
})
function increment () {
this.setState(prev => ({
...prev,
count: prev.count + 1
}))
}
const {showComponent} = this.state
return <div>
<span>Hello World</span>
{showComponent && <MyComponent2 />}
</div>;
}
A key different between this.setState
and React.useState is that in class components this.setState
updates the state partially, whereas the React.useState
setState overwrites the state.
Hence, to avoid that, we can expose and include the previous state like above.
Step 6 - Remove all this
references
export function MyComponent ({initialCount}) {
const [state, setState] = React.useState({
count: initialCount,
countB: 0,
showComponent: false
})
function increment () {
setState(prev => ({
...prev,
count: prev.count + 1
}))
}
return <div>
<span>Hello World</span>
{state.showComponent && <MyComponent2 />}
</div>;
}
And finally, we have a functional component equivalent to the class component we started with.
Conclusion
I have tried this 6-step in several occasions and it worked like a treat! Also of note is that the order of steps is not that important. I will let you be the judge though.