data-provider

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Data Provider Pattern

Data Provider Pattern

Table of Contents

目录

In a previous article, we've come to learn how renderless components help separate the logic of a component from its presentation. This becomes useful when we need to create reusable logic that can be applied to different UI implementations.
Renderless components also allow us to leverage another helpful pattern known as the data provider pattern.
在之前的文章中,我们了解了renderless components如何帮助将组件的逻辑与展示分离。当我们需要创建可复用的逻辑并应用于不同的UI实现时,这一点非常有用。
renderless components还能让我们利用另一种实用的模式——Data Provider Pattern

When to Use

适用场景

  • Use this when multiple components need to consume the same data but display it differently
  • This is helpful for centralizing data-fetching logic without coupling it to specific UI components
  • 当多个组件需要使用相同数据但展示方式不同时
  • 有助于集中管理数据获取逻辑,且无需与特定UI组件耦合

When NOT to Use

不适用场景

  • When composables can handle the data logic without the extra component layer (Vue 3+)
  • When only one component consumes the data — a composable or inline fetch is simpler
  • When the data-provider nesting adds indirection that makes the template harder to follow
  • 当组合式函数(composables)无需额外组件层即可处理数据逻辑时(Vue 3+)
  • 仅单个组件使用数据时——使用组合式函数或内联获取方式更简单
  • 当Data Provider的嵌套结构增加了间接性,导致模板难以理解时

Instructions

实现步骤

  • Create a data provider component whose template is a single
    <slot>
    with scoped slot props
  • Pass data, loading state, and action methods as scoped slot props to child components
  • Use
    v-slot
    destructuring in the parent to access provided data
  • Keep child components focused purely on presentation; the data provider handles all data logic
  • 创建一个数据提供者组件,其模板仅包含单个带有作用域插槽属性的
    <slot>
  • 将数据、加载状态和操作方法作为作用域插槽属性传递给子组件
  • 在父组件中使用
    v-slot
    解构来访问提供的数据
  • 让子组件专注于纯展示逻辑;数据提供者处理所有数据相关逻辑

Details

详细说明

Data Provider Pattern

Data Provider Pattern

The data provider pattern is a design pattern that complements the renderless component pattern in Vue by focusing on providing data and state management capabilities to components without being concerned about how the data is rendered or displayed.
In the data provider pattern, a data provider component encapsulates the logic for fetching, managing, and exposing data to its child components. The child components can then consume this data and use it in their own rendering or behavior.
This pattern promotes separation of concerns, as the data provider component takes care of data-related tasks, while the child components can focus on presentation and interaction.
Let's illustrate the data provider pattern with an example. Consider a simple application that displays the setup of a funny joke followed by its punchline. To keep the example self-contained, we'll use a local in-memory data source instead of depending on an external API.
js
const jokes = [
  { id: 1, setup: "Why did the dev go broke?", punchline: "Because they used up all their cache." },
  { id: 2, setup: "Why do functions love TypeScript?", punchline: "Because it keeps their arguments in order." },
];
We'll first create a data provider component called
DataProvider
that will hold the responsibility of loading a joke. In the
<script>
section of the component, we'll import the
ref()
and
reactive()
functions from Vue, define a local data source, and set up
data
and
loading
reactive properties to capture the selected joke and loading state.
html
<script setup>
  import { ref, reactive } from "vue";

  const jokes = [
    { id: 1, setup: "Why did the dev go broke?", punchline: "Because they used up all their cache." },
    { id: 2, setup: "Why do functions love TypeScript?", punchline: "Because it keeps their arguments in order." },
  ];

  const data = reactive({
    setup: null,
    punchline: null,
  });

  const loading = ref(false);
</script>
We can then create a
fetchJoke()
function in our
DataProvider
component to simulate loading data asynchronously.
js
const fetchJoke = async () => {
  loading.value = true;
  try {
    await new Promise((resolve) => setTimeout(resolve, 300));
    const jokeData = jokes[Math.floor(Math.random() * jokes.length)];
    data.setup = jokeData.setup;
    data.punchline = jokeData.punchline;
  } catch (error) {
    console.error("Error loading joke:", error);
  } finally {
    loading.value = false;
  }
};
With the fetch function ready, we can call it when the component mounts using the
onMounted()
lifecycle hook.
js
import { ref, reactive, onMounted } from "vue";

// ...

onMounted(() => {
  fetchJoke();
});
The key element in a data provider component is that its template consists purely of a single
<slot>
element. This slot will provide the fetched data and the relevant method to its child components using scoped slots.
html
<template>
  <slot :data="data" :loading="loading" :fetchJoke="fetchJoke"></slot>
</template>
The
DataProvider
component passes
data
,
loading
, and
fetchJoke
as scoped slot props. This means any child component placed inside the
DataProvider
can access these properties.
Now, let's create a
JokeCard
component that will present the joke data.
html
<template>
  <div class="joke-card">
    <p v-if="loading">Loading...</p>
    <div v-else>
      <p class="setup">{{ data.setup }}</p>
      <p class="punchline">{{ data.punchline }}</p>
    </div>
    <button @click="fetchJoke">Get Another Joke</button>
  </div>
</template>

<script setup>
  defineProps(["data", "loading", "fetchJoke"]);
