container-presentational

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Container/Presentational Pattern

Container/Presentational Pattern

Table of Contents

目录

In 2015, Dan Abramov wrote an article titled "Presentational and Container Components" that changed the way many developers thought about component architecture in React. He introduced a pattern that separated components into two categories:
  1. Presentational Components (or Dumb Components): These are concerned with how things look. They don't specify how the data is loaded or mutated but rather receive data and callbacks exclusively via props.
  2. Container Components (or Smart Components): These are concerned with how things work. They provide the data and behavior to presentational or other container components.
2015年,Dan Abramov发表了一篇题为《Presentational and Container Components》的文章,改变了许多开发者对React组件架构的思考方式。他提出了一种将组件分为两类的模式:
  1. 展示组件(或无状态组件):专注于内容的展示方式。它们不负责数据的加载或修改,仅通过props接收数据和回调函数。
  2. 容器组件(或有状态组件):专注于业务逻辑的实现。它们为展示组件或其他容器组件提供数据和行为。

When to Use

使用场景

  • Use this when you want a clear separation between data-fetching logic and UI rendering
  • This is helpful for making presentational components reusable and easy to test
  • 当你希望清晰分离数据获取逻辑与UI渲染逻辑时使用
  • 有助于提升展示组件的复用性和可测试性

Instructions

实施指南

  • Container components handle data fetching and state; presentational components handle rendering via props
  • Prefer composables over container components in Vue 3 for the same separation of concerns
  • Keep presentational components stateless — they receive data only through props
  • Use the
    useDogImages()
    composable pattern as a modern alternative to container wrappers
  • 容器组件处理数据获取和状态管理;展示组件通过props负责渲染
  • 在Vue 3中,优先使用Composables而非容器组件来实现相同的关注点分离
  • 保持展示组件无状态——仅通过props接收数据
  • 使用
    useDogImages()
    这类Composable模式作为容器组件的现代替代方案

Details

详细说明

While this pattern was mainly associated with React, its fundamental principle was adopted and adapted in various forms across other libraries and frameworks.
Dan's distinction offered a clearer and more scalable way to structure JavaScript applications. By clearly defining the responsibilities of different types of components, developers could ensure better reusability of the UI components (presentational) and logic (containers).
However, with the emergence of hooks in React and the Composition API in Vue 3, the clear boundary between presentational and container components began to blur. Hooks and the Composition API began allowing developers to encapsulate and reuse state and logic without necessarily being confined to a class-based container component or the Options API. With that being said, the pattern can still be helpful at certain times.
Let's say we want to create an application that fetches 6 dog images, and renders these images on the screen.
To follow the container/presentational pattern, we want to enforce the separation of concerns by separating this process into two parts:
  1. Presentational Components: Components that care about how data is shown to the user. In this example, that's the rendering of the list of dog images.
  2. Container Components: Components that care about what data is shown to the user. In this example, that's fetching the dog images.
Fetching the dog images deals with application logic, whereas displaying the images only deals with the view.
虽然该模式最初主要与React相关,但其核心原则被其他库和框架以各种形式采纳和适配。
Dan提出的这种区分方式为JavaScript应用提供了更清晰、更具扩展性的架构思路。通过明确不同类型组件的职责,开发者可以确保UI组件(展示型)和逻辑(容器型)的更好复用。
然而,随着React Hooks和Vue 3 Composition API的出现,展示组件和容器组件之间的清晰界限开始变得模糊。Hooks和Composition API允许开发者封装和复用状态与逻辑,而不必局限于基于类的容器组件或Options API。尽管如此,在某些场景下该模式仍然十分有用。
假设我们要创建一个应用,获取6张狗狗图片并在页面上展示。
为了遵循容器/展示模式,我们需要将这个过程分为两部分,以实现关注点分离:
  1. 展示组件:专注于如何向用户展示数据。在这个例子中,就是渲染狗狗图片列表。
  2. 容器组件:专注于向用户展示哪些数据。在这个例子中,就是获取狗狗图片。
获取狗狗图片属于业务逻辑,而展示图片仅涉及视图层

Presentational Component

展示组件

A presentational component receives its data through
props
. Its primary function is to simply display the data it receives the way we want them to, including styles, without modifying that data.
When rendering the dog images, we simply want to map over each dog image that was fetched from the API, and render those images. We can create a
DogImages
component that receives the data through
props
, and renders the data it received.
html
<template>
  <div>
    <div v-for="(dog, index) in dogs" :key="index">
      <img :src="dog" alt="Dog" />
    </div>
  </div>
</template>

<script setup>
  defineProps(["dogs"]);
