Mimic React life cycle methods with Hooks

- 239 views

Until somewhat recently, if you wanted to use state in React you had to use a class component extending from either React.Component or React.PureComponent. The release of React 16.8 brought hooks which allowed state to be used in functional components.

If you wanted to do something like converting an existing class component to a functional component or fetch data in a functional component, you might be wondering how we can bring over the functionality of the life cycle methods. Three of the more popular methods, namely componentDidMount, componentWillUnmount and componentDidUpdate, can all be implemented with a single hook, useEffect.

componentDidMount

Say we have a component like this.

import React from "react";
 
class App extends React.Component {
  constructor(props) {
    super(props);
 
    this.state = {
      posts: [],
    };
  }
 
  componentDidMount = async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts",
    ).then((response) => response.json());
 
    this.setState({ posts: response });
  };
 
  render = () => {
    return (
      <div>
        {this.state.posts.map((post) => (
          <div key={post.id}>
            <h1>{post.title}</h1>
 
            <p>{post.body}</p>
          </div>
        ))}
      </div>
    );
  };
}
 
export default App;

The example above is relatively straightforward. We fetch a list of posts once the component loads, the state with response and list them out.

If we write the same thing as a functional component.

import React from "react";
 
const App = () => {
  const [posts, setPosts] = React.useState([]);
 
  React.useEffect(async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts",
    ).then((response) => response.json());
 
    setPosts(response);
  }, []);
 
  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>
          <h1>{post.title}</h1>
 
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
};
 
export default App;

Basically all we’re doing is taking the code that was inside componentDidMount and running it inside an effect.

The thing to remember with useEffect if that they run after each render. The second argument in useEffect is used to control when to run the effect. The argument is an array of states which to run the effect after one of them is updated. To make sure the effect only runs once, an empty array is passed as the argument.

If you don’t set the 2nd argument in a useEffect where the state is updated, an infinite loop will occur.

While the above effect will run as is, React will show a warning saying that “An effect function must not return anything besides a function, which is used for clean-up.” since our effect returns a Promise. To fix this, we’ll move the data fetching code to an asynchronous function outside the effect.

const fetchData = async () => {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/posts",
  ).then((response) => response.json());
 
  setPosts(response);
};
 
React.useEffect(() => {
  fetchData();
}, []);

The final code will look like this.

import React from "react";
 
const App = () => {
  const [posts, setPosts] = React.useState([]);
 
  const fetchData = async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts",
    ).then((response) => response.json());
 
    setPosts(response);
  };
 
  React.useEffect(() => {
    fetchData();
  }, []);
 
  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>
          <h1>{post.title}</h1>
 
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
};
 
export default App;

componentWillUnmount

To show how we can implement componentWillUnmount with hooks, let’s consider the following example where we create a event listener to check for the window dimensions after the component has mounted and removing the listener when the component is about to unmount.

import React from "react";
 
export default class App extends React.Component {
  state = { width: 0, height: 0 };
 
  updateDimensions = () => {
    this.setState({ width: window.innerWidth, height: window.innerHeight });
  };
 
  componentDidMount = () => {
    window.addEventListener("resize", this.updateDimensions);
  };
 
  componentWillUnmount = () => {
    window.removeEventListener("resize", this.updateDimensions);
  };
 
  render = () => {
    return (
      <span>
        Window size: {this.state.width} x {this.state.height}
      </span>
    );
  };
}

First let’s create the functional component with just the state and jsx.

const App = () => {
  const [width, setWidth] = React.useState(0);
  const [height, setHeight] = React.useState(0);
 
  return (
    <span>
      Window size: {width} x {height}
    </span>
  );
};
 
export default App;

Next we’ll create the function used to update the states.

const updateDimensions = () => {
  setWidth(window.innerWidth);
  setHeight(window.innerHeight);
};

After that we’ll create an event listener using useEffect like we did using componentDidMount in the class component.

React.useEffect(() => {
  window.addEventListener("resize", updateDimensions);
}, []);

Notice how we set the second argument as an empty array for useEffect to make sure is runs only once.

Once we set up the event listener its important that we remember to remove the listener when needed to prevent any memory leaks. In the class component this was done in componentWillUnmount. We can achieve the same thing in hooks with the cleanup functionality in useEffect. useEffect can return a function which will be run when it is time to cleanup when the component unmounts. So we can remove the listener here.

