27
Container/View Pattern in React inc.hooks
We at Ornio love clean, readable code. In order to achieve this we are in constant search for new techniques and methods to make our code as robust as possible.
Few years ago we switched from Ember to React. At first React seemed like strange unexplored territory where everything made sense and nothing did.
Questions started popping up. What's the best way to make a component ? When to make one ? How to keep them as reusable as possible ?
In search of answers I came across this article by Dan Abramov on Presentational and Container components. After reading it I instantly fell in love with the idea that it represented.
Container/View pattern (also known as Presentational/Container, Thick/thin, Smart/Dumb) is a technique of splitting components into 'Containers' which are responsible for any stateful logic and data fetching and 'Views' which are responsible for data presentation.
If used right this pattern allows for immense scaling options in React applications. By keeping views clean of any logic we can reuse them as much as we want. But also now that all our logic is contained inside a container it allows us for faster and easier debugging.
Here is a simple example on how to implement this pattern.
Let's start by creating our view component. In our case it will be a simple user card showing a profile picture, name, location, gender and email of a user.
import style from "./Card.module.css";
const Card = ({ title, location, email, gender, image }) => (
<section className={style.card}>
<img
className={style.cardImage}
src={image}
alt={title}
/>
<div className={style.cardContent}>
<h3 className={style.cardTitle}>{title}</h3>
<span className={style.cardLocation}>{location}</span>
<div className={style.cardContact}>
<span className={style.cardMail}>{`email: ${email}`}</span>
<span className={style.cardGender}>{`gender: ${gender}`}</span>
</div>
</div>
</section>
);
export default Card;
Now let's add some style to make it pretty.
.card {
display: flex;
align-self: center;
width: fit-content;
background: #ffffff;
box-shadow: 0px 2px 4px rgba(119, 140, 163, 0.06),
0px 4px 6px rgba(119, 140, 163, 0.1);
border-radius: 8px;
padding: 24px;
margin: 0 auto;
}
.cardImage {
height: 80px;
width: 80px;
border-radius: 100px;
}
.cardContent {
font-family: sans-serif;
line-height: 0;
margin-left: 20px;
}
.cardContact {
display: flex;
flex-direction: column;
}
.cardTitle {
font-size: 20px;
color: #112340;
margin-bottom: 20px;
}
.cardLocation {
font-size: 12px;
color: #112340;
margin-bottom: 22px;
opacity: 0.85;
}
.cardMail,
.cardGender {
font-size: 12px;
color: #112340;
margin-top: 15px;
opacity: 0.65;
}
Voila. Our card is finished and ready to use.
Now here is where the magic happens. We are going to create a new component called CardContainer. Inside this component is where the logic happens. We are going to fetch a user from a random user api and display data to our card.
import { useState, useEffect } from "react";
import axios from "axios";
import Card from "@components/Card";
const CardContainer = () => {
const [userData, setUserData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios("https://randomuser.me/api/");
const user = result.data.results[0];
setUserData({
gender: user.gender,
email: user.email,
location: `${user.location.city}, ${user.location.country}`,
title: `${user.name.title}. ${user.name.first} ${user.name.last}`,
image: user.picture.thumbnail,
});
};
fetchData();
}, []);
return (
<Card
title={userData?.title || "N/A"}
location={userData?.location || "N/A"}
email={userData?.email || "N/A"}
gender={userData?.gender || "N/A"}
image={userData?.image || ""}
/>
);
};
export default CardContainer;
As you can see by isolating all the logic in the container our view component is clean and ready to be reused as many times as we wish.
As we can see from Dan's blog with the introduction of hooks there is no need to package components like this. Since hooks allow us to isolate logic inside them and then just call them on demand, the need for a container is slowly fading away.
But as great as hooks are, they do not solve every problem, hence the reason why this approach is still widely used.
First let's move our container logic to a custom hook called useUserData.
import { useState, useEffect } from "react";
import axios from "axios";
export const useUserData = () => {
const [userData, setUserData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios("https://randomuser.me/api/");
const user = result.data.results[0];
setUserData({
gender: user.gender,
email: user.email,
location: `${user.location.city}, ${user.location.country}`,
title: `${user.name.title}. ${user.name.first} ${user.name.last}`,
image: user.picture.thumbnail,
});
};
fetchData();
}, []);
return {
gender: userData?.gender || "N/A",
email: userData?.email || "N/A",
location: userData?.location || "N/A",
title: userData?.title || "N/A",
image: userData?.image || "",
};
};
Looks good right. Now our logic is inside a hook instead of a container.
But how do I mix them now ?
Well we can try making a wrapper.
Let's do that.
import { useUserData } from '@hooks/useUserData';
import Card from "@componets/Card";
const UserCardContainer = () => {
const {
title,
location,
email,
gender,
image,
} = useUserData();
return (
<Card
title={title}
location={location}
email={email}
gender={gender}
image={image}
/>
);
};
export default UserCardContainer;
Now isn't this just another container? This creates a new arbitrary division where now ur logic is separated in 3 different files.
To me this was a really hacky way and it just wasn't as clean as i was hoping for.
I loved the idea of hooks and the idea of container/view pattern so I wasn't ready to give up yet.
To the internet!
After some digging online I have found a solution in the form of a library called react-hooks-compose.
What this library allows us to do is compose our views with our custom hooks removing the need for a container.
Let's compose our useUserData hook and Card component.
import composeHooks from "react-hooks-compose";
import { useUserData } from "@hooks/useUserData";
import Card from "@components/Card";
import CardContainer from "@containers/CardContainer"
// composing card with our hook
const ComposedCard = composeHooks({ useUserData })(Card);
const App = () => {
return (
<div className="app">
<ComposedCard />
<CardContainer />
</div>
);
};
export default App;
Success at last 🎉 🎉
Personally I think that container/view pattern in any shape or form is a great way to separate the concerns and keep your code as reusable as possible.
We at Ornio love this approach and will continue to use it as it helped us scale faster and it made building and testing components so much easier.
Hope you found this article helpful.
27