Skip to content

@msflib/react-profile

Standalone user-profile module for React applications. Built on TanStack Query v5. Supports real-time multi-tenant switching via @msflib/core.


Table of Contents


Installation

pnpm add @msflib/react-profile @msflib/core @msflib/react-shared

Peer Dependencies

Package Version
react >= 18
react-dom >= 18
@tanstack/react-query >= 5

Quick Start

1. Configure the application once (e.g. in your app's entry point):

// app/config.ts
import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: process.env.NEXT_PUBLIC_API_URL!,
  accessTokenKey: 'access_token',
  endpoints: {
    profile: {
      profile: '/profiles', // default — override here if needed
    },
  },
});

2. Wrap your app with QueryClientProvider and ProfileProvider:

// app/providers.tsx
'use client'; // Next.js App Router

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

const queryClient = new QueryClient();

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

3. Use the useProfile hook in any child component:

import { useProfile } from '@msflib/react-profile';

export function ProfileCard() {
  const { profile, loading, updateProfile } = useProfile();

  if (loading.profile) return <p>Loading…</p>;
  if (!profile) return <p>No profile found.</p>;

  return (
    <div>
      <h2>
        {profile.first_name} {profile.last_name}
      </h2>
      {profile.avatar && <img src={profile.avatar} alt="Avatar" />}
      <button
        disabled={loading.update}
        onClick={() => updateProfile({ bio: 'Hello world!' })}
      >
        Update Bio
      </button>
    </div>
  );
}

Configuration

Application Config

Call configureApplication once before rendering your app. It is idempotent — subsequent calls after the first are silently ignored.

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

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token', // localStorage key for the JWT
  workspace: 'acme', // optional: initial active workspace
  endpoints: {
    profile: {
      /* override defaults here */
    },
  },
});

Custom Endpoints

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'my_token',
  endpoints: {
    profile: {
      profile: '/v2/user/profiles',
    },
  },
});

API Reference

ProfileProvider

A React context provider that manages the current user's profile state. Must be rendered inside a QueryClientProvider.

import { ProfileProvider } from '@msflib/react-profile';

<QueryClientProvider client={queryClient}>
  <ProfileProvider>{/* your app */}</ProfileProvider>
</QueryClientProvider>;

Props

Prop Type Required Description
children React.ReactNode Yes Child component tree.
options object No Configuration options for the provider. See below for details.

options Configuration

Key Type Default Description
isWorkspaceScoped boolean true If true, the profile is isolated per workspace.
requireAuth boolean false If true, the profile will only be fetched when isAuthenticated is true.
isAuthenticated boolean false The authentication status of the user (e.g. from useAuth().status).

Behaviour

  • On mount, ProfileProvider automatically fetches GET /profiles to load the user's profile.
  • The result is cached for 60 seconds via TanStack Query (staleTime: 60_000).
  • All mutations (create, update, uploadAvatar, delete) automatically update the query cache on success — no manual invalidations needed.

useProfile

Returns the profile context. Must be called inside a component tree wrapped by ProfileProvider.

import { useProfile } from '@msflib/react-profile';

const {
  profile,
  createProfile,
  updateProfile,
  uploadAvatar,
  deleteProfile,
  getAccountProfile,
  updateAccountProfile,
  refetch,
  loading,
} = useProfile();

profile

Profile | null — The current user's profile. Is null before the fetch resolves or when no profile exists.

createProfile(data, options?)

Creates a new profile for the current user (POST /profiles).

await createProfile({
  first_name: 'Jane',
  last_name: 'Smith',
  date_of_birth: '1995-05-05',
  gender: 'female',
  marital_status: 'single',
});

Returns Promise<Profile>.

updateProfile(data, options?)

Partially updates the current user's profile (PUT /profiles).

await updateProfile({ first_name: 'Johnny', city: 'Austin' });

Returns Promise<Profile>.

uploadAvatar(data, options?)

Uploads or replaces the user's avatar image (PUT /profiles/avatar). Accepts a FormData object.

const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file) return;
  const fd = new FormData();
  fd.append('avatar', file);
  uploadAvatar(fd);
};

Returns Promise<Profile>.

deleteProfile(options?)

Deletes the current user's profile (DELETE /profiles) and clears the cache.

