Skip to content

Report a problem:

Problems can be reported via Microsoft Teams in your team channel within the "IT - Codey" team.

Please include the following information:

Report type:
Docs problem report a bug instead →
Path:
/vnext/cookbook/the-stack/state-management/z__bring-it-all-together.html
Message:

DESCRIBE THE PROBLEM BEFORE SUBMITTING TO CODEY

Bringing It All Together

After reading about the different possibilities for handling state, how is it all brought together?

Depending on your application's needs and data flow, this can vary. To give you a general idea, you can find a showcase below. The approach is not set in stone, but mainly aims to highlight concerns you need to take into account.

Example App

We have an application that has user information we can fetch after we've logged in. We want to display the user's full name in the top right of the header bar.

A second feature is a profile page that will allow you to update your name.

The schema below gives a visual representation of how interactions are structured:

interaction-schema

user-info.api.ts

This file holds the interactions with the API to fetch user information (repository pattern). It also validates the input it receives before sending it to the API.

Additionally, it can handle certain errors or log information to metric tools like Application Insights.

Why have this layer?

The goal is to centralize all outbound API communication.

  1. Single Responsibility: Each API file has one clear purpose - managing communication with a specific API endpoint or domain
  2. Easier Testing: Mock and test API interactions in isolation without affecting other parts of the application
  3. Consistent Error Handling: Centralized error handling and logging ensures uniform behavior across all API calls
  4. Maintainability: Changes to API contracts or endpoints only require updates in one location, reducing code duplication

Example

user-info.api.ts
ts
import { UserInfo } from "../types/user.types";

const BASE_URL = "/api/users";

export const getUserInfo = async (userId: string): Promise<UserInfo> => {
  try {
    const response = await fetch(`${BASE_URL}/${userId}`);

    if (!response.ok) {
      throw new Error(`Failed to fetch user info: ${response.status}`);
    }

    const data: UserInfo = await response.json();
    return data;
  } catch (error) {
    // Log to Application Insights or other monitoring tool
    throw error;
  }
};

export const updateUserInfo = async (userInfo: UserInfo) => {
  try {
    const response = await fetch(`${BASE_URL}/${userInfo.id}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(userInfo),
    });

    if (!response.ok) {
      throw new Error(`Failed to update user info: ${response.status}`);
    }

    const data: UserInfo = await response.json();
    return data;
  } catch (error) {
    // Log to Application Insights or other monitoring tool
    throw error;
  }
};

user-info.query.ts

In the query file, we define the specific query definitions for fetching user information through vue-query. Its purpose is to be reusable throughout the application.

Data management is a responsibility of vue-query, so this file should not hold any state.

Example

user-info.query.ts
ts
import { queryOptions, type QueryClient } from "@tanstack/vue-query";
import { getUserInfo, updateUserInfo } from "./user-info.api";
import type { UserInfo } from "../types/user.types";

export const userInfoQueryOptions = (userId: string) =>
  queryOptions({
    queryKey: ["user-info", userId],
    queryFn: () => getUserInfo(userId),
    // These options can be centralized or set as defaults.
    staleTime: 5 * 60 * 1000, // 5 minutes
    gcTime: 10 * 60 * 1000, // 10 minutes
    enabled: !!userId, // Only run query if userId exists
    retry: 2,
  });

// Provide the query client as a parameter as this needs to be created within a setup context
export const updateUserInfoMutation = (userInfo: UserInfo, queryClient: QueryClient) =>
  useMutation({
    mutationFn: () => updateUserInfo(userInfo),
    onSuccess: () => {
      // Invalidate and refetch user info query
      queryClient.invalidateQueries(["user-info", userInfo.id]);
    },
  });

useUserInfo.ts

This composable extends the functionality of the user information data layer in a reusable manner.

This composable combines the query functionality with additional computed properties and business logic specific to user information display.

Example

useUserInfo.ts
typescript
import { computed } from "vue";
import { useQuery } from "@tanstack/vue-query";
import { userInfoQueryOptions } from "./user-info.query";
import type { UserInfo } from "../types/user.types";

export function useUserInfo(userId: string) {
  const { data: userInfo } = useQuery(userInfoQueryOptions(userId));

  // Computed property for full name
  const fullName = computed(() => {
    if (!userInfo.value) return "";

    const { firstName, lastName } = userInfo.value;
    if (firstName && lastName) {
      return `${firstName} ${lastName}`;
    }
    return firstName || lastName || "";
  });

  return {
    fullName,
  };
}

Note

This is a simple example, and in the real world, when you are not intending to show the fullName in other places, the computed property could just live in the component and this composable would be overkill.

Always remember that composables exist to extract and encapsulate logic so it can be reused across multiple components as an optimization.

Not everything needs to be a composable.

Considerations & Key Takeaways

Combining different practices

Above example shows a common solution for a simple case. It is possible to combine different state management practices as needed. Some other samples include:

  • api layer <- vue-query <- composable <- ...
  • api layer <- vue-query <- pinia <- ...
  • api layer <- pinia <- composables <- ...
  • api layer <- composable <- composable <- ...
  • api layer <- Single File Component

The key here is the first choice in this chain after the api layer. It will determine the overall architecture and data flow of that resource within your application.

Exposing the data through the composable

As you might have noticed, we are not exposing any of the query state (loading, error, etc.) through the composable. This is an intentional choice. We leave the calling of the useQuery for the actual data to the orchestrating component within the application.

This can be a composable but most likely will be a component.

Why don't we recommend exposing it through the composable?

  • Code Duplication: Destructuring and exporting pieces of vue-query functionality is required for them to be used.
  • Increased Complexity without benefit: You are essentially wrapping a framework.
  • Boilerplate: This paradigm forces you to create composables for everything, even when it's not necessary.

Keep query/mutation definitions reusable

At some point there will be a need to access the QueryClient. This should be a parameter of your reusable function.

If you want to get this client inside a user-info.query.ts, you will need to reform it into a composable as it requires the setup context. This makes it less flexible for reuse.

The QueryClient instance can be fetched in the orchestrating component and passed down to the composable or query function as needed.

Report a problem:

Problems can be reported via Microsoft Teams in your team channel within the "IT - Codey" team.

Please include the following information:

Report type:
Docs problem report a bug instead →
Path:
/vnext/cookbook/the-stack/state-management/z__bring-it-all-together.html
Message:

DESCRIBE THE PROBLEM BEFORE SUBMITTING TO CODEY

Last updated: