Skip to content

codevinehq/data-pkg-example

Repository files navigation

@benefex/data Proposal

The @benefex/data package builds on the concepts behind both useApi and our Redux sagas.

Its primary focus is centralising service creation and types. It will also provide convenience methods for React Query, though this could be expanded to more libs in the future if we migrate away from React Query (see SWR example).

Once you have read this document feel free to try the tasks here.

How Will It Work?

Service Definitions

A service definition is a logical group of endpoints. This might be an entire API (e.g., Alerts) or a subset (e.g., WalletUser). It contains key information to facilitate the creation of data-fetching helpers.

  • url: The endpoint for the API. :userId and :companyId are automatically populated from the user's token unless specified in urlParams (similar to useApi and Redux).
  • tags: These form part of the queryKey and are used for invalidation (see invalidators).
  • call: This defines the logic for the service. Its only requirement is a URL; however, other common properties are also typed using the ServiceArgs helper.

Example:

const notesService = {
  get: createQueryService({
    url: "/notes/:noteId",
    tags: ["notes"] as const,
    // urlParams only
    call: (args: ServiceArgs<NoteParams>) => mockFetcher<Note>(args),
    // urlParams and searchParams
    // call: (args: ServiceArgs<{ urlParams: NoteParams, searchParams: NoteQuery }>) => mockFetcher<Note>(args),
  }),
  create: createService({
    url: "/notes",
    call: ({ url, body }: ServiceArgs<{ body: CreateNote }>) =>
      mockFetcher<Note>({ url, options: { method: "POST", body } }),
  }),
};

Queries

const query = useQuery(api.notes.get.query({ urlParams: { noteId } }));

const queryWithOptions = useQuery({
  ...api.notes.get.query({ urlParams: { noteId } }),
  select: ({ content }) => content
});

Mutations

const createNote = useMutation({
  mutationFn: api.notes.create.call
});

createNote.mutate({
  title: '...',
  content: '...'
});

Example query migration

Everything is typed based on the service definitions, useQuery (including select) and useMutation (including onSuccess etc.) inherit the types automatically. No more generics 🎉

Before After
import { useApi } from "@benefex/components";
import createRouter from "@benefex/react/utils/api/router/createRouter";
import routes from "store/api/routes";
import type { DiscoverTopic } from "components/FollowTopic/FollowTopic.types";
import { personalisationKeys } from "store/api/queryKeys";

const router = createRouter(routes);

const {
  data: topics = [],
  isLoading
} = useApi<{ content: DiscoverTopic[] }, DiscoverTopic[]>({
  method: "GET",
  options: {
    enabled: false,
    queryKey: personalisationKeys.topics,
    select: ({ content }) => content,
  },
  queryParams: {
    size: "5",
  },
  url: router(["content", "discoverTopics"]),
});
import { useQuery } from "@tanstack/react-query";
import { api } from "@benefex/data";

const { data: topics = [], isLoading } = useQuery({
  ...api.content.discoverTopics.query({ queryParams: { size: "5" } }),
  select: ({ content }) => content,
});

Example mutation migration

It's worth noting that currently mutations are completely untyped and in the migrated version they are fully typed.

alt text

Before After
import { useEffect } from "react";
import { useApi, RQC } from "@benefex/components";
import createRouter from "@benefex/react/utils/api/router/createRouter";
import routes from "store/api/routes";
import { personalisationKeys } from "store/api/queryKeys";
import type { DiscoverTopic } from "./FollowTopic.types";

const router = createRouter(routes);

const followTopic = useApi({
  method: "PUT",
  url: router(["follows", "topic"]),
  urlParams: {
    userId: userId as string,
  },
});

const [isFollowing, setIsFollowing] = useState(topic.isUserFollowing);

useEffect(() => {
  if (followTopic.isSuccess) {
    setIsFollowing(true);
    setToastText("home.feed.topics.successToast");

    setShowToast(true);

    RQC.setQueriesData(personalisationKeys.topics(), (previous: any) => ({
      ...previous,
      content: previous?.content?.map((prevTopic: DiscoverTopic) =>
        prevTopic.id === topic.id
          ? {
              ...topic,
              isUserFollowing: true,
            }
          : prevTopic,
      ),
    }));
  }
}, [followTopic.isSuccess]);

useEffect(() => {
  if (followTopic.isError) {
    setToastText("home.feed.topics.errorToast");
    setShowToast(true);
  }
}, [followTopic.isError]);

const handleClickSubscribe = () => {
  followTopic.callEndpoint({
    id: topic.id,
    name: topic.name,
  });
};

{isFollowing ? <Button>Unfollow</Button> : ...}
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api, invalidateByTags } from "@benefex/data";

const queryClient = useQueryClient();
const followTopic = useMutation({
  mutationFn: () => api.topics.follow.call({
    urlParams: { userId }
  }),
  onSuccess() {
    setIsFollowing(true);
    setToastText("home.feed.topics.successToast");
    setShowToast(true);

    // Return invalidation so isPending stays true until the topic is refetched
    return queryClient.invalidateQueries({
      predicate: invalidateByTags(api.topics.all.tags)
    });
  },
  onError() {
    setToastText("home.feed.topics.errorToast");
    setShowToast(true);
  },
});
const isFollowing = followTopic.isPending || topic.isFollowing;

const handleClickSubscribe = () => {
  followTopic.mutate({ id: topic.id, name: topic.name });
};

{isFollowing ? <Button>Unfollow</Button> : ...}

Files with examples

typesafe onSuccess redirect

typed select

Service definitions are not tied to a specific data fetching library. This means they can be used by Redux, RTK, React Query, useSWR or any other lib.

Each service should expose mock factories to help with testing

Contains MSW helpers built using the api service

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published