Learn how to configure SonamuProvider, the starting point for Sonamu frontend integration. Configure authentication, file uploads, and internationalization in one place.
Setup Overview
Auth Integration Connect UserService.useMe Login/logout flow
File Upload FileService integration Auto-connect to useTypeForm
Internationalization SD function provider Type-safe translations
Global State Context API based Access from all components
Basic Setup
1. Create Config File
Create src/config/sonamu-provider.config.ts in your project.
// src/config/sonamu-provider.config.ts
import type { SonamuAuth , SonamuContextValue , SonamuFile } from "@sonamu-kit/react-components" ;
import { useQueryClient } from "@tanstack/react-query" ;
import { useNavigate } from "@tanstack/react-router" ;
import type { DictKey , MergedDictionary } from "@/i18n/sd.generated" ;
import { SD } from "@/i18n/sd.generated" ;
import { FileService , UserService } from "@/services/services.generated" ;
import type { UserSubsetSS } from "@/services/sonamu.generated" ;
import type { UserLoginParams } from "@/services/user/user.types" ;
export function createSonamuConfig () : SonamuContextValue < MergedDictionary > {
// Auth config
const queryClient = useQueryClient ();
const navigate = useNavigate ();
const { data : user , isLoading , refetch } = UserService . useMe ();
const loginMutation = UserService . useLoginMutation ();
const logoutMutation = UserService . useLogoutMutation ();
const auth_config : SonamuAuth < UserSubsetSS , UserLoginParams > = {
user: user ?? null ,
loading: isLoading || loginMutation . isPending || logoutMutation . isPending ,
login : ( loginParams : UserLoginParams ) => {
loginMutation . mutate (
{ params: loginParams },
{
onSuccess : async ({ user : _user }) => {
await queryClient . invalidateQueries ({ queryKey: [ "User" , "me" ] });
await queryClient . refetchQueries ({ queryKey: [ "User" , "me" ] });
navigate ({ to: "/admin" , replace: true });
},
onError : ( error ) => {
console . error ( "Login failed:" , error );
alert ( SD ( "user.login.failed" ));
},
},
);
},
logout : () => {
logoutMutation . mutate ( undefined , {
onSuccess : async () => {
await queryClient . invalidateQueries ({ queryKey: [ "User" , "me" ] });
await queryClient . refetchQueries ({ queryKey: [ "User" , "me" ] });
},
onError : ( error ) => {
console . error ( "Logout failed:" , error );
alert ( SD ( "user.logout.failed" ));
},
});
},
refetch ,
};
// Uploader config
const uploadMutation = FileService . useUploadMutation ();
const uploader_config = async ( files : File []) : Promise < SonamuFile []> => {
if ( files . length === 0 ) {
return [];
}
const result = await uploadMutation . mutateAsync ({ files });
return result . files ;
};
// SD config
const sd_config = < K extends DictKey >( key : K ) : ReturnType < typeof SD < K >> => SD ( key );
return { auth: auth_config , uploader: uploader_config , SD: sd_config };
}
2. Apply to App
Configure SonamuProvider in __root.tsx.
// src/routes/__root.tsx
import { SonamuProvider } from "@sonamu-kit/react-components" ;
import { QueryClientProvider } from "@tanstack/react-query" ;
import { createRootRouteWithContext , Outlet } from "@tanstack/react-router" ;
import { createSonamuConfig } from "@/config/sonamu-provider.config" ;
import type { MergedDictionary } from "@/i18n/sd.generated" ;
export const Route = createRootRouteWithContext ()({
component: RootComponent ,
});
function RootComponent () {
const { queryClient } = Route . useRouteContext ();
return (
< QueryClientProvider client = { queryClient } >
< SonamuProviderWrapper >
< Outlet />
</ SonamuProviderWrapper >
</ QueryClientProvider >
);
}
function SonamuProviderWrapper ({ children } : { children : React . ReactNode }) {
const sonamuConfig = createSonamuConfig ();
return < SonamuProvider < MergedDictionary > { ... sonamuConfig }>{ children } </ SonamuProvider > ;
}
Why create a separate SonamuProviderWrapper? createSonamuConfig uses React hooks (useQueryClient, useNavigate, etc.), so it must be called inside a component.
Therefore, it’s separated into a component and placed under QueryClientProvider.
Auth Configuration
Auth Interface
export type SonamuAuth < TUser = any , TLoginParams = any > = {
user : TUser | null ;
loading : boolean ;
login : ( params : TLoginParams ) => void ;
logout : () => void ;
refetch : () => void ;
};
Specify User Type
Use the auto-generated User Subset.
import type { UserSubsetSS } from "@/services/sonamu.generated" ;
const auth_config : SonamuAuth < UserSubsetSS , UserLoginParams > = {
// UserSubsetSS: Session Summary (minimal info for session)
// Example: { id, username, email, role }
user: user ?? null ,
// ...
};
What is Subset “SS”? Short for Session Summary, it includes only the minimal user information needed for session management.
It’s lighter and faster than loading the complete user information (Subset “A”).
Login Flow
login : ( loginParams : UserLoginParams ) => {
loginMutation . mutate (
{ params: loginParams },
{
onSuccess : async ({ user : _user }) => {
// 1. Invalidate cache
await queryClient . invalidateQueries ({ queryKey: [ "User" , "me" ] });
// 2. Refetch latest user info
await queryClient . refetchQueries ({ queryKey: [ "User" , "me" ] });
// 3. Navigate to admin page
navigate ({ to: "/admin" , replace: true });
},
onError : ( error ) => {
console . error ( "Login failed:" , error );
alert ( SD ( "user.login.failed" ));
},
},
);
},
Key Steps :
loginMutation.mutate: Call backend login API
invalidateQueries: Invalidate existing user info cache
refetchQueries: Fetch new user info
navigate: Redirect on success
Logout Flow
logout : () => {
logoutMutation . mutate ( undefined , {
onSuccess : async () => {
// Refetch user info (becomes null)
await queryClient . invalidateQueries ({ queryKey: [ "User" , "me" ] });
await queryClient . refetchQueries ({ queryKey: [ "User" , "me" ] });
},
onError : ( error ) => {
console . error ( "Logout failed:" , error );
alert ( SD ( "user.logout.failed" ));
},
});
},
Using Auth
Access auth via useSonamuContext in components.
import { useSonamuContext } from "@sonamu-kit/react-components" ;
export function Header () {
const { auth } = useSonamuContext ();
if ( auth . loading ) {
return < div > Loading ...</ div > ;
}
if ( ! auth . user ) {
return (
< button onClick = {() => auth.login({ email : "[email protected] " , password : "password" })} >
Login
</ button >
);
}
return (
< div >
< span > Welcome , { auth . user . username } </ span >
< button onClick = {() => auth.logout()} > Logout </ button >
</ div >
);
}
File Upload Configuration
Uploader Interface
type Uploader = ( files : File []) => Promise < SonamuFile []>;
type SonamuFile = {
name : string ;
url : string ;
mime_type : string ;
size : number ;
};
FileService Integration
const uploadMutation = FileService . useUploadMutation ();
const uploader_config = async ( files : File []) : Promise < SonamuFile []> => {
if ( files . length === 0 ) {
return [];
}
const result = await uploadMutation . mutateAsync ({ files });
return result . files ;
};
How it works :
Upload files to backend with mutateAsync
Return uploaded file info (SonamuFile[])
useTypeForm automatically uses this uploader
When uploader is configured, useTypeForm’s submit automatically uploads files.
import { useTypeForm } from "@sonamu-kit/react-components" ;
import { FileInput } from "@sonamu-kit/react-components/components" ;
export function UserForm () {
const { form , setForm , register , submit } = useTypeForm ( UserSaveParams , {
username: "" ,
avatar: null , // SonamuFile | null
});
return (
< form
onSubmit = {(e) => {
e . preventDefault ();
submit ( async ( formData ) => {
// formData.avatar is already uploaded as SonamuFile
await UserService . save ({ params: formData });
});
}}
>
< Input { ... register (" username ")} />
{ /* FileInput: File → SonamuFile conversion handled by submit */ }
< FileInput { ... register (" avatar ")} />
< button type = "submit" > Save </ button >
</ form >
);
}
Auto-upload Mechanism The submit function internally calls traverseAndUploadFiles to find and upload all File objects.
Files in nested objects or arrays are automatically handled.
Custom Upload Logic
You can implement your own if using a different upload service.
// AWS S3 direct upload example
import { S3Client , PutObjectCommand } from "@aws-sdk/client-s3" ;
const s3Client = new S3Client ({ region: "ap-northeast-2" });
const uploader_config = async ( files : File []) : Promise < SonamuFile []> => {
const uploadPromises = files . map ( async ( file ) => {
const key = `uploads/ ${ Date . now () } - ${ file . name } ` ;
await s3Client . send (
new PutObjectCommand ({
Bucket: "my-bucket" ,
Key: key ,
Body: file ,
ContentType: file . type ,
})
);
return {
name: file . name ,
url: `https://my-bucket.s3.amazonaws.com/ ${ key } ` ,
mime_type: file . type ,
size: file . size ,
};
});
return Promise . all ( uploadPromises );
};
Internationalization (SD)
SD Function
The SD (Sonamu Dictionary) function provides type-safe translations.
const sd_config = < K extends DictKey >( key : K ) : ReturnType < typeof SD < K >> => SD ( key );
Usage Example
import { useSonamuContext } from "@sonamu-kit/react-components" ;
export function WelcomeMessage () {
const { SD } = useSonamuContext ();
return (
< div >
< h1 >{ SD ( "common.welcome" )} </ h1 >
< p >{ SD ( "user.login.prompt" )} </ p >
</ div >
);
}
Why provide SD through Context? While you can import and use it directly, using Context makes it easy to add runtime locale switching or dynamic dictionary loading in the future.
Type Safety
Specify Generic Type
Specifying the Dictionary type in SonamuProvider makes the SD function type-safe.
import type { MergedDictionary } from "@/i18n/sd.generated" ;
< SonamuProvider < MergedDictionary > { ... sonamuConfig } >
{ children }
</ SonamuProvider >
Auto-completion
const { SD } = useSonamuContext < MergedDictionary >();
SD ( "
↓
common . welcome
common . save
common . cancel
user . login . failed
user . logout . failed
entity . User . email
...
Advanced Configuration
Minimal Setup (Auth Only)
You can omit uploader if file uploads are not needed.
export function createSonamuConfig () : SonamuContextValue < MergedDictionary > {
const auth_config = { /* ... */ };
const sd_config = SD ;
return { auth: auth_config , SD: sd_config };
// uploader omitted (fallback function auto-configured)
}
Omitting uploader will cause errors when using FileInput.
Always configure it if file upload functionality is needed.
Using Without Auth
If authentication isn’t needed, auth can also be omitted.
export function createSonamuConfig () : SonamuContextValue < MergedDictionary > {
const sd_config = SD ;
return { SD: sd_config };
}
Display Loading State
export function App ({ children } : { children : React . ReactNode }) {
const { auth } = useSonamuContext ();
// Initial loading
if ( auth . loading ) {
return (
< div className = "flex items-center justify-center h-screen" >
< Spinner />
</ div >
);
}
// Authentication required
if ( ! auth . user ) {
return < LoginPage />;
}
// Authenticated
return children ;
}
Customize Redirect
You can dynamically determine the redirect path after successful login.
login : ( loginParams : UserLoginParams ) => {
loginMutation . mutate (
{ params: loginParams },
{
onSuccess : async ({ user : _user }) => {
await queryClient . invalidateQueries ({ queryKey: [ "User" , "me" ] });
await queryClient . refetchQueries ({ queryKey: [ "User" , "me" ] });
// Navigate to different pages based on role
if ( _user . role === "admin" ) {
navigate ({ to: "/admin" , replace: true });
} else {
navigate ({ to: "/dashboard" , replace: true });
}
},
},
);
},
Troubleshooting
Error: [SonamuProvider] uploader is not configured.
Please provide uploader configuration to SonamuProvider.
Solution : Return uploader_config in createSonamuConfig.
Error: [SonamuProvider] auth is not configured.
Please provide auth configuration to SonamuProvider.
Solution : Return auth_config in createSonamuConfig.
Cannot Find QueryClient
Error: useQueryClient must be used within a QueryClientProvider
Solution : Place SonamuProvider inside QueryClientProvider.
< QueryClientProvider client = { queryClient } >
< SonamuProvider { ... config } >
{ children }
</ SonamuProvider >
</ QueryClientProvider >
Next Steps