Learn how to use the ID Async Select component for handling foreign key relationships.
ID Async Select Overview
Async Loading API calls Autocomplete
Type Safe Entity based ID type guaranteed
Search Support Real-time filtering Debounce
Multiple Selection Single/Multiple modes Array return
What is ID Async Select?
Problem: Complexity of Foreign Key Selection
When handling foreign key relationships in databases, the frontend needs to select related entities .
Problems with traditional approach :
// ❌ Load all users at once (inefficient)
const [ users , setUsers ] = useState ([]);
useEffect (() => {
userService . list ({ pageSize: 1000 }). then (({ users }) => {
setUsers ( users ); // Load all 1000 users!
});
}, []);
return (
< select >
{ users . map (( user ) => (
< option key = {user. id } value = {user. id } >
{ user . username }
</ option >
))}
</ select >
);
Problems :
Performance : Loading time increases with more data
Memory : All unnecessary data loaded into memory
UX : Hard for users to find desired items
Scalability : Can’t handle continuous data growth
Solution: ID Async Select
Sonamu’s ID Async Select loads asynchronously only when needed and provides search functionality .
// ✅ Load only needed data based on search terms
import { IdAsyncSelect } from "@sonamu-kit/react-components" ;
import { UserAsyncIdConfig } from "@/services/services.generated" ;
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { selectedUserId }
onValueChange = { setSelectedUserId }
/>
Benefits :
✨ Performance : Load minimum initially, only what’s needed on search
✨ User experience : Quick selection with autocomplete
✨ Type safe : Entity type automatically applied
✨ Scalability : No problem regardless of data volume
Basic Usage
Required Props
ID Async Select uses these two core props:
config: AsyncIdConfig object auto-generated from services.generated.ts
subset: Subset key to query (e.g., “A”, “D”, etc.)
import { IdAsyncSelect } from "@sonamu-kit/react-components" ;
import { UserAsyncIdConfig } from "@/services/services.generated" ;
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { userId }
onValueChange = { setUserId }
/>
Single Selection
Select one entity.
import { IdAsyncSelect } from "@sonamu-kit/react-components" ;
import { UserAsyncIdConfig } from "@/services/services.generated" ;
import { useState } from "react" ;
function PostForm () {
const [ authorId , setAuthorId ] = useState < number | null >( null );
return (
< div >
< label > Author </ label >
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { authorId }
onValueChange = { setAuthorId }
displayField = "username"
placeholder = "Select author..."
/>
</ div >
);
}
How it works :
Call search API when user types
Display search results in dropdown
Only save ID when selected (memory efficient)
Multiple Selection
Select multiple entities.
import { TagAsyncIdConfig } from "@/services/services.generated" ;
function PostForm () {
const [ tagIds , setTagIds ] = useState < number []>([]);
return (
< div >
< label > Tags </ label >
< IdAsyncSelect
config = { TagAsyncIdConfig }
subset = "A"
value = { tagIds }
onValueChange = { setTagIds }
displayField = "name"
multiple
placeholder = "Select tags..."
/>
</ div >
);
}
Multiple selection features :
Display selected items as tags
Remove individually with X button
Return IDs as array (number[])
Setting Initial Values
Set initial values when editing existing data.
function EditPostForm ({ post } : { post : Post }) {
const [ authorId , setAuthorId ] = useState ( post . author_id );
return (
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { authorId }
onValueChange = { setAuthorId }
displayField = "username"
/>
);
}
Initial value handling :
Passing ID to value automatically loads the entity
Background single query for label display
Display only ID while loading
displayField Options
Specify by Field Name
The simplest way - specify the field name as a string.
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { userId }
onValueChange = { setUserId }
displayField = "username"
/>
Specify by Callback Function
Use a callback function when complex labels are needed.
< IdAsyncSelect
config = { EmployeeAsyncIdConfig }
subset = "A"
value = { employeeId }
onValueChange = { setEmployeeId }
displayField = {(row) =>
` ${ row . employee_number } (Department: ${ row . department ?. name } )`
}
/>
Auto Detection (Omit displayField)
When displayField is omitted, an appropriate field is automatically detected.
// Auto detection applied when displayField is omitted
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { userId }
onValueChange = { setUserId }
/>
Detection priority :
name-like fields: name, title, label, display_name, username
First string type column (excluding id)
fallback: id
Advanced Usage
Pass Search Conditions with baseListParams
Set default search parameters.
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { userId }
onValueChange = { setUserId }
displayField = "username"
// Apply additional conditions on search
baseListParams = {{
search : "username" ,
role : "admin"
}}
/>
Get Full Row Data with onRowChange
When you need the full selected Row data, not just the ID:
function SelectWithRowData () {
const [ userId , setUserId ] = useState < number | null >( null );
const [ selectedUser , setSelectedUser ] = useState < User | undefined >();
return (
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { userId }
onValueChange = { setUserId }
onRowChange = {(row) => setSelectedUser ( row as User )}
displayField = "username"
/>
);
}
Change valueField
By default, the id field is used as the value, but you can use a different field.
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { userUuid }
onValueChange = { setUserUuid }
valueField = "uuid"
displayField = "username"
/>
Type Safety
AsyncIdConfig Structure
The Config auto-generated from services.generated.ts has this structure:
// Auto-generated in services.generated.ts
export const UserAsyncIdConfig : AsyncIdConfig <
"A" | "D" , // TSubsetKey - Available Subset keys
UserSubsetMapping , // TSubsetMapping - Subset type mapping
UserListParams // TListParams - Search parameter types
> = {
placeholderKey: "user.placeholder" ,
useList: UserService . useList ,
};
Utilizing Type Inference
Types are automatically inferred through Config:
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A" // Choose from "A" | "D"
displayField = "username" // Auto-complete only UserSubsetA fields
baseListParams = {{ role : "admin" }} // Auto-complete based on UserListParams
value = { userId }
onValueChange = { setUserId }
onRowChange = {(row) => {
// row type is inferred as UserSubsetA
console . log ( row . username );
}}
/>
Practical Examples
Post Creation Form
import { useState } from "react" ;
import { IdAsyncSelect } from "@sonamu-kit/react-components" ;
import {
UserAsyncIdConfig ,
CategoryAsyncIdConfig ,
TagAsyncIdConfig
} from "@/services/services.generated" ;
import { postService } from "@/services/post.service" ;
function CreatePostForm () {
const [ title , setTitle ] = useState ( "" );
const [ content , setContent ] = useState ( "" );
const [ authorId , setAuthorId ] = useState < number | null >( null );
const [ categoryId , setCategoryId ] = useState < number | null >( null );
const [ tagIds , setTagIds ] = useState < number []>([]);
async function handleSubmit () {
if ( ! authorId || ! categoryId ) {
alert ( "Please select author and category" );
return ;
}
await postService . create ({
title ,
content ,
author_id: authorId ,
category_id: categoryId ,
tag_ids: tagIds ,
});
}
return (
< form onSubmit = { handleSubmit } >
< div >
< label > Title </ label >
< input
value = { title }
onChange = {(e) => setTitle (e.target.value)}
/>
</ div >
< div >
< label > Content </ label >
< textarea
value = { content }
onChange = {(e) => setContent (e.target.value)}
/>
</ div >
{ /* Author selection (single) */ }
< div >
< label > Author * </ label >
< IdAsyncSelect
config = { UserAsyncIdConfig }
subset = "A"
value = { authorId }
onValueChange = { setAuthorId }
displayField = "username"
placeholder = "Select author..."
/>
</ div >
{ /* Category selection (single) */ }
< div >
< label > Category * </ label >
< IdAsyncSelect
config = { CategoryAsyncIdConfig }
subset = "A"
value = { categoryId }
onValueChange = { setCategoryId }
displayField = "name"
placeholder = "Select category..."
/>
</ div >
{ /* Tag selection (multiple) */ }
< div >
< label > Tags </ label >
< IdAsyncSelect
config = { TagAsyncIdConfig }
subset = "A"
value = { tagIds }
onValueChange = { setTagIds }
displayField = "name"
multiple
placeholder = "Select tags..."
/>
</ div >
< button type = "submit" > Create Post </ button >
</ form >
);
}
Hierarchical Selection
Pattern of selecting child after parent.
import {
DepartmentAsyncIdConfig ,
EmployeeAsyncIdConfig
} from "@/services/services.generated" ;
function EmployeeAssignForm () {
const [ departmentId , setDepartmentId ] = useState < number | null >( null );
const [ employeeId , setEmployeeId ] = useState < number | null >( null );
return (
< div >
{ /* Step 1: Department selection */ }
< div >
< label > Department </ label >
< IdAsyncSelect
config = { DepartmentAsyncIdConfig }
subset = "A"
value = { departmentId }
onValueChange = {(id) => {
setDepartmentId ( id );
setEmployeeId ( null ); // Reset employee when department changes
}}
displayField = "name"
/>
</ div >
{ /* Step 2: Employee selection (only after department selected) */ }
{ departmentId && (
< div >
< label > Employee </ label >
< IdAsyncSelect
config = { EmployeeAsyncIdConfig }
subset = "A"
value = { employeeId }
onValueChange = { setEmployeeId }
displayField = "employee_number"
// Query only employees in selected department
baseListParams = {{ department_id : departmentId }}
/>
</ div >
)}
</ div >
);
}
Example using with Sonamu’s useForm.
import { useForm } from "@sonamu-kit/react-components" ;
import { CompanyAsyncIdConfig } from "@/services/services.generated" ;
function CompanySelectForm () {
const form = useForm ({
defaultValues: { company_id: null as number | null },
});
return (
< IdAsyncSelect
config = { CompanyAsyncIdConfig }
subset = "A"
displayField = "name"
{ ... form . register (" company_id ")}
placeholder = "Search for a company"
/>
);
}
Props Reference
Prop Type Required Description configAsyncIdConfig✓ Entity’s AsyncIdConfig object subsetstring✓ Subset key to query valueTValue | TValue[] | nullSelected value onValueChange(value) => voidValue change callback onRowChange(row) => voidRow data change callback displayFieldstring | ((row) => string)Display field (auto-detected if omitted) valueFieldstringValue field (default: “id”) baseListParamsobjectAdditional parameters for search multiplebooleanMultiple selection mode placeholderstringPlaceholder text clearablebooleanWhether selection can be cleared disabledbooleanDisabled state classNamestringAdditional CSS class
Cautions
Cautions when using ID Async Select : 1. config should use objects auto-generated from
services.generated.ts 2. subset must be a valid Subset key defined in the Entity 3. Verify
array type for multiple selection 4. Field specified in displayField must be included in the
Subset 5. Error handling required for initial value loading failure
Next Steps
View Scaffolding Auto view generation
Search Input Search component
Custom Components Component customization
Using Services Service integration