The Container/Presentational Pattern with React and Vue
9th January 2025 • 6 min read — by Aleksandar Trpkovski
The Container/Presentational Pattern is a fundamental concept in frontend development. By separating logic and UI, it creates clean, reusable, and testable components. Traditionally, this pattern was implemented using "container components" to manage state and "presentational components" to handle UI rendering. However, modern tools like React Hooks and Vue Composables provide an alternative, more modular way to achieve the same goals, without relying on explicit container components.
In this blog post, we’ll:
- Explore the traditional implementation of the Container/Presentational Pattern in React and Vue.
- Enhance the pattern using Hooks and Composables for a cleaner, reusable design approach.
Introduction to the Container/Presentational Pattern
The Container/Presentational Pattern divides components into two types:
- Presentational Components:
- Focus on rendering the UI.
- Receive data via props.
- Are stateless and reusable across different contexts.
- Container Components:
- Manage state, handle logic, and perform data fetching.
- Pass data to presentational components.
This separation of concerns promotes maintainability and reusability. However, in modern frameworks like React and Vue, container components can be replaced by Hooks and Composable functions, encapsulating logic in reusable, framework-specific utilities.
React Implementation with Traditional Container Components
Presentational Component - Card.jsx
The Card
component in React is focused purely on displaying user information:
function Card({ data }) {
return (
<div className="card">
<h2>{data.name}</h2>
<p>Age: {data.age}</p>
</div>
);
}
export default Card;
Presentational Component - Details.jsx
Similarly, the Details
component handles additional user details:
function Card({ data }) {
return (
<div className="card">
<p>Email: {data.email}</p>
<p>Address: {data.address}</p>
<p>City: {data.city}</p>
<p>State: {data.state}</p>
<p>Zip: {data.zip}</p>
<p>Phone: {data.phone}</p>
<p>Occupation: {data.occupation}</p>
<p>Company: {data.company}</p>
<p>Hobbies: {data.hobbies?.join(", ")}</p>
<p>Website: {data.website}</p>
</div>
);
}
export default Card;
Container Component - DataContainer.jsx
The DataContainer
component manages data fetching and distributes the fetched data to child components.
import { useState, useEffect, Children, cloneElement } from "react";
function DataContainer({ children }) {
const [data, setData] = useState(null);
useEffect(() => {
// Simulating data fetching
setTimeout(() => {
setData({
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
phone: "555-1234",
occupation: "Software Developer",
company: "Tech Corp",
hobbies: ["reading", "gaming", "hiking"],
website: "https://johndoe.com",
});
}, 1000);
}, []);
return <>{children && Children.map(children, (child) => cloneElement(child, { data }))}</>;
}
export default DataContainer;
Using the Components Together
import Card from "./components/Card";
import Details from "./components/Details";
import DataContainer from "./components/DataContainer";
function App() {
return (
<>
<DataContainer>
<Card />
<Details />
</DataContainer>
</>
);
}
export default App;
Vue 3 Implementation with Traditional Container Components
Presentational Component - Card.vue
The Card
component in Vue focuses on rendering basic user information:
<script setup>
import { defineProps } from "vue";
defineProps({
data: Object,
});
</script>
<template>
<div class="card">
<h2>{{ data.name }}</h2>
<p>Age: {{ data.age }}</p>
</div>
</template>
Presentational Component - Details.vue
The Details
component renders additional user details:
<script setup>
import { defineProps } from "vue";
defineProps({
data: Object,
});
</script>
<template>
<div class="card">
<p>Email: {{ data.email }}</p>
<p>Address: {{ data.address }}</p>
<p>City: {{ data.city }}</p>
<p>State: {{ data.state }}</p>
<p>Zip: {{ data.zip }}</p>
<p>Phone: {{ data.phone }}</p>
<p>Occupation: {{ data.occupation }}</p>
<p>Company: {{ data.company }}</p>
<p>Hobbies: {{ data.hobbies?.join(", ") }}</p>
<p>Website: {{ data.website }}</p>
</div>
</template>
Container Component - DataContainer.vue
The DataContainer
encapsulates data fetching and passes it to its slot:
<script setup>
import { ref, onMounted } from "vue";
const data = ref(null);
onMounted(() => {
// Simulating data fetching
setTimeout(() => {
data.value = {
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
phone: "555-1234",
occupation: "Software Developer",
company: "Tech Corp",
hobbies: ["reading", "gaming", "hiking"],
website: "https://johndoe.com",
};
}, 1000);
});
</script>
<template>
<slot :data="data"></slot>
</template>
Using the Components Together
<script setup>
import DataContainer from "./components/DataContainer.vue";
import Card from "./components/Card.vue";
import Details from "./components/Details.vue";
</script>
<template>
<DataContainer v-slot="{ data }">
<Card :data="data" />
<details :data="data" />
</DataContainer>
</template>
Enhancing the Pattern with Modern Approaches
Using React Hooks
Encapsulate logic with a custom hook:
import { useState, useEffect } from "react";
function useData() {
const [data, setData] = useState(null);
useEffect(() => {
setTimeout(() => {
setData({
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
phone: "555-1234",
occupation: "Software Developer",
company: "Tech Corp",
hobbies: ["reading", "gaming", "hiking"],
website: "https://johndoe.com",
});
}, 1000);
}, []);
return { data };
}
export default useData;
Using the React Custom Hook in a Component
import Card from "./components/Card";
import Details from "./components/Details";
import useData from "./components/useData";
function App() {
const { data } = useData();
return (
<>
<Card data={data} />
<Details data={data} />
</>
);
}
export default App;
Using Vue 3 Composables
Similarly, encapsulate logic with a composable function:
import { ref, onMounted } from "vue";
const data = ref(null);
export default function useData() {
onMounted(() => {
setTimeout(() => {
data.value = {
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
phone: "555-1234",
occupation: "Software Developer",
company: "Tech Corp",
hobbies: ["reading", "gaming", "hiking"],
website: "https://johndoe.com",
};
}, 1000);
});
return { data };
}
Using the Vue Composable in a Component
<script setup>
import Card from "./components/Card.vue";
import Details from "./components/Details.vue";
import useData from "./components/useData";
const { data } = useData();
</script>
<template>
<Card :data="data" />
<details :data="data" />
</template>
Conclusion
The Container/Presentational Pattern remains a powerful approach for separating UI and logic in modern frontend development. While traditional container components are effective, React Hooks and Vue 3 Composables provide an even more elegant solution through modular, reusable functions. By moving logic into dedicated functions like useData
, we gain several advantages:
- Better reusability across components
- Cleaner code with less boilerplate
- Improved testing and scalability
For modern frontend projects, embracing React Hooks or Vue 3 Composables can significantly enhance your development workflow and align with contemporary component design principles.
The code for this is available in the following GitHub repository here.