React.useEffect(() => {
  window.addEventListener("resize", updateDimensions);
 
  return () => {
    window.removeEventListener("resize", updateDimensions);
  };
}, []);

The return of useEffect being reserved for the cleanup function is the reason why we got an error in the componentDidMount example initially when when we made the function inside useEffect async as it was returning a Promise.

The final code will be like this.

import React from "react";
 
const App = () => {
  const [width, setWidth] = React.useState(0);
  const [height, setHeight] = React.useState(0);
 
  const updateDimensions = () => {
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);
  };
 
  React.useEffect(() => {
    window.addEventListener("resize", updateDimensions);
 
    return () => {
      window.removeEventListener("resize", updateDimensions);
    };
  }, []);
 
  return (
    <span>
      Window size: {width} x {height}
    </span>
  );
};
 
export default App;

componentDidUpdate

Finally for componentDidUpdate let’s look at this component.

import React from "react";
 
export default class App extends React.Component {
  state = {
    id: 1,
    post: {},
  };
 
  getPost = async (id) => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${id}`,
    ).then((response) => response.json());
 
    this.setState({ post: response });
  };
 
  setId = (id) => this.setState({ id });
 
  componentDidMount = () => {
    this.getPost(this.state.id);
  };
 
  componentDidUpdate = (prevProps, prevState) => {
    if (this.state.id !== prevState.id) {
      this.getPost(this.state.id);
    }
  };
 
  render = () => {
    return (
      <div>
        <span>
          <button
            disabled={this.state.id === 1}
            onClick={() => this.setId(this.state.id - 1)}
          >
            -
          </button>
          {this.state.id}
          <button
            disabled={this.state.id === 100}
            onClick={() => this.setId(this.state.id + 1)}
          >
            +
          </button>
        </span>
 
        <h1>{`${this.state.post.id} - ${this.state.post.title}`}</h1>
 
        <p>{this.state.post.body}</p>
      </div>
    );
  };
}

In the above example we fetch a post once when the component mounts in componentDidMount and then every time the id gets updated in componentDidUpdate.

To get started on turning this into a functional component, let’s first write the following code declaring the states and the returning jsx.

import React from "react";
 
const App = () => {
  const [id, setId] = React.useState(1);
  const [post, setPost] = React.useState({});
 
  return (
    <div>
      <span>
        <button disabled={id === 1} onClick={() => setId(id - 1)}>
          -
        </button>
        {id}
        <button disabled={id === 100} onClick={() => setId(id + 1)}>
          +
        </button>
      </span>
 
      <h1>{`${post.id} - ${post.title}`}</h1>
 
      <p>{post.body}</p>
    </div>
  );
};

Then let’s declare a function to retrieve a post.

const getPost = async (id) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`,
  ).then((response) => response.json());
 
  setPost(response);
};

Next we need to think about retrieving the first post when the component mounts. We can do this in a useEffect with the second argument being an empty array.

React.useEffect(() => {
  getPost(id);
}, []);

The component should load a new post when the ID is changed. The second argument in useEffect is a list of states for which the effect should run when once of them updates. So to run the effect again when the ID changes, we can add the ID to the array.

React.useEffect(() => {
  getPost(id);
}, [id]);

Finally your component should look like this.

import React from "react";
 
const App = () => {
  const [id, setId] = React.useState(1);
  const [post, setPost] = React.useState({});
 
  const getPost = async (id) => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${id}`,
    ).then((response) => response.json());
 
    setPost(response);
  };
 
  React.useEffect(() => {
    getPost(id);
  }, [id]);
 
  return (
    <div>
      <span>
        <button disabled={id === 1} onClick={() => setId(id - 1)}>
          -
        </button>
        {id}
        <button disabled={id === 100} onClick={() => setId(id + 1)}>
          +
        </button>
      </span>
 
      <h1>{`${post.id} - ${post.title}`}</h1>
 
      <p>{post.body}</p>
    </div>
  );
};
 
export default App;

As you can see we can take of the both the componentDidMount and componentDidUpdate functionality in one useEffect which cuts down on duplicating code.`

Wrapping up

I hope you find this post useful on how to achieve some of the functionality in class components in functional components. If you want to learn more about hooks, the React documentation has a great introduction to hooks here.