</script>
The
JokeCard
component is a simple presentational component. It expects
data
,
loading
, and
fetchJoke
as props, and renders the joke data along with a button to fetch a new joke.
Now, to bring it all together, we use the
DataProvider
component in our
App
component. We wrap the
JokeCard
component inside the
DataProvider
and pass the scoped slot props to it:
html
<template>
  <DataProvider v-slot="{ data, loading, fetchJoke }">
    <JokeCard :data="data" :loading="loading" :fetchJoke="fetchJoke" />
  </DataProvider>
</template>

<script setup>
  import DataProvider from "./components/DataProvider.vue";
  import JokeCard from "./components/JokeCard.vue";
</script>
With this setup, the
DataProvider
handles all data fetching and management, while the
JokeCard
focuses solely on displaying the data. This clean separation makes it easy to swap out the presentational component for a different one without touching the data-fetching logic.
The data provider pattern is especially useful when:
  • Multiple components need to consume the same data but display it differently.
  • You want to centralize data fetching logic without tightly coupling it to specific UI components.
  • You want to keep your components focused on a single responsibility.
Data Provider Pattern是Vue中与renderless components模式互补的一种设计模式,专注于为组件提供数据和状态管理能力,而不关心数据如何渲染或展示
在Data Provider Pattern中,数据提供者组件封装了数据获取、管理和暴露给子组件的逻辑。子组件可以消费这些数据,并将其用于自身的渲染或行为逻辑。
该模式促进了关注点分离,数据提供者组件负责处理数据相关任务,而子组件可以专注于展示和交互。
让我们通过一个示例来演示Data Provider Pattern。考虑一个简单的应用,展示一个搞笑笑话的铺垫和笑点。为了让示例独立完整,我们将使用本地内存数据源,而非依赖外部API。
js
const jokes = [
  { id: 1, setup: "Why did the dev go broke?", punchline: "Because they used up all their cache." },
  { id: 2, setup: "Why do functions love TypeScript?", punchline: "Because it keeps their arguments in order." },
];
我们首先创建一个名为
DataProvider
的数据提供者组件,它将负责加载笑话。在组件的
<script>
部分,我们从Vue导入
ref()
reactive()
函数,定义本地数据源,并设置
data
loading
响应式属性来存储选中的笑话和加载状态。
html
<script setup>
  import { ref, reactive } from "vue";

  const jokes = [
    { id: 1, setup: "Why did the dev go broke?", punchline: "Because they used up all their cache." },
    { id: 2, setup: "Why do functions love TypeScript?", punchline: "Because it keeps their arguments in order." },
  ];

  const data = reactive({
    setup: null,
    punchline: null,
  });

  const loading = ref(false);
</script>
然后我们可以在
DataProvider
组件中创建一个
fetchJoke()
函数,模拟异步加载数据。
js
const fetchJoke = async () => {
  loading.value = true;
  try {
    await new Promise((resolve) => setTimeout(resolve, 300));
    const jokeData = jokes[Math.floor(Math.random() * jokes.length)];
    data.setup = jokeData.setup;
    data.punchline = jokeData.punchline;
  } catch (error) {
    console.error("Error loading joke:", error);
  } finally {
    loading.value = false;
  }
};
获取函数准备好后,我们可以使用
onMounted()
生命周期钩子在组件挂载时调用它。
js
import { ref, reactive, onMounted } from "vue";

// ...

onMounted(() => {
  fetchJoke();
});
数据提供者组件的核心元素是其模板仅包含单个
<slot>
元素。该插槽将通过scoped slots把获取到的数据和相关方法提供给子组件。
html
<template>
  <slot :data="data" :loading="loading" :fetchJoke="fetchJoke"></slot>
</template>
DataProvider
组件将
data
loading
fetchJoke
作为作用域插槽属性传递。这意味着任何放置在
DataProvider
内部的子组件都可以访问这些属性。
现在,我们创建一个
JokeCard
组件来展示笑话数据。
html
<template>
  <div class="joke-card">
    <p v-if="loading">Loading...</p>
    <div v-else>
      <p class="setup">{{ data.setup }}</p>
      <p class="punchline">{{ data.punchline }}</p>
    </div>
    <button @click="fetchJoke">Get Another Joke</button>
  </div>
</template>

<script setup>
  defineProps(["data", "loading", "fetchJoke"]);
</script>
JokeCard
是一个简单的展示型组件。它接收
data
loading
fetchJoke
作为props,并渲染笑话数据以及一个用于获取新笑话的按钮。
现在,我们将所有部分整合到
App
组件中。我们将
JokeCard
组件包裹在
DataProvider
内部,并将作用域插槽属性传递给它:
html
<template>
  <DataProvider v-slot="{ data, loading, fetchJoke }">
    <JokeCard :data="data" :loading="loading" :fetchJoke="fetchJoke" />
  </DataProvider>
</template>

<script setup>
  import DataProvider from "./components/DataProvider.vue";
  import JokeCard from "./components/JokeCard.vue";
</script>
通过这种设置,
DataProvider
处理所有数据获取和管理逻辑,而
JokeCard
仅专注于展示数据。这种清晰的分离使得我们可以轻松替换展示组件,而无需修改数据获取逻辑。
Data Provider Pattern在以下场景中尤为有用:
  • 多个组件需要使用相同数据但展示方式不同时。
  • 你希望集中管理数据获取逻辑,且不与特定UI组件紧密耦合时。
  • 你希望让组件专注于单一职责时。

Source

来源

References

参考资料