Separation of Concerns: Split Your React Components into Containers and Views

Many frontend engineers probably have experienced the head-scratching moment when their React components become so gigantic that they keep losing their train of thought when navigating through their frontend code. To salvage readability, one of the most common practices is to split up the humongous React components into smaller pieces. Traditionally, they would do this by chopping up the JSX elements. This method can effectively improve the readability of React components whose sole responsibility is to display data. However, on top of displaying data, some React components are also responsible for retrieving data from the backend. The more effective way to make such React components readable is to separate them into Containers and Views. In this article, I will demonstrate how to divide a React component into a Container and a View based on the principle of separation of concerns.

What Are Containers and Views?

First, let me clarify what Containers and Views are respectively in charge of. A Container component is responsible for fetching data and maintaining the state. It does not concern itself with data presentation at all. On the other hand, a View component’s sole responsibility is to display data. It should not worry about how to retrieve data from the backend.

As you probably have noticed, there is a clear separation of concerns between Container and View components. Such an approach helps improve the overall code readability. When looking at Container components, we can concentrate on how we retrieve and update data. When going through View components, we can have a laser focus on the way we present data to our users.

How to Split a Component into a Container and a View?

In this section, I will demonstrate how to split a React component into a Container and a View component with a simple example. Let’s say we have a page-level component. It retrieves user information from the backend, displays the user information, and allows users to change their nickname.

 

import { useEffect, useState } from 'react';
import styles from './UserPage.module.css';

import { useEffect, useState } from 'react';
import styles from './UserPage.module.css';

type User = {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  avatarUrl: string;
  nickname: string;
};

type UserPageProps = {
  userId: number;
};

export function UserPage({ userId }: UserPageProps) {
  const [user, setUser] = useState(null);
  const [newNickname, setNewNickname] = useState('');

  // When we pass an empty dependency array to the `useEffect` hook, it will only execute the function we pass to it once upon the component's creation.
  useEffect(() => {
    fetch(`/api/v1/users/${userId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    })
      .then((res) => {
        return res.json();
      })
      .then((user: User) => {
        setUser(user);
      });
  }, []);

  const handleNicknameSubmit = () => {
    setUser(null);
    fetch(`/api/v1/users/${userId}/nickname`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        nickname: newNickname,
      }),
    })
      .then((res) => {
        return res.json();
      })
      .then((user: User) => {
        setUser(user);
      });
  };

  return user ? (
    <>
      
        First Name: {user.firstName}
        Last Name: {user.lastName}
      
      
        
      
      
        Nickname: 
         {
            setNewNickname(e.target.value);
          }}
        />
        Submit
      
    
  ) : (
    Loading...
  );
}

This page-level component first makes a GET request to /api/v1/users/${userId} to retrieve information about the current user on mount. Then, this component displays the information upon receiving it. This component also has an input for the current user to change their nickname. It triggers a POST request to /api/v1/users/${userId}/nickname to update the current user’s nickname when the user clicks the Submit button.

Throughout my career, I have seen many page-level components like this one, some even more complicated. They are all-encompassing, and you can find everything in one place. However, it is also easy to get lost when reading them because there’s simply too much going on in the same place.

Next, let’s split this page-level component into a Container and a View component to make it more readable.

 

import { useEffect, useState } from 'react';
import styles from './UserPage.module.css';

import { useEffect, useState } from 'react';
import styles from './UserPage.module.css';

type User = {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  avatarUrl: string;
  nickname: string;
};

type UserPageViewProps = {
  user: User;
  newNickname: string;
  setNewNickname: (newNickname: string) => void;
  handleNicknameSubmit: () => void;
};

function UserPageView({
  user,
  newNickname,
  setNewNickname,
  handleNicknameSubmit,
}: UserPageViewProps) {
  return (
    <>
      
        First Name: {user.firstName}
        Last Name: {user.lastName}
      
      
        
      
      
        Nickname: 
         {
            setNewNickname(e.target.value);
          }}
        />
        Submit
      
    
  );
}

type UserPageContainerProps = {
  userId: number;
};

export function UserPageContainer({ userId }: UserPageContainerProps) {
  const [user, setUser] = useState(null);
  const [newNickname, setNewNickname] = useState('');

  useEffect(() => {
    fetch(`/api/v1/users/${userId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    })
      .then((res) => {
        return res.json();
      })
      .then((user: User) => {
        setUser(user);
      });
  }, []);

  const handleNicknameSubmit = () => {
    setUser(null);
    fetch(`/api/v1/users/${userId}/nickname`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        nickname: newNickname,
      }),
    })
      .then((res) => {
        return res.json();
      })
      .then((user: User) => {
        setUser(user);
      });
  };

  if (!user) {
    return Loading...;
  }

  return (
    
  );
}

As you can see in the code above, we have extracted the code for displaying user information and made it into a new View component named UserPageView. As for the logic for retrieving user information and making an API call to change the user’s nickname, we have kept it in the original page-level component that we have renamed to UserPageContainer. When working on the UserPageView component, we can focus on what data it displays and the styling of the elements. When adjusting UserPageContainer, we do not have to get distracted by anything irrelevant to state maintenance or fetching and updating user information. Overall, it becomes easier to read and refactor the frontend code for this page.

In this article, I started by defining Containers and Views. Then, I explained why splitting React components into Containers and Views can improve readability. Finally, I demonstrated how to separate a React component into a Container and a View with a concrete example. It might be time-consuming to refactor your React components with this technique, but it can improve your and your coworkers’ work efficiency in the long term. Please give this refactoring technique a try when you have time!