Skip to content

@msflib/react-categories

Standalone categories module for React applications, built on TanStack Query v5 and integrated with @msflib/core + @msflib/react-shared.

This package provides:

  • A strongly typed categories scope provider (CategoryProvider)
  • A hook for consuming a single category type with optional query params (useCategories)
  • A compatibility alias for older code paths (useCategory)
  • A direct API factory (createCategoryApi) for non-context usage
  • Exported query keys (categoriesQueryKeys) for cache invalidation from consuming apps
  • End-to-end TypeScript types for payloads, responses, and context contracts

Table of Contents


Installation

pnpm add @msflib/react-categories @msflib/core @msflib/react-shared @tanstack/react-query

Peer Dependencies

Package Version
react ^18 || ^19
react-dom ^18 || ^19
@tanstack/react-query ^5
@msflib/core workspace:*
@msflib/react-shared workspace:*

What This Module Covers

The categories module wraps these backend operations:

  • GET cat/{category_type}
  • POST cat/{category_type}
  • PUT cat/{category_type}/{category_id}
  • DELETE cat/{category_type}/{category_id}

At runtime, these route calls are executed through configuredApiClient from @msflib/core, which applies workspace-scoping behavior consistently with the rest of the ecosystem.


Quick Start

1) Configure your app once

import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token',
  endpoints: {
    categories: {
      categories: '/cat',
    },
  },
});

2) Wrap your app with QueryClient + CategoryProvider

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CategoryProvider } from '@msflib/react-categories';

const queryClient = new QueryClient();

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <CategoryProvider
        categoryTypes={['track', 'stage', 'level']}
        options={{
          query: { offset: 0, limit: 100 },
        }}
      >
        {children}
      </CategoryProvider>
    </QueryClientProvider>
  );
}

3) Consume one category type in a screen

import { useCategories } from '@msflib/react-categories';

export function TrackList() {
  const {
    categories,
    createCategory,
    updateCategory,
    deleteCategory,
    loading,
  } = useCategories({
    categoryType: 'track',
    query: { offset: 0, limit: 20 },
  });

  return (
    <div>
      <p>Total categories: {categories.length}</p>

      <button
        disabled={loading.create}
        onClick={() =>
          createCategory({
            name: 'frontend',
            title: 'Frontend',
            description: 'Frontend topics',
            tags: ['react'],
            order: 1,
          })
        }
      >
        Create
      </button>

      <button
        disabled={loading.update}
        onClick={() => updateCategory(1, { title: 'Updated Frontend' })}
      >
        Update
      </button>

      <button disabled={loading.delete} onClick={() => deleteCategory(1)}>
        Delete
      </button>
    </div>
  );
}

Configuration

Base Application Configuration

@msflib/react-categories expects @msflib/core application config to be initialized before provider usage.

import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token',
  workspace: 'workspace-a',
});

Categories Endpoint Override

By default the module uses /cat. Override it if your API namespace differs:

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token',
  endpoints: {
    categories: {
      categories: '/v2/categories',
    },
  },
});

API Reference

CategoryProvider

Context provider for the stable category scope. It also primes TanStack Query with the provider-level default query for each configured category type.

import { CategoryProvider } from '@msflib/react-categories';

<CategoryProvider
  categoryTypes={['track', 'stage', 'level']}
  options={{
    isWorkspaceScoped: true,
    requireAuth: true,
    isAuthenticated: true,
    query: { offset: 0, limit: 100 },
  }}
>
  {children}
</CategoryProvider>;

Props

Prop Type Required Description
children React.ReactNode Yes Descendant tree that will consume category hooks.
categoryTypes string[] No Category namespaces managed by this provider. Use this for the multi-category setup.
categoryType string No Backward-compatible single-type fallback if categoryTypes is not supplied.
options.isWorkspaceScoped boolean No Controls whether endpoints are workspace-scoped. Default: true.
options.requireAuth boolean No If true, queries only run when isAuthenticated is true. Default: false.
options.isAuthenticated boolean No Auth gate flag used only when requireAuth is true.
options.query object No Default list query applied by hooks unless overridden at the hook call site.

Behavior

  • The provider defines the stable category scope and default query config.
  • The hook decides which category type to load and which query params to use.
  • Mutations invalidate the matching category type + workspace cache scope.

useCategories

Hook that returns data and actions for one category type:

import { useCategories } from '@msflib/react-categories';

const {
  categories,
  createCategory,
  updateCategory,
  deleteCategory,
  refetch,
  loading,
} = useCategories({
  categoryType: 'track',
  query: { offset: 0, limit: 20 },
});

If used outside CategoryProvider, it throws:

useCategories must be used inside <CategoryProvider />

useCategory

Backward-compatible alias for useCategories.


