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.
So what is the Container/View pattern?
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.
Introduction of hooks in React
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.
Top comments (8)
The problem I see with this is that you don't need a hook here. What you had in the CardContainer was enough. By making this into a hook you are creating the need for added dependencies on your application such as react-hooks-compose, and also you need to bind each hook to a component in the app file. I dont think this is a scalable approach.
If you keep it as a container and move on, that works fine for you. If you need to make the same api call in a number of places and keep the same data across the application you need to re-architect this and maybe hooks wouldnt be the correct path for that in my opinion
This is likely going to be a rather noob question however I hadn’t seen it done before so I figured I’d ask. The ? after the element name, does that just cause it to disregard the index value? (Likely butchering my Js terminology here but hopefully the question makes sense)
Hello. The ? operator will check only for the existence of value(it's called optional chaining).
Ex: userData?.email will return value of email variable if it exists otherwise undefined.
Here is a good documentation on this subject:
developer.mozilla.org/en-US/docs/W...
Hope u find this useful.
Useful ⭐
I have a question about the your import path, how you do that?
I mean how you use "@" prefix to your folder? 🤔
Thank 😉 ✌️
Hello, I've achieved that by using craco with craco-alias.
You can find out more here:
npmjs.com/package/craco-alias
Thanks 🌺✌
Why did you import CardContainer in Composed Card while explaining the elimination of container ?
Just to show a side by side comparison that the result is the same.