Skip to content

@msflib/react-users

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

This package provides:

  • A strongly typed Users context provider (UsersProvider)
  • A hook for consumers (useUsers)
  • A direct API factory (createUsersApi) for non-context usage
  • Exported query keys (usersQueryKeys) 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-users @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 users module wraps these backend operations:

  • POST users/join
  • POST users/switch
  • GET users/me
  • GET users/{user_id}

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

user and get are workspace-scoped. joinWorkspace and switchWorkspace are never workspace-scoped — they run before a workspace is joined/selected, so there's no workspace context to scope them to.


Quick Start

1) Configure your app once

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

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

2) Wrap your app with QueryClient + UsersProvider

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

const queryClient = new QueryClient();

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <UsersProvider>{children}</UsersProvider>
    </QueryClientProvider>
  );
}

3) Consume data and actions

import { useUsers } from '@msflib/react-users';

export function WorkspaceSwitcher() {
  const { user, loading, joinWorkspace, switchWorkspace } = useUsers();

  return (
    <div>
      <p>Current workspace: {user?.workspace_slug ?? 'none'}</p>

      <button
        disabled={loading.joinWorkspace}
        onClick={() =>
          joinWorkspace({
            workspace_id: 10,
            user_type: 'learner',
          })
        }
      >
        Join
      </button>

      <button
        disabled={loading.switchWorkspace}
        onClick={() => switchWorkspace({ workspace_id: '10' })}
      >
        Switch
      </button>
    </div>
  );
}

Configuration

Base Application Configuration

@msflib/react-users 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', // optional initial workspace
});

Users Endpoint Override

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

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

API Reference

UsersProvider

Context provider for user profile/workspace membership actions.

import { UsersProvider } from '@msflib/react-users';

<UsersProvider
  options={{
    isWorkspaceScoped: true,
    requireAuth: true,
    isAuthenticated: true,
  }}
>
  {children}
</UsersProvider>;

Props

Prop Type Required Description
children React.ReactNode Yes Descendant tree that will consume useUsers.
options.isWorkspaceScoped boolean No Controls whether endpoints are workspace-scoped. Default: true.
options.requireAuth boolean No If true, user query only runs when isAuthenticated === true. Default: false.
options.isAuthenticated boolean No Auth gate flag used only when requireAuth is true.

Behavior

  • user is loaded through TanStack useQuery with:
  • staleTime: 60_000
  • retry: false
  • joinWorkspace and switchWorkspace on success:
  • call setActiveWorkspace(user.workspace_slug)
  • rely on the active workspace change to move subsequent user requests onto the next query key

useUsers

Hook that returns the full users context contract:

import { useUsers } from '@msflib/react-users';

const { user, getUser, joinWorkspace, switchWorkspace, refetchUser, loading } =
  useUsers();

If used outside UsersProvider, it throws:

useUsers must be used inside <UsersProvider />

createUsersApi

Factory for direct API usage (without context).

import { createUsersApi } from '@msflib/react-users';

const usersApi = createUsersApi(true); // workspaceScoped = true by default

const currentUser = await usersApi.user();
const userById = await usersApi.get(123);
await usersApi.joinWorkspace({ workspace_id: 20, user_type: 'learner' });
await usersApi.switchWorkspace({ workspace_id: '20' });

Signature

createUsersApi(workspaceScoped?: boolean)

usersQueryKeys

Exported helper for query cache key generation.

import { usersQueryKeys } from '@msflib/react-users';

usersQueryKeys.all();
usersQueryKeys.user('workspace-slug');
usersQueryKeys.byId('workspace-slug', 123);

Use these keys from consuming apps for invalidation:

import { usersQueryKeys } from '@msflib/react-users';
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

await queryClient.invalidateQueries({
  queryKey: usersQueryKeys.user('workspace-slug'),
});

TypeScript Reference

UserRecord

Represents the response entity returned by user, get, joinWorkspace, and switchWorkspace.

Core fields:

  • id: number
  • created_at: string
  • updated_at: string
  • account_id: number
  • workspace_id: number
  • type: UserType
  • status: UserStatus
  • workspace_slug: string

It also supports extra backend fields through an index signature.

JoinWorkspacePayload

Required:

  • workspace_id: number
  • user_type: UserType

Optional:

  • level_id?: number
  • track_id?: number
  • stage_id?: number
  • info?: { ... }

SwitchWorkspacePayload

  • workspace_id: string

UsersContextType

Includes:

  • user: UserRecord | null
  • getUser(userId, options?)
  • joinWorkspace(payload, options?)
  • switchWorkspace(payload, options?)
  • refetchUser()
  • loading flags for all operations

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


Endpoint Defaults and Resolution

Default users base endpoint:

/users

Resolved endpoints:

  • GET /users/me
  • GET /users/:user_id
  • POST /users/join
  • POST /users/switch

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

join and switch always opt out of workspace scoping (isWorkspaceScoped: false), regardless of the isWorkspaceScoped passed to createUsersApi/UsersProvider. This is not configurable — a user has no workspace context to scope into until after join/switch succeeds. Under the path tenancy strategy, this holds even though /users itself is registered as scoped by get/user — @msflib/core's workspace-scoping registry treats an explicit opt-out as an override that wins over a broader registered base path.


Caching Behavior

  • user query key: usersQueryKeys.user(activeWorkspaceOrNull)
  • user query is stale for 60 seconds (staleTime: 60_000)
  • joinWorkspace and switchWorkspace update the active workspace; the user query refetches naturally when the key changes
  • getUser is exposed as a mutation (mutateAsync) for on-demand fetch

Multi-Tenancy Behavior

  • Provider reads current workspace from useActiveWorkspace().
  • When options.isWorkspaceScoped === false, workspace value for query-keying is treated as null.
  • joinWorkspace/switchWorkspace requests themselves are always unscoped (see Endpoint Defaults and Resolution), independent of options.isWorkspaceScoped — that option only affects user/get and query-key caching.
  • On successful join/switch:
  • active workspace is updated via setActiveWorkspace(workspace_slug)
  • subsequent user reads use the next workspace-specific query key

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


Usage Recipes

Auth-gated user query

<UsersProvider
  options={{
    requireAuth: true,
    isAuthenticated,
  }}
>
  {children}
</UsersProvider>

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

Using mutation callbacks

await joinWorkspace(
  { workspace_id: 10, user_type: 'learner' },
  {
    onSuccess: (user) => {
      console.log('Joined workspace slug:', user.workspace_slug);
    },
    onError: (error) => {
      console.error(error.message);
    },
  },
);

Manual refresh

const { refetchUser } = useUsers();

refetchUser();

Testing

Run package tests:

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

The package includes tests for:

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

Troubleshooting

useUsers must be used inside <UsersProvider />

Cause: hook is called outside provider scope.

Fix: ensure component is rendered under UsersProvider.

user not fetching

Check:

  • QueryClientProvider is present
  • UsersProvider is mounted
  • 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.users.users override is correct
  • isWorkspaceScoped option matches your intended path mode

Public Exports

From @msflib/react-users:

  • Context and provider:
  • UsersProvider
  • UsersContext
  • Hook:
  • useUsers
  • API:
  • createUsersApi
  • Query keys:
  • usersQueryKeys
  • Types:
  • UserType
  • UserStatus
  • UserRecord
  • JoinWorkspacePayload
  • SwitchWorkspacePayload
  • UsersContextType