@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
- Peer Dependencies
- What This Module Covers
- Quick Start
- Configuration
- Base Application Configuration
- Users Endpoint Override
- API Reference
- UsersProvider
- useUsers
- createUsersApi
- usersQueryKeys
- TypeScript Reference
- Endpoint Defaults and Resolution
- Caching Behavior
- Multi-Tenancy Behavior
- Usage Recipes
- Testing
- Troubleshooting
- Public Exports
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/joinPOST users/switchGET users/meGET 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¶
useris loaded through TanStackuseQuerywith:staleTime: 60_000retry: falsejoinWorkspaceandswitchWorkspaceon success:- call
setActiveWorkspace(user.workspace_slug) - rely on the active workspace change to move subsequent
userrequests 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: numbercreated_at: stringupdated_at: stringaccount_id: numberworkspace_id: numbertype: UserTypestatus: UserStatusworkspace_slug: string
It also supports extra backend fields through an index signature.
JoinWorkspacePayload¶
Required:
workspace_id: numberuser_type: UserType
Optional:
level_id?: numbertrack_id?: numberstage_id?: numberinfo?: { ... }
SwitchWorkspacePayload¶
workspace_id: string
UsersContextType¶
Includes:
user: UserRecord | nullgetUser(userId, options?)joinWorkspace(payload, options?)switchWorkspace(payload, options?)refetchUser()loadingflags 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/meGET /users/:user_idPOST /users/joinPOST /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¶
userquery key:usersQueryKeys.user(activeWorkspaceOrNull)userquery is stale for 60 seconds (staleTime: 60_000)joinWorkspaceandswitchWorkspaceupdate the active workspace; theuserquery refetches naturally when the key changesgetUseris 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 asnull. joinWorkspace/switchWorkspacerequests themselves are always unscoped (see Endpoint Defaults and Resolution), independent ofoptions.isWorkspaceScoped— that option only affectsuser/getand query-key caching.- On successful join/switch:
- active workspace is updated via
setActiveWorkspace(workspace_slug) - subsequent
userreads 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:
QueryClientProvideris presentUsersProvideris mounted- If using auth gating:
requireAuth: trueandisAuthenticatedmust betrue
Wrong endpoint path or missing workspace prefix¶
Check:
configureApplication(...)ran before provider/API usageendpoints.users.usersoverride is correctisWorkspaceScopedoption matches your intended path mode
Public Exports¶
From @msflib/react-users:
- Context and provider:
UsersProviderUsersContext- Hook:
useUsers- API:
createUsersApi- Query keys:
usersQueryKeys- Types:
UserTypeUserStatusUserRecordJoinWorkspacePayloadSwitchWorkspacePayloadUsersContextType