Skip to main content
Learn how to use the ID Async Select component for handling foreign key relationships.

ID Async Select Overview

Async Loading

API callsAutocomplete

Type Safe

Entity basedID type guaranteed

Search Support

Real-time filteringDebounce

Multiple Selection

Single/Multiple modesArray 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:
  1. Performance: Loading time increases with more data
  2. Memory: All unnecessary data loaded into memory
  3. UX: Hard for users to find desired items
  4. 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:
  1. Performance: Load minimum initially, only what’s needed on search
  2. User experience: Quick selection with autocomplete
  3. Type safe: Entity type automatically applied
  4. 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:
  1. Call search API when user types
  2. Display search results in dropdown
  3. 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:
  1. name-like fields: name, title, label, display_name, username
  2. First string type column (excluding id)
  3. 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>
  );
}

Using with Forms

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

PropTypeRequiredDescription
configAsyncIdConfigEntity’s AsyncIdConfig object
subsetstringSubset 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