await deleteProfile();

Returns Promise<Profile>.

getAccountProfile(accountId, options?)

Fetches another account's profile by id (GET /profiles/{account_id}) — e.g. for viewing a teammate's profile, not just your own. On success, the result is cached under its own account-scoped query key (it does not touch the current user's profile).

const teammate = await getAccountProfile(42);

Returns Promise<Profile>. For a declarative, cache-backed read instead of an imperative call, use the standalone useAccountProfile(accountId) hook (exported alongside useProfile):

import { useAccountProfile } from '@msflib/react-profile';

function TeammateCard({ accountId }: { accountId: number }) {
  const { data: teammate, isPending } = useAccountProfile(accountId);

  if (isPending) return <p>Loading…</p>;
  return <h3>{teammate?.first_name}</h3>;
}

useAccountProfile is disabled (no request fired) while accountId is undefined.

updateAccountProfile(id, data, options?)

Updates another account's profile by id (PUT /profiles/{id}) — e.g. an admin editing a member's profile. Unlike updateProfile, this does not touch the current user's own profile cache.

await updateAccountProfile(42, { first_name: 'Johnny' });

Returns Promise<Profile>.

refetch()

Manually triggers a fresh fetch of the profile data.

refetch();

loading

An object of booleans indicating in-flight operations:

loading.profile; // true while the initial profile fetch is in progress
loading.create; // true while createProfile is in flight
loading.update; // true while updateProfile is in flight
loading.uploadAvatar; // true while uploadAvatar is in flight
loading.delete; // true while deleteProfile is in flight
loading.getAccountProfile; // true while getAccountProfile is in flight
loading.updateAccountProfile; // true while updateAccountProfile is in flight

Multi-Tenancy Support

@msflib/react-profile integrates with the workspace runtime in @msflib/core. All API requests are automatically prefixed with the active workspace, and the query cache reacts to workspace changes in real time.

How it works

  1. Endpoint paths — The API layer is built with configuredApiClient() from @msflib/core, which applies the workspace prefix/header for scoped endpoints at request time. When no workspace is set, the path is used as-is.
  2. Query cache isolation — The profile query key includes the current workspace: [workspace, 'msflib', 'profile']. Switching workspaces produces a new key, triggering a fresh network fetch automatically — no manual invalidation needed.
  3. Real-time updates — ProfileProvider uses useActiveWorkspace() from @msflib/react-shared (backed by useSyncExternalStore), so any call to setActiveWorkspace() causes an immediate re-render and re-fetch with zero configuration.

Setting the initial workspace

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token',
  workspace: 'acme-corp', // seeds the active workspace on startup
});

Switching workspaces at runtime

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

// User switches workspace / organisation
setActiveWorkspace('new-workspace');

// ProfileProvider re-renders and re-fetches automatically, along with every
// other provider that subscribes via useActiveWorkspace() — notably,
// react-auth's AuthProvider does NOT, so it will not auto-refetch here.

Clearing the workspace

setActiveWorkspace(null); // or setActiveWorkspace('')

TypeScript Types

export type Profile = {
  id: number;
  first_name: string;
  last_name: string;
  date_of_birth: string;
  gender: string;
  marital_status: string;
  phone?: string;
  address?: string;
  city?: string;
  state?: string;
  country?: string;
  postal_code?: string;
  avatar?: string;
  bio?: string;
  created_at: string;
  updated_at: string;
  [k: string]: unknown;
};

export type ProfilePayload = Omit<Profile, 'id' | 'created_at' | 'updated_at'>;

export type CreateProfilePayload = ProfilePayload;

export type UpdateProfilePayload = Partial<ProfilePayload>;

export type AvatarPayload = FormData;

Endpoint Defaults

Key Default HTTP Method
profile /profiles GET / POST / PUT / DELETE, plus GET/PUT on /{account_id}

The same base path is used for all profile operations — the current user's own profile (GET/POST/PUT/DELETE /profiles), the avatar upload (PUT /profiles/avatar), and looking up/updating another account's profile by id (GET/PUT /profiles/{id}, via getAccountProfile/useAccountProfile/updateAccountProfile). Override via configureApplication({ endpoints: { profile: { profile: '/custom-path' } } }).