import { useCategory } from '@msflib/react-categories';

const trackCategories = useCategory({ categoryType: 'track' });


createCategoryApi

Factory for direct API usage (without context).

import { createCategoryApi } from '@msflib/react-categories';

const categoryApi = createCategoryApi('track', true); // isWorkspaceScoped = true by default

const list = await categoryApi.list();
await categoryApi.create({
  name: 'frontend',
  title: 'Frontend',
  description: 'Frontend topics',
  tags: ['react'],
  order: 1,
});
await categoryApi.update(10, { title: 'Updated Frontend' });
await categoryApi.delete(10);

Signature

createCategoryApi(categoryType: string, isWorkspaceScoped?: boolean)

categoriesQueryKeys

Exported helper for category query cache key generation.

import { categoriesQueryKeys } from '@msflib/react-categories';

categoriesQueryKeys.all();
categoriesQueryKeys.scope('track', 'workspace-slug');

Use these keys from consuming apps for invalidation:

import { categoriesQueryKeys } from '@msflib/react-categories';
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

await queryClient.invalidateQueries({
  queryKey: categoriesQueryKeys.scope('track', 'workspace-slug'),
});

TypeScript Reference

Category

Represents the category entity returned by list/create/update/delete.

Core fields:

  • id: number
  • name: string
  • title: string
  • description: string
  • tags: string[]
  • order: number

It also supports extra backend fields through an index signature.

CategoryPayload

Required:

  • name: string
  • title: string
  • description: string
  • tags: string[]
  • order: number

UpdateCategoryPayload

Partial category update payload excluding name.

CategoryConsumerContextType

Includes:

  • categoryType: string
  • query?: CategoryListParams
  • categories: Category[]
  • createCategory(data, options?)
  • updateCategory(categoryId, data, options?)
  • deleteCategory(categoryId, options?)
  • refetch()
  • loading flags for all operations

Mutation option parameters use TanStack MutateOptions for per-call success/error handlers.


Endpoint Defaults and Resolution

Default categories base endpoint:

/cat

Resolved endpoints:

  • GET /cat/:categoryType
  • POST /cat/:categoryType
  • PUT /cat/:categoryType/:categoryId
  • DELETE /cat/:categoryType/:categoryId

Path generation and workspace scoping are delegated to the core configured API client, so workspace prefixes are applied consistently with your global core configuration.


Caching Behavior

  • categories query key: categoriesQueryKeys.list(categoryType, activeWorkspaceOrNull, query)
  • categories query is stale for 60 seconds (staleTime: 60_000)
  • createCategory, updateCategory, deleteCategory all invalidate the same category scope

Multi-Tenancy Behavior

  • Provider reads current workspace from useActiveWorkspace().
  • When options.isWorkspaceScoped === false, workspace value for query-keying is treated as null.

This enables downstream modules relying on active workspace to react to workspace changes.


Usage Recipes

Auth-gated categories query

<CategoryProvider
  categoryTypes={['track', 'stage', 'level']}
  options={{
    requireAuth: true,
    isAuthenticated,
  }}
>
  {children}
</CategoryProvider>

When requireAuth is true and isAuthenticated is false, the initial categories request is not executed.

Using mutation callbacks

await createCategory(
  {
    name: 'frontend',
    title: 'Frontend',
    description: 'Frontend topics',
    tags: ['react'],
    order: 1,
  },
  {
    onSuccess: (category) => {
      console.log('Created category id:', category.id);
    },
    onError: (error) => {
      console.error(error.message);
    },
  },
);

Manual refresh

const { refetch } = useCategory({ categoryType: 'track' });

refetch();

Testing

Run package tests:

pnpm --filter @msflib/react-categories test:build

The package includes tests for:

  • API endpoint construction and client interaction
  • Provider query and mutation behavior
  • Auth-gated query execution
  • Hook/provider contract smoke coverage

Troubleshooting

useCategory must be used inside

Cause: hook is called outside provider scope.

Fix: ensure component is rendered under CategoryProvider.

categories not fetching

Check:

  • QueryClientProvider is present
  • CategoryProvider is mounted with categoryTypes
  • If using auth gating: requireAuth: true and isAuthenticated must be true

Wrong endpoint path or missing workspace prefix

Check:

  • configureApplication(...) ran before provider/API usage
  • endpoints.categories.categories override is correct
  • isWorkspaceScoped option matches your intended path mode

Public Exports

From @msflib/react-categories:

  • Context and provider:
  • CategoryProvider
  • CategoryContext
  • Hook:
  • useCategory
  • API:
  • createCategoryApi
  • Query keys:
  • categoriesQueryKeys
  • Types:
  • Category
  • CategoryPayload
  • UpdateCategoryPayload
  • CategoryConsumerContextType