Async Loading
Load options dynamically from your Laravel backend API endpoints.
Basic Setup
1. Define the Endpoint
<livewire:async-select
name="user_id"
wire:model="selectedUser"
endpoint="/api/users/search"
placeholder="Search users..."
/>
2. Create the Controller
If your endpoint requires authentication, you MUST apply the async-auth middleware. Without it, internal authentication tokens will not be verified and users will not be authenticated.
// routes/api.php or routes/web.php
use App\Models\User;
use Illuminate\Http\Request;
// ✅ Apply middleware for authenticated routes
Route::middleware(['async-auth'])->get('/api/users/search', function (Request $request) {
$search = $request->get('search', '');
// User is now authenticated (via internal auth or normal auth)
$user = auth()->user();
$users = User::query()
->when($search, function($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->limit(20)
->get()
->map(function($user) {
return [
'value' => $user->id,
'label' => $user->name,
'email' => $user->email,
'image' => $user->avatar_url
];
});
return response()->json(['data' => $users]);
});
Note: The async-auth middleware is automatically registered by the package and works exactly like auth middleware, but also handles internal authentication automatically when the X-Internal-User header is present.
Without middleware, authentication won't work:
// ❌ No middleware - authentication won't work
Route::get('/api/users/search', function (Request $request) {
// auth()->user() will be null even if X-Internal-User header is present
});
Using with different guards:
// Default guard
Route::middleware(['async-auth'])->get('/api/users/search', ...);
// Sanctum
Route::middleware(['async-auth:sanctum'])->get('/api/users/search', ...);
// Web guard with session
Route::middleware(['web', 'async-auth:web'])->get('/api/users/search', ...);
// Multiple guards
Route::middleware(['async-auth:web,sanctum'])->get('/api/users/search', ...);
Response Format
Your endpoint must return JSON in this format:
{
"data": [
{
"value": "1",
"label": "John Doe"
},
{
"value": "2",
"label": "Jane Smith"
}
]
}
Auto-Detection of Fields
The component automatically detects common field names:
For value field (in order of priority):
idvalue- array key
For label field (in order of priority):
titlenamelabeltext
This means you can return data like this without extra configuration:
{
"data": [
{
"id": 1,
"name": "John Doe"
},
{
"id": 2,
"name": "Jane Smith"
}
]
}
Custom Field Names
If your API uses different field names, specify them explicitly:
<livewire:async-select
endpoint="/api/products"
value-field="sku"
label-field="title"
/>
Your API can then return:
{
"data": [
{
"sku": "PROD-001",
"title": "Product Name",
"price": 99.99
}
]
}
With Additional Fields
{
"data": [
{
"value": "1",
"label": "John Doe",
"email": "john@example.com",
"image": "https://example.com/avatar.jpg",
"role": "Admin"
}
]
}
Selected Items Endpoint
When editing forms, you need to load already-selected items. You have two options:
Option 1: Using selected-endpoint (API Call)
Load selected items from an endpoint:
<livewire:async-select
wire:model="userId"
endpoint="/api/users/search"
selected-endpoint="/api/users/selected"
/>
The selected endpoint receives the current value:
Route::middleware(['async-auth'])->get('/api/users/selected', function (Request $request) {
$selected = $request->get('selected');
$users = User::whereIn('id', (array) $selected)
->get()
->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
'image' => $user->avatar_url
]);
return response()->json(['data' => $users]);
});
<|tool▁calls▁begin|><|tool▁call▁begin|> read_file
Option 2: Using value-labels (No API Call)
Version 1.1.0 Feature
Use value-labels to provide labels directly without making any API requests. Perfect when you already know the labels.
If you already have the labels (e.g., from the form data or previous API calls), use value-labels to avoid the API request:
<livewire:async-select
wire:model="userId"
endpoint="/api/users/search"
:value-labels="[
5 => 'John Doe',
7 => 'Jane Smith'
]"
/>
Benefits of value-labels:
- ✅ No API requests - Labels displayed immediately
- ✅ Better performance - Reduces network traffic
- ✅ Works with pre-selected values - Labels show on mount
- ✅ Perfect for edit forms - Use existing data from the model
When to use each:
- Use
value-labelswhen you already have the labels (form data, model attributes, etc.) - Use
selected-endpointwhen labels need to be fetched from the server or might change
Learn more about value-labels →
Configuration
Minimum Search Length
Require a minimum number of characters before triggering search:
<livewire:async-select
endpoint="/api/search"
:min-search-length="3"
/>
Search Parameter Name
Customize the query parameter name:
<livewire:async-select
endpoint="/api/search"
search-param="q"
/>
Your endpoint will receive: /api/search?q=searchterm
Selected Parameter Name
Customize the parameter for selected items:
<livewire:async-select
selected-endpoint="/api/selected"
selected-param="ids"
/>
Auto-load
Load options immediately on mount:
<livewire:async-select
endpoint="/api/popular-items"
:autoload="true"
/>
Reload Options
You can programmatically reload options from your Livewire component:
// In your Livewire component
public function refreshUsers()
{
$this->dispatch('reload-options')->to('async-select');
}
Or call the reload method directly if you have a reference:
<button wire:click="$wire.call('reload')">Refresh</button>
Extra Parameters
Pass additional parameters to your endpoints:
<livewire:async-select
endpoint="/api/cities/search"
:extra-params="[
'country_id' => $countryId,
'active' => true
]"
/>
Your endpoint receives:
/api/cities/search?search=london&country_id=1&active=1
Custom Headers
Pass custom headers (e.g., for authentication) with HTTP requests:
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:headers="[
'Authorization' => 'Bearer ' . $token,
'X-Custom-Header' => 'custom-value'
]"
/>
Learn more about headers and authentication →
Internal Authentication
For secure internal API requests, use internal authentication:
<livewire:async-select
endpoint="/api/users/search"
wire:model="userId"
:use-internal-auth="true"
/>
This automatically generates signed tokens for authenticated users when making requests to endpoints on the same domain.
Learn more about internal authentication →
Dynamic Parameters
Use Livewire properties for dynamic values:
class MyComponent extends Component
{
public $countryId;
public $selectedCity;
public function render()
{
return view('livewire.my-component');
}
}
<livewire:async-select
wire:model="selectedCity"
endpoint="/api/cities/search"
:extra-params="['country_id' => $this->countryId]"
/>
Error Handling
The component automatically handles:
- Network errors
- Invalid responses
- Timeouts
Display user-friendly messages by catching errors in your endpoint:
Route::middleware(['async-auth'])->get('/api/search', function (Request $request) {
try {
// Your logic...
return response()->json(['data' => $results]);
} catch (\Exception $e) {
return response()->json([
'error' => 'Failed to load options'
], 500);
}
});
Pagination
The component supports pagination for loading large datasets efficiently.
Basic Pagination with paginate()
Route::middleware(['async-auth'])->get('/api/users/search', function (Request $request) {
$search = $request->get('search', '');
$page = $request->get('page', 1);
$perPage = $request->get('per_page', 20);
$users = User::query()
->when($search, function($query, $search) {
$query->where('name', 'like', "%{$search}%");
})
->paginate($perPage, ['*'], 'page', $page);
return response()->json([
'data' => $users->items(),
'current_page' => $users->currentPage(),
'last_page' => $users->lastPage(),
'total' => $users->total(),
'per_page' => $users->perPage(),
]);
});
Component Configuration
<livewire:async-select
endpoint="/api/users/search"
:per-page="20"
wire:model="userId"
/>
Load More (Infinite Scroll)
The component automatically detects pagination and shows a "Load More" button:
Route::middleware(['async-auth'])->get('/api/products/search', function (Request $request) {
$search = $request->get('search', '');
$page = $request->get('page', 1);
$products = Product::query()
->when($search, fn($q) => $q->where('name', 'like', "%{$search}%"))
->paginate(15);
return response()->json([
'data' => $products->map(fn($product) => [
'value' => $product->id,
'label' => $product->name,
'price' => $product->price,
]),
'current_page' => $products->currentPage(),
'last_page' => $products->lastPage(),
]);
});
Supported Pagination Formats
The component supports multiple pagination response formats:
Laravel Paginator (Recommended):
{
"data": [...],
"current_page": 1,
"last_page": 5
}
Custom Format with has_more:
{
"data": [...],
"has_more": true
}
Laravel API Resources:
{
"data": [...],
"meta": {
"current_page": 1,
"last_page": 5,
"total": 100
}
}
Complete Pagination Example
Controller:
namespace App\Http\Controllers\Api;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function search(Request $request)
{
$validated = $request->validate([
'search' => 'nullable|string|max:255',
'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:5|max:100',
]);
$search = $validated['search'] ?? '';
$perPage = $validated['per_page'] ?? 20;
$users = User::query()
->when($search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->orderBy('name')
->paginate($perPage);
return response()->json([
'data' => $users->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
'email' => $user->email,
'image' => $user->avatar_url,
]),
'current_page' => $users->currentPage(),
'last_page' => $users->lastPage(),
'per_page' => $users->perPage(),
'total' => $users->total(),
]);
}
}
Livewire Component:
use Livewire\Component;
class UserSelector extends Component
{
public $userId;
public function render()
{
return view('livewire.user-selector');
}
}
Blade View:
<div>
<livewire:async-select
wire:model.live="userId"
endpoint="/api/users/search"
:per-page="25"
:min-search-length="2"
placeholder="Search users..."
searchable
/>
@if($userId)
<p>Selected User ID: {{ $userId }}</p>
@endif
</div>
Performance Tips
1. Optimize Database Queries
$users = User::query()
->select(['id', 'name', 'email', 'avatar']) // Only select needed columns
->where('name', 'like', "%{$search}%")
->limit(20)
->get();
2. Add Database Indexes
Schema::table('users', function (Blueprint $table) {
$table->index('name');
$table->index('email');
});
3. Cache Results
$cacheKey = "user_search_{$search}";
$users = Cache::remember($cacheKey, 300, function() use ($search) {
return User::where('name', 'like', "%{$search}%")->get();
});
4. Use Resource Classes
use App\Http\Resources\UserResource;
return response()->json([
'data' => UserResource::collection($users)
]);
Complete Example
namespace App\Http\Controllers\Api;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class UserSearchController extends Controller
{
public function search(Request $request)
{
$search = $request->get('search');
$role = $request->get('role');
$users = User::query()
->when($search, fn($q) => $q->where('name', 'like', "%{$search}%"))
->when($role, fn($q) => $q->where('role', $role))
->with('avatar')
->limit(20)
->get()
->map(function($user) {
return [
'value' => $user->id,
'label' => $user->name,
'email' => $user->email,
'role' => $user->role,
'image' => $user->avatar?->url
];
});
return response()->json(['data' => $users]);
}
public function selected(Request $request)
{
$ids = $request->get('selected', []);
$users = User::whereIn('id', $ids)
->get()
->map(fn($user) => [
'value' => $user->id,
'label' => $user->name,
'image' => $user->avatar?->url
]);
return response()->json(['data' => $users]);
}
}