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 SonamuProvider Config File
Create src/contexts/sonamu-provider.tsx in your project.
// src/contexts/sonamu-provider.tsx
import {
SonamuProvider as BaseSonamuProvider ,
useSonamuBaseContext ,
} from "@sonamu-kit/react-components" ;
import { type SonamuFile } from "@sonamu-kit/react-components" ;
import { type BetterAuthClientOptions } from "better-auth/client" ;
import { inferAdditionalFields } from "better-auth/client/plugins" ;
import { type ReactNode } from "react" ;
import { type MergedDictionary } from "@/i18n/sd.generated" ;
import { SD } from "@/i18n/sd.generated" ;
import { FileService } from "@/services/services.generated" ;
// Define better-auth client options
const authOptions = {
plugins: [
inferAdditionalFields ({
user: {
role: { type: "string" },
},
}),
],
} satisfies BetterAuthClientOptions ;
// Typed useSonamuContext
export function useSonamuContext () {
return useSonamuBaseContext < MergedDictionary , typeof authOptions >();
}
export function SonamuProvider ({ children } : { children : ReactNode }) {
const uploadMutation = FileService . useUploadMutation ();
const uploader = async ( files : File []) : Promise < SonamuFile []> => {
if ( files . length === 0 ) {
return [];
}
const result = await uploadMutation . mutateAsync ({ files });
return result . files ;
};
return (
< BaseSonamuProvider < MergedDictionary > authOptions = { authOptions } uploader = { uploader } SD = { SD } >
{ children }
</ BaseSonamuProvider >
);
}
2. Apply to App
Configure SonamuProvider in __root.tsx.
// src/routes/__root.tsx
import { QueryClientProvider } from "@tanstack/react-query" ;
import { createRootRouteWithContext , Outlet } from "@tanstack/react-router" ;
import { SonamuProvider } from "@/contexts/sonamu-provider" ;
export const Route = createRootRouteWithContext ()({
component: RootComponent ,
});
function RootComponent () {
const { queryClient } = Route . useRouteContext ();
return (
< QueryClientProvider client = { queryClient } >
< SonamuProvider >
< Outlet />
</ SonamuProvider >
</ QueryClientProvider >
);
}
Why create a separate SonamuProvider file? SonamuProvider uses React hooks like FileService.useUploadMutation(), so it must be placed under QueryClientProvider.
Separating it into its own file allows you to co-locate authOptions, useSonamuContext, and type definitions.
Auth Configuration
Sonamu uses better-auth for authentication. Pass authOptions to SonamuProvider and a better-auth client is created internally.
Defining authOptions
Define better-auth client options. You can extend user fields through plugins.
import { type BetterAuthClientOptions } from "better-auth/client" ;
import { inferAdditionalFields } from "better-auth/client/plugins" ;
const authOptions = {
plugins: [
inferAdditionalFields ({
user: {
role: { type: "string" },
// Add additional fields as needed
},
}),
],
} satisfies BetterAuthClientOptions ;
What is inferAdditionalFields? It adds project-specific fields (like role) to the client-side User type beyond better-auth defaults (id, name, email, etc.).
These should match the user schema defined on the server.
Login Flow
Use the better-auth client’s signIn method.
import { useSonamuContext } from "@/contexts/sonamu-provider" ;
export function LoginPage () {
const { auth } = useSonamuContext ();
const [ email , setEmail ] = React . useState ( "" );
const [ password , setPassword ] = React . useState ( "" );
const handleSubmit = async () => {
const result = await auth . signIn . email ({ email , password });
if ( result . error ) {
alert ( result . error . message );
return ;
}
};
return (
< form onSubmit = {(e) => { e . preventDefault (); handleSubmit (); }} >
< input value = { email } onChange = {(e) => setEmail (e.target.value)} />
< input type = "password" value = { password } onChange = {(e) => setPassword (e.target.value)} />
< button type = "submit" > Login </ button >
</ form >
);
}
Logout Flow
const handleLogout = () => {
auth . signOut ();
};
Using Session
Access the auth client via useSonamuContext in components, and use auth.useSession() to query the current session.
import { useSonamuContext } from "@/contexts/sonamu-provider" ;
export function Header () {
const { auth } = useSonamuContext ();
const session = auth . useSession ();
const user = session . data ?. user ?? null ;
if ( session . isPending ) {
return < div > Loading ...</ div > ;
}
if ( ! user ) {
return < a href = "/login" > Login </ a > ;
}
return (
< div >
< span > Welcome , { user . name } </ span >
< button onClick = {() => auth.signOut()} > 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 BaseSonamuProvider makes the SD function type-safe.
import type { MergedDictionary } from "@/i18n/sd.generated" ;
< BaseSonamuProvider < MergedDictionary > authOptions = { authOptions } uploader = { uploader } SD = { SD } >
{ children }
</ BaseSonamuProvider >
Auto-completion
// Specify Dictionary and authOptions types in useSonamuBaseContext.
const { SD } = useSonamuBaseContext < MergedDictionary , typeof authOptions >();
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 SonamuProvider ({ children } : { children : ReactNode }) {
return (
< BaseSonamuProvider < MergedDictionary > authOptions = { authOptions } SD = { SD } >
{ children }
</ BaseSonamuProvider >
);
// 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, authOptions can be omitted.
export function SonamuProvider ({ children } : { children : ReactNode }) {
return (
< BaseSonamuProvider < MergedDictionary > SD = { SD } >
{ children }
</ BaseSonamuProvider >
);
}
Display Loading State
import { useSonamuContext } from "@/contexts/sonamu-provider" ;
export function App ({ children } : { children : React . ReactNode }) {
const { auth } = useSonamuContext ();
const session = auth . useSession ();
const user = session . data ?. user ?? null ;
// Initial loading
if ( session . isPending ) {
return (
< div className = "flex items-center justify-center h-screen" >
< Spinner />
</ div >
);
}
// Authentication required
if ( ! user ) {
return < LoginPage />;
}
// Authenticated
return children ;
}
Customize Redirect
You can dynamically determine the redirect path after successful login.
const handleLogin = async ( email : string , password : string ) => {
const result = await auth . signIn . email ({ email , password });
if ( result . error ) {
alert ( result . error . message );
return ;
}
// Navigate to different pages based on role
const session = await auth . getSession ();
if ( session . data ?. 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 : Pass uploader to SonamuProvider.
Error: [SonamuProvider] auth is not configured.
Please provide auth configuration to SonamuProvider.
Solution : Pass authOptions to SonamuProvider.
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