</script>
The
DogImages
component is a presentational component. Presentational components are usually stateless: they do not contain their own Vue state, unless they need a state for UI purposes. Presentational components receive their data from container components.
展示组件通过
props
接收数据。它的主要功能是按照我们期望的方式展示接收到的数据,包括样式,但不修改数据。
在渲染狗狗图片时,我们只需遍历从API获取的每张狗狗图片并渲染它们。我们可以创建一个
DogImages
组件,通过
props
接收数据并进行渲染。
html
<template>
  <div>
    <div v-for="(dog, index) in dogs" :key="index">
      <img :src="dog" alt="Dog" />
    </div>
  </div>
</template>

<script setup>
  defineProps(["dogs"]);
</script>
DogImages
组件是一个展示组件。展示组件通常是无状态的:它们不包含自身的Vue状态,除非是为了UI交互需要的状态。展示组件从容器组件接收数据。

Container Component

容器组件

The primary function of container components is to pass data to presentational components, which they contain. Container components themselves usually don't render any other components besides the presentational components that care about their data. Since they don't render anything themselves, they usually do not contain any styling either.
We need to create a container component that fetches this data, and passes this data to the presentational component
DogImages
in order to display it on the screen.
html
<template>
  <DogImages :dogs="dogs" />
</template>

<script setup>
  import { ref, onMounted } from "vue";
  import DogImages from "./DogImages.vue";

  const dogs = ref([]);

  onMounted(async () => {
    const response = await fetch(
      "https://dog.ceo/api/breed/labrador/images/random/6"
    );
    const { message } = await response.json();
    dogs.value = message;
  });
</script>
Combining these two components together makes it possible to separate handling application logic with the view.
容器组件的主要功能是向其包含的展示组件传递数据。容器组件自身通常除了展示组件外不渲染其他内容,因此它们通常也不包含样式。
我们需要创建一个容器组件来获取数据,并将数据传递给展示组件
DogImages
以在页面上展示。
html
<template>
  <DogImages :dogs="dogs" />
</template>

<script setup>
  import { ref, onMounted } from "vue";
  import DogImages from "./DogImages.vue";

  const dogs = ref([]);

  onMounted(async () => {
    const response = await fetch(
      "https://dog.ceo/api/breed/labrador/images/random/6"
    );
    const { message } = await response.json();
    dogs.value = message;
  });
</script>
将这两个组件结合使用,就可以实现业务逻辑与视图层的分离。

Composables

Composables

In many cases, the Container/Presentational pattern can be replaced with composables. The introduction of the Composition API made it easy for developers to add statefulness without needing a container component to provide that state.
Instead of having the data fetching logic in a container component, we can create a custom composable that fetches the images, and returns the array of dogs.
js
import { ref, onMounted } from "vue";

export function useDogImages() {
  const dogs = ref([]);

  onMounted(async () => {
    const response = await fetch(
      "https://dog.ceo/api/breed/labrador/images/random/6"
    );
    const { message } = await response.json();
    dogs.value = message;
  });

  return { dogs };
}
By using this composable, we no longer need the wrapping container component to fetch the data. Instead, we can use this composable directly in our presentational
DogImages
component!
html
<template>
  <div>
    <div v-for="(dog, index) in dogs" :key="index">
      <img :src="dog" alt="Dog" />
    </div>
  </div>
</template>

<script setup>
  import { useDogImages } from "../composables/useDogImages";

  const { dogs } = useDogImages();
</script>
By using the
useDogImages
composable, we still separated the application logic from the view. We're simply using the returned data from the composable, without modifying that data within the component.
Composables make it easy to separate logic and view in a component, just like the Container/Presentational pattern. It saves us the extra layer that was necessary in order to wrap the presentational component within the container component.
在很多场景下,容器/展示模式可以被Composables替代。Composition API的引入让开发者无需通过容器组件提供状态,就能轻松实现状态封装和复用。
我们不必将数据获取逻辑放在容器组件中,而是可以创建一个自定义Composable来获取图片并返回狗狗图片数组。
js
import { ref, onMounted } from "vue";

export function useDogImages() {
  const dogs = ref([]);

  onMounted(async () => {
    const response = await fetch(
      "https://dog.ceo/api/breed/labrador/images/random/6"
    );
    const { message } = await response.json();
    dogs.value = message;
  });

  return { dogs };
}
通过使用这个Composable,我们不再需要额外的容器组件来获取数据。相反,我们可以直接在展示组件
DogImages
中使用它!
html
<template>
  <div>
    <div v-for="(dog, index) in dogs" :key="index">
      <img :src="dog" alt="Dog" />
    </div>
  </div>
</template>

<script setup>
  import { useDogImages } from "../composables/useDogImages";

  const { dogs } = useDogImages();
</script>
通过使用
useDogImages
Composable,我们仍然实现了业务逻辑与视图层的分离。我们只是使用Composable返回的数据,而不在组件内部修改该数据。
Composables能够像容器/展示模式一样,轻松实现组件中逻辑与视图的分离,同时省去了为包裹展示组件而额外添加的容器层。

Source

来源

References

参考资料