1. מה משתנה ב-Livewire 4
Livewire 4 היא הגרסה הגדולה ביותר בתולדות הפריימוורק. מודל הקומפוננטה עצמו לא השתנה — מחלקת PHP ועוד תצוגת Blade, state בצד השרת, HTML על גבי הרשת — אבל כמעט כל מה שסביבו זז: היכן הקומפוננטות נמצאות, איך כותבים אותן, וכמה מהעמוד נוגע עדכון בודד.
חמשת הדברים החשובים
| יכולת | מה היא נותנת לכם |
|---|---|
| קומפוננטות בקובץ יחיד | המחלקה והמרקאפ נמצאים בקובץ .blade.php אחד. אין יותר קפיצות בין שתי תיקיות בשביל קומפוננטה של עשרים שורות. |
| Islands | אזורים מבודדים בתוך קומפוננטה שמתרנדרים מחדש בעצמם. מונה הכנסות מתעדכן כבר לא מריץ מחדש את השאילתות של כל הדשבורד. |
| Slots | קומפוננטת אב מזריקה מרקאפ לקומפוננטות בת — והוא מוערך בהקשר של האב, כך ש-wire:click בתוך slot קורא למתודה של האב. |
| Optimistic UI | wire:show, wire:text, wire:bind מעדכנים את ה-DOM מיד בצד הלקוח, בלי סבב מול השרת. |
| גרירה ושחרור | wire:sort מובנה. בלי SortableJS ובלי קוד דבק. |
האם צריך לשכתב את האפליקציה?
לא. Livewire 4 שומרת על תאימות לאחור חזקה: קומפוננטות מבוססות מחלקה ממשיכות לעבוד. קובץ יחיד הוא ברירת המחדל לקומפוננטות חדשות, לא מיגרציה כפויה. יש סדרה של שינויים שוברים שכדאי לקרוא לפני השדרוג — הם מרוכזים ב השוואה בין Livewire 3 ל-4, ו תיעוד השדרוג הרשמי הוא הרשימה המחייבת.
wire:model, islands ו-slots. שאר
מודל הקומפוננטה ירגיש מוכר.
2. התקנה ודרישות
composer require livewire/livewire:^4.0
php artisan optimize:clear
כמו בגרסה 3, ה-assets מוזרקים אוטומטית. את @livewireStyles ואת
@livewireScripts צריך למקם ידנית רק כשמשתמשים ב-layout לא סטנדרטי או
ב-Content Security Policy מחמיר.
/livewire/ הפך ל-/livewire-{hash}/. אם יש לכם חוקי firewall, החרגות
WAF, חוקי עקיפה ב-CDN או locations ב-nginx שמקובעים לנתיב המילולי /livewire/,
צריך להרחיב אותם לתבנית החדשה — אחרת האפליקציה תיראה שבורה ב-production בזמן שהיא עובדת
מצוין על הלפטופ שלכם.
פרסום קובץ הקונפיגורציה
php artisan livewire:publish --configהגדרות חדשות או ששמן שונה בגרסה 4:
| הגדרה | תפקיד |
|---|---|
component_locations | תיקיות שנסרקות לאיתור קומפוננטות. ברירת המחדל: resources/views/components ו-resources/views/livewire. |
component_namespaces | שורשי קומפוננטות בעלי שם, למשל pages::. |
component_layout | נקרא layout ב-v3. משתמש ב-namespace layouts::. |
component_placeholder | נקרא lazy_placeholder ב-v3. |
make_command | קובע מה make:livewire מייצר: קובץ יחיד או מבוסס מחלקה. |
smart_wire_keys | ברירת המחדל כעת היא true. |
csp_safe | מייצר build שעומד ב-Content Security Policies מחמירים יותר. |
בחירת סגנון הקומפוננטה כברירת מחדל
// Keep generating class-based components instead of single-file ones
'make_command' => [
'type' => 'class',
],3. קומפוננטות בקובץ יחיד
השינוי המרכזי בעבודה היומיומית. קומפוננטה היא כעת קובץ Blade אחד שנפתח בבלוק PHP המצהיר על מחלקה אנונימית, ואחריו המרקאפ.
php artisan make:livewire post.create
# resources/views/components/post/⚡create.blade.php<?php
use Livewire\Component;
use App\Models\Post;
use Livewire\Attributes\Validate;
new class extends Component {
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
public function save()
{
$this->validate();
Post::create([
'title' => $this->title,
'body' => $this->body,
'user_id' => auth()->id(),
]);
$this->reset();
$this->dispatch('notify', message: 'Post created');
}
};
?>
<div>
<form wire:submit="save">
<input type="text" wire:model="title" placeholder="Title">
@error('title') <span class="error">{{ $message }}</span> @enderror
<textarea wire:model="body" placeholder="Body"></textarea>
@error('body') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Save post</button>
</form>
</div>
מאפיינים ציבוריים זמינים במרקאפ כמשתנים רגילים — {{ $title }} עובד
בדיוק כמו בקובץ תצוגה נפרד.
לגבי האימוג'י
שם הקובץ כברירת מחדל מתחיל בקידומת ⚡. זהו סימון ויזואלי שמבדיל קומפוננטות
Livewire מקומפוננטות Blade רגילות באותה תיקייה, והוא
אופציונלי — אפשר לכבות אותו ב-config/livewire.php אם
כלי הפיתוח, הטרמינל או הצוות שלכם מעדיפים שמות פשוטים.
קומפוננטות מרובות קבצים
כשקומפוננטה גדלה מעבר למה שנוח להחזיק בקובץ אחד, צרו אותה כקומפוננטה מרובת קבצים וקבלו שוב מחלקה, תצוגה ובדיקה מופרדות.
php artisan make:livewire post.create --mfcקומפוננטות מבוססות מחלקה עדיין עובדות
<?php
namespace App\Livewire;
use Livewire\Component;
class CreatePost extends Component
{
public string $title = '';
public function render()
{
return view('livewire.create-post');
}
}app/Livewire.
4. מיקומים, שמות ו-namespaces
קומפוננטות Livewire כבר לא יושבות כברירת מחדל בסילו נפרד משלהן ב-resources/views/livewire;
הן נמצאות לצד שאר קומפוננטות ה-Blade שלכם.
resources/views/
├── components/
│ ├── ⚡counter.blade.php → <livewire:counter />
│ ├── post/
│ │ ├── ⚡create.blade.php → <livewire:post.create />
│ │ └── ⚡index.blade.php → <livewire:post.index />
│ └── button.blade.php (a plain Blade component)
└── pages/
└── ⚡dashboard.blade.php → <livewire:pages::dashboard />רינדור
{{-- By name --}}
<livewire:counter />
{{-- Nested directories use dots --}}
<livewire:post.create />
{{-- Namespaced --}}
<livewire:pages::post.create />
{{-- With props --}}
<livewire:post.create :title="$initialTitle" :author="$user" /><livewire:some-component> שלא נסגרה פשוט לא מתרנדרת — בלי שגיאה ובלי פלט. תמיד
השתמשו בסגירה עצמית: <livewire:some-component />. זו ההפתעה הנפוצה ביותר
בהעברת קבצי Blade מ-v3.
ניתוב לקומפוננטה
קומפוננטות עמוד מלא מקבלות מאקרו ניתוב ייעודי. הצורה הישנה
Route::get('/dashboard', Dashboard::class) מוחלפת ב:
use App\Livewire\Dashboard;
// By class
Route::livewire('/dashboard', Dashboard::class);
// By component name — the usual choice for view-based components
Route::livewire('/dashboard', 'pages::dashboard')
->middleware('auth')
->name('dashboard');Layouts
'component_layout' => 'layouts::app',use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
new class extends Component {
#[Layout('layouts::app')]
#[Title('Dashboard')]
public function render() { /* … */ }
};5. מאפיינים ו-props
מאפיינים ציבוריים הם עדיין ה-state של הקומפוננטה, עדיין עוברים סריאליזציה ל-snapshot בין
בקשות, ועדיין כפופים לאותן מגבלות טיפוסים: ערכים סקלריים, מערכים של סקלרים, מודלים
וקולקציות של Eloquent, Carbon, enums.
new class extends Component {
public string $name = '';
public ?int $age = null;
public array $tags = [];
public bool $isPublic = false;
};Props מהאב: #[Prop]
ערכים שמועברים מתבנית האב אפשר להצהיר במפורש. כך הממשק הציבורי של הקומפוננטה קריא במבט אחד, ונוצרת הפרדה בין "זה מגיע מבחוץ" לבין "זה state פנימי".
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $type = 'info';
#[Prop]
public bool $dismissible = false;
};
?>
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $slot }}
@if ($dismissible)
<button wire:click="$dispatch('dismiss')">×</button>
@endif
</div><livewire:alert type="warning" dismissible class="mt-4">
The invoice is overdue.
</livewire:alert>mount() עדיין רץ ראשון
use App\Models\Order;
new class extends Component {
public Order $order;
public string $mode;
public function mount(Order $order, string $mode = 'compact'): void
{
$this->order = $order;
$this->mode = $mode;
}
};#[Locked] חשוב כתמיד
use Livewire\Attributes\Locked;
new class extends Component {
#[Locked]
public int $invoiceId; // the client cannot substitute another id
public string $note = '';
};6. wire:model בגרסה 4
wire:model מתנהגים אחרת ב-v4, ושניהם נכשלים בשקט ולא ברעש.
שינוי 1: המודיפיירים שולטים כעת בסנכרון בצד הלקוח
ב-v3, .blur ו-.change קבעו רק מתי נשלחת בקשת רשת.
ב-v4 הם קובעים מתי הערך מסתנכרן בכלל — כולל בצד הלקוח. כדי לקבל את ההתנהגות
הישנה, הוסיפו .live.
{{-- Livewire 3 --}}
<input wire:model.blur="title">
{{-- The v4 equivalent of that behaviour --}}
<input wire:model.live.blur="title">שינוי 2: אין יותר bubbling של אירועים מאלמנטים פנימיים
wire:model על אלמנט עוטף כבר לא תופס אירועים שמבעבעים משדות קלט מקוננים. אם
הסתמכתם על זה, הוסיפו .deep.
{{-- Livewire 3: the wrapper caught the input's event --}}
<div wire:model="value">
<input type="text">
</div>
{{-- Livewire 4: opt back in explicitly --}}
<div wire:model.deep="value">
<input type="text">
</div>כל השאר לא השתנה
{{-- Deferred by default --}}
<input type="text" wire:model="name">
{{-- Request on every change --}}
<input type="text" wire:model.live="search">
{{-- Debounced live search --}}
<input type="text" wire:model.live.debounce.400ms="search">
{{-- Nested data --}}
<input wire:model="form.address.city">
<input wire:model="post.title">post.title) עדיין מחייב חוק ולידציה תואם.
זו ההגנה מפני mass assignment, והיא עברה ל-v4 ללא שינוי.
7. Actions, async ו-renderless
Actions מוצהרים בדיוק כמו קודם — מתודות ציבוריות שנקראות מהמרקאפ.
new class extends Component {
public function save()
{
$this->validate();
// …
}
};<button wire:click="save">Save</button>
<form wire:submit="save">…</form>
<button wire:click="delete({{ $post->id }})"
wire:confirm="Delete this post? This cannot be undone.">Delete</button>Actions ללא רינדור
כש-action לא משנה שום דבר שהמשתמש רואה — הגדלת מונה צפיות, כתיבת שורת audit — דילוג על הרינדור מחדש חוסך מעבר מלא על התבנית והשוואת DOM.
use Livewire\Attributes\Renderless;
new class extends Component {
#[Renderless]
public function incrementViewCount(): void
{
$this->post->increment('views');
}
};{{-- Or from the template side --}}
<button type="button" wire:click.renderless="incrementViewCount">Track</button>המקבילה האימפרטיבית בתוך מתודה היא עדיין $this->skipRender().
Actions אסינכרוניים
#[Async] (או המודיפייר .async) מריץ action במקביל,
מחוץ לתור הבקשות הרגיל. אידיאלי לעבודת fire-and-forget: פינגים לאנליטיקה, לוגים,
חימום cache.
use Livewire\Attributes\Async;
new class extends Component {
#[Async]
public function logInteraction(string $element): void
{
Analytics::record($element, auth()->id());
}
};<button wire:click.async="logInteraction('cta-hero')">Get started</button>Magic actions
<button wire:click="$refresh">Refresh</button>
<button wire:click="$set('tab', 'settings')">Settings</button>
<button wire:click="$toggle('showFilters')">Filters</button>
<button wire:click="$dispatch('open-modal', { name: 'create' })">New</button>
<button wire:click="$parent.closeModal()">Close</button>protected או private כדי שלא ניתן יהיה להפעיל אותן מהלקוח.
8. מחזור חיים ו-hooks
מחזור החיים לא השתנה מאז גרסה 3. הכרת הסדר עדיין מעלימה את רוב הבאגים המבלבלים.
1. Component instance created
2. boot()
3. mount($params)
4. booted()
5. render() 1. POST /livewire-{hash}/update arrives with the state snapshot
2. Snapshot checksum verified
3. Component instance created
4. boot()
5. State restored from the snapshot
6. hydrate() / hydrateFoo()
7. booted()
8. updating($prop, $value) / updatingFoo($value)
9. Properties receive new values
10. updated($prop, $value) / updatedFoo($value)
11. Queued actions run
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — state packed back into the snapshot
14. Response: markup + effectsnew class extends Component {
public float $price = 0;
public function boot(): void {}
public function booted(): void {}
public function hydrate(): void {}
public function dehydrate(): void {}
public function updating(string $property, mixed $value): void {}
public function updated(string $property, mixed $value): void
{
$this->validateOnly($property);
}
// Property-specific
public function updatedPrice(mixed $value): void
{
$this->price = round((float) $value, 2);
}
// Nested key form.email
public function updatedFormEmail(mixed $value): void
{
$this->form['email'] = strtolower(trim($value));
}
};9. ולידציה ו-Form objects
הוולידציה עוברת מ-v3 ללא שינוי, כולל attributes ו-Form objects.
use Livewire\Attributes\Validate;
new class extends Component {
#[Validate('required|string|min:2|max:80')]
public string $name = '';
#[Validate('required|email:rfc,dns')]
public string $email = '';
#[Validate('accepted', message: 'Please agree to the processing of your data.')]
public bool $consent = false;
public function submit(): void
{
$data = $this->validate();
Contact::create($data);
$this->reset();
}
};חוקים מותנים
protected function rules(): array
{
return [
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($this->userId),
],
'password' => $this->userId ? 'nullable|min:8|confirmed' : 'required|min:8|confirmed',
];
}Form objects
<?php
namespace App\Livewire\Forms;
use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post = null;
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
public function setPost(Post $post): void
{
$this->post = $post;
$this->title = $post->title;
$this->body = $post->body;
}
public function store(): Post
{
$this->validate();
return Post::create($this->except('post'));
}
public function update(): void
{
$this->validate();
$this->post->update($this->except('post'));
}
}<?php
use Livewire\Component;
use App\Livewire\Forms\PostForm;
new class extends Component {
public PostForm $form;
public function save()
{
$this->form->post ? $this->form->update() : $this->form->store();
$this->redirect(route('posts.index'), navigate: true);
}
};
?>
<div>
<form wire:submit="save">
<input wire:model="form.title">
@error('form.title') <span>{{ $message }}</span> @enderror
<textarea wire:model="form.body"></textarea>
@error('form.body') <span>{{ $message }}</span> @enderror
<button type="submit">Save</button>
</form>
</div>שגיאות ב-JavaScript
גרסה 4 חושפת את שגיאות הוולידציה ללקוח דרך ה-magic property $errors.
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>10. Islands
Islands הם היכולת הדגל של Livewire 4, וזו שמשנה את הדרך שבה חושבים על גודל קומפוננטה. island הוא אזור בתוך קומפוננטה שמתרנדר מחדש באופן עצמאי: כשהוא מתעדכן, רק הקטע הזה מחושב מחדש ומוטלא — לא הקומפוננטה כולה.
@island
<div>Revenue: {{ $this->revenue }}</div>
@endislandלמה זה חשוב
ב-v3 הפתרון המקובל ל"הדשבורד הזה איטי" היה לפצל אותו לשש קומפוננטות בת, כדי שעדכון ווידג'ט אחד לא יריץ מחדש את השאילתות של חמשת האחרים. Islands נותנים את הבידוד הזה בלי גבול של קומפוננטה: קומפוננטה אחת, מחלקה אחת, כמה אזורים שמתעדכנים באופן עצמאי.
<?php
use Livewire\Component;
use Livewire\Attributes\Computed;
new class extends Component {
#[Computed]
public function revenue()
{
return Order::whereMonth('created_at', now()->month)->sum('total');
}
#[Computed]
public function queue()
{
return Job::pending()->count();
}
#[Computed]
public function feed()
{
return Activity::latest()->take(20)->get();
}
};
?>
<div>
@island(name: 'revenue')
<div class="card">
<h3>Revenue this month</h3>
<p class="stat">{{ number_format($this->revenue, 2) }} €</p>
</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">
<h3>Jobs pending</h3>
<p class="stat">{{ $this->queue }}</p>
</div>
@endisland
@island(name: 'feed', lazy: true)
@placeholder
<div class="card skeleton animate-pulse h-64"></div>
@endplaceholder
<div class="card">
@foreach ($this->feed as $activity)
<div wire:key="activity-{{ $activity->id }}">{{ $activity->summary }}</div>
@endforeach
</div>
@endisland
</div>אפשרויות
| אפשרות | השפעה |
|---|---|
name | מזהה את ה-island כדי ש-actions ו-JavaScript יוכלו לפנות אליו. כמה islands עם אותו שם תמיד מתרנדרים כקבוצה. |
lazy | מתרנדר כשה-island נגלל לתוך שטח התצוגה (intersection observer). |
defer | מתרנדר מיד אחרי טעינת העמוד, בלי קשר לנראות. |
always | מאלץ את ה-island להתעדכן בכל פעם שהאב מתרנדר מחדש. |
skip | מדלג לגמרי על הרינדור הראשוני. |
פנייה ל-island מתוך action
@island(name: 'revenue')
Revenue: {{ $this->revenue }}
@endisland
<button wire:click="$refresh" wire:island="revenue">Refresh revenue</button>Append ו-prepend — גלילה אינסופית בלי צנרת
@island(name: 'feed')
@foreach ($this->posts as $post)
<article wire:key="post-{{ $post->id }}">{{ $post->title }}</article>
@endforeach
@endisland
<button wire:click="loadMore" wire:island.append="feed">Load more</button>אותו דבר, מונע מ-Alpine או מ-JavaScript רגיל:
<button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
Load more
</button>Polling מוגבל ל-island
@island(name: 'queue')
<div wire:poll.3s>
Jobs pending: {{ $this->queue }}
</div>
@endisland
ה-poll מרענן רק את ה-island. השוו זאת ל-v3, שבה wire:poll על דשבורד
הריץ מחדש כל שאילתה בעמוד כל כמה שניות.
11. Slots והעברת מאפיינים
רכיבי Livewire יכולים כעת לקבל תוכן slot בדיוק כמו רכיבי Blade — עם הבדל אחד
חשוב: תוכן ה-slot מוערך בהקשר של רכיב האב.
wire:click שנכתב בתוך slot קורא למתודה של רכיב האב, לא של רכיב הבן.
Slot ברירת מחדל
<livewire:modal>
<h2>Create a post</h2>
<form wire:submit="save">
<input wire:model="title">
<button type="submit">Save</button>
</form>
</livewire:modal><?php
use Livewire\Component;
new class extends Component {
public bool $isOpen = false;
public function toggle(): void
{
$this->isOpen = ! $this->isOpen;
}
};
?>
<div wire:show="isOpen" class="modal">
<button wire:click="toggle" class="modal__close">×</button>
<div class="modal__body">
{{ $slot }}
</div>
</div>
בדוגמה זו wire:submit="save" ו-wire:model="title" שייכים
לרכיב האב, בעוד wire:click="toggle" שייך ל-modal. זו בדיוק החלוקה
הרצויה: ה-modal אחראי על פתיחה וסגירה, ורכיב האב אחראי על הטופס.
Slots בעלי שם
<livewire:modal>
<wire:slot name="header">
<h2>Create a post</h2>
</wire:slot>
<form wire:submit="save">
<input wire:model="title">
</form>
<wire:slot name="footer">
<button wire:click="save">Save</button>
<button wire:click="$parent.close()">Cancel</button>
</wire:slot>
</livewire:modal><div class="modal">
@if ($header = $slot('header'))
<div class="modal__header">{{ $header }}</div>
@endif
<div class="modal__body">{{ $slot }}</div>
@if ($footer = $slot('footer'))
<div class="modal__footer">{{ $footer }}</div>
@endif
</div>העברת מאפיינים
בדיוק כמו רכיבי Blade, רכיב Livewire יכול לקבל ולמזג מאפייני HTML שרירותיים
דרך $attributes.
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $type = 'info';
};
?>
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $slot }}
</div><livewire:alert type="danger" class="mt-6" data-testid="overdue-alert">
This invoice is 14 days overdue.
</livewire:alert>12. רכיבים מקוננים ו-wire:key
קינון עובד כמו ב-v3: כל רכיב בן הוא יחידה עצמאית עם state משלו ומחזור עדכון משלו.
<div>
@foreach ($orders as $order)
<livewire:order-row :order="$order" :key="'order-'.$order->id" />
@endforeach
</div>Smart wire keys
גרסה 4 מפעילה את smart_wire_keys כברירת מחדל, כך ש-Livewire מסיק מפתחות במצבי לולאה
רבים שבעבר דרשו מפתח מפורש. זה מצמצם קוד שבלוני, אך לא הופך את המפתחות
למיותרים.
wire:key מפורש הקשור ל-id של הרשומה. Smart keys
הם נוחות למקרים הפשוטים, לא תחליף לכך שתגידו ל-framework מהי שורה
בפועל.
@foreach ($rows as $row)
<div wire:key="row-{{ $row->id }}">…</div>
@endforeachProps ריאקטיביים
use Livewire\Attributes\Reactive;
new class extends Component {
#[Reactive]
public int $quantity;
};קריאה לרכיב האב
<button wire:click="$parent.refreshList()">Refresh the list</button>קישור דו-כיווני על רכיב
use Livewire\Attributes\Modelable;
new class extends Component {
#[Modelable]
public int $value = 0;
};<livewire:rating-input wire:model.live="review.rating" />13. אירועים
ה-API של האירועים לא השתנה מאז גרסה 3.
// שליחה
$this->dispatch('post-created');
$this->dispatch('post-created', postId: $post->id, title: $post->title);
$this->dispatch('refresh')->to(OrderList::class);
$this->dispatch('recalculate')->self();use Livewire\Attributes\On;
new class extends Component {
#[On('post-created')]
public function onPostCreated(int $postId, string $title): void
{
// …
}
};<button wire:click="$dispatch('open-modal', { name: 'create-order' })">New order</button>אירועים לדפדפן
$this->dispatch('notify', type: 'success', message: 'Order saved');<div x-data="{ show: false, message: '' }"
x-on:notify.window="message = $event.detail.message; show = true; setTimeout(() => show = false, 3000)"
x-show="show"
x-transition
class="toast">
<span x-text="message"></span>
</div>14. Computed properties
#[Computed] עובד כמו ב-v3 ומשתלב היטב במיוחד עם islands: island שקורא
computed property מחשב אותו מחדש רק כשה-island הזה מתעדכן.
use Livewire\Attributes\Computed;
new class extends Component {
public array $items = [];
#[Computed]
public function products()
{
return Product::whereIn('id', array_keys($this->items))->get();
}
#[Computed]
public function subtotal(): float
{
return $this->products->sum(
fn ($product) => $product->price * $this->items[$product->id]
);
}
#[Computed]
public function total(): float
{
return round($this->subtotal * 1.2, 2);
}
};@island(name: 'totals')
<p>Subtotal: {{ number_format($this->subtotal, 2) }} €</p>
<p>Total incl. VAT: {{ number_format($this->total, 2) }} €</p>
@endislandCache מעבר לבקשה
#[Computed(persist: true, seconds: 300)]
public function statistics(): array
{
return [
'orders' => Order::whereMonth('created_at', now()->month)->count(),
'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
];
}
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
return app(StatsService::class)->build();
}ניקוי ה-cache
unset($this->products, $this->subtotal, $this->total);$this->products, לעולם לא $products.
15. State ב-URL וב-session
use Livewire\Attributes\Url;
use Livewire\Attributes\Session;
new class extends Component {
#[Url]
public string $search = '';
#[Url(as: 'cat')]
public ?int $categoryId = null;
#[Url(except: '')]
public string $sort = 'popular';
#[Url(keep: true)]
public int $perPage = 24;
#[Url(history: true)]
public string $tab = 'all';
#[Session]
public bool $sidebarCollapsed = false;
#[Session(key: 'admin.table.density')]
public string $density = 'comfortable';
};התוצאה: /catalog?search=laptop&cat=5&sort=price-asc&tab=sale
הודעות flash
public function save(): void
{
$this->validate();
Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);
session()->flash('status', 'Settings saved');
}@if (session('status'))
<div class="alert alert-success">{{ session('status') }}</div>
@endif#[Url] קורא ל-History API בכל שינוי. עבור שדות שמשתנים בכל
הקשה, שלבו אותו עם wire:model.live.debounce.
16. העלאת קבצים
העלאות עובדות כמו בגרסה 3, באמצעות ה-trait WithFileUploads.
<?php
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
new class extends Component {
use WithFileUploads;
#[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]
public $avatar;
public function save(): void
{
$this->validate();
$path = $this->avatar->store('avatars', 'public');
auth()->user()->update(['avatar_path' => $path]);
$this->reset('avatar');
$this->dispatch('notify', message: 'Avatar updated');
}
};
?>
<div>
<form wire:submit="save">
<input type="file" wire:model="avatar" accept="image/*">
<div wire:loading wire:target="avatar">Uploading…</div>
@if ($avatar)
<img src="{{ $avatar->temporaryUrl() }}" alt="Preview" class="preview">
@endif
@error('avatar') <p class="error">{{ $message }}</p> @enderror
<button type="submit" wire:loading.attr="disabled">Save</button>
</form>
</div>סרגל התקדמות מדויק
<div x-data="{ progress: 0, uploading: false }"
x-on:livewire-upload-start="uploading = true"
x-on:livewire-upload-finish="uploading = false; progress = 0"
x-on:livewire-upload-error="uploading = false"
x-on:livewire-upload-progress="progress = $event.detail.progress">
<input type="file" wire:model="avatar">
<div x-show="uploading" class="progress">
<div class="progress__bar" :style="`width: ${progress}%`"></div>
</div>
</div>'temporary_file_upload' => [
'disk' => 's3',
'rules' => ['file', 'max:12288'],
'directory' => 'livewire-tmp',
'middleware' => 'throttle:60,1',
'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
'max_upload_time' => 5,
],upload_max_filesize, post_max_size ב-PHP ו-
client_max_body_size ב-nginx חייבים כולם לעלות על כלל ה-max: שלכם, אחרת ההעלאות
נכשלות ללא שגיאה קריאה.
17. עימוד
use Livewire\WithPagination;
use Livewire\Attributes\Url;
new class extends Component {
use WithPagination;
#[Url]
public string $search = '';
public string $sortField = 'created_at';
public string $sortDirection = 'desc';
public function updatedSearch(): void
{
$this->resetPage();
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function with(): array
{
return [
'orders' => Order::query()
->with('customer')
->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
->orderBy($this->sortField, $this->sortDirection)
->paginate(25),
];
}
};Islands הופכים עימוד לגלילה אינסופית
<div>
<input type="search" wire:model.live.debounce.400ms="search">
@island(name: 'rows')
@foreach ($orders as $order)
<div wire:key="order-{{ $order->id }}">{{ $order->number }}</div>
@endforeach
@endisland
<button wire:click="nextPage" wire:island.append="rows">Load more</button>
</div>paginate() עולה COUNT(*) נוסף. אם אינכם מציגים
את מספר העמודים הכולל — ובטעינה בסגנון append בדרך כלל לא — simplePaginate()
זול משמעותית.
18. מצבי טעינה
משפחת wire:loading לא השתנתה, ו-v4 מוסיפה מאפייני data-loading לעיצוב.
<div wire:loading>Loading…</div>
<div wire:loading.remove>Content</div>
<button wire:click="save">Save</button>
<span wire:loading wire:target="save">Saving…</span>
<span wire:loading wire:target="save,delete,publish">Working…</span>
<div wire:loading wire:target.except="search">Updating…</div>
<button wire:click="save" wire:loading.attr="disabled">Save</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Save</button>
{{-- הצגת האינדיקטור רק אם הבקשה נמשכת יותר מהסף --}}
<div wire:loading.delay>Loading…</div>
<div wire:loading.delay.long>Loading…</div>עיצוב מ-CSS
[data-loading] .btn {
opacity: 0.5;
pointer-events: none;
}שינויים שלא נשמרו וקישוריות
<input wire:model="title">
<span wire:dirty wire:target="title">You have unsaved changes</span>
<div wire:offline class="banner banner--warn">
No connection to the server. Changes are not being saved.
</div>19. Optimistic UI בצד הלקוח
סוג שלם של אינטראקציות מעולם לא נזקק לשרת: פתיחה וסגירה של פאנל, הצגת מונה תווים, צביעת שדה באדום כשהוא ארוך מדי. גרסה 4 מעניקה להם directives מובנים שמעדכנים את ה-DOM מיידית, ללא בקשה כלל.
wire:show
{{-- מחליף נראות דרך CSS, מיידית, ללא סבב מול השרת --}}
<div wire:show="showModal" class="modal">…</div>
<button wire:click="$toggle('showModal')">Open</button>wire:text
{{-- תוכן הטקסט עוקב אחרי ה-property בצד הלקוח --}}
<span wire:text="title"></span>
<input wire:model="title">wire:bind
<textarea wire:model="message" maxlength="280"></textarea>
<span wire:text="message.length"></span> / 280
<span wire:bind:class="message.length > 240 && 'text-red-500'">
Getting long
</span>פעולות צד לקוח בלבד עם $js
<?php
new class extends Component {
public function save() { /* … */ }
};
?>
<div>
<button wire:click="save">Save</button>
{{-- רץ בצד הלקוח בלבד --}}
<button x-on:click="$wire.$js.showToast = true">Show toast</button>
</div>wire:show / wire:text למשוב שהמשתמש צריך להרגיש
מיידית; action רגיל לכל מה שחייב להישמר.
20. גרירה ושחרור
שינוי סדר פירושו היה בעבר להוסיף את SortableJS, לחבר את ה-callbacks שלו ל-action של Livewire ולשמור על סנכרון בין שני המודלים. בגרסה 4 זה directive.
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
{{ $task->title }}
</li>
@endforeach
</ul>new class extends Component {
public function reorder(array $order): void
{
foreach ($order as $position => $id) {
Task::where('id', $id)->update(['position' => $position]);
}
}
};ידיות גרירה
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
<span wire:sort:handle class="cursor-grab">⠿</span>
<span>{{ $task->title }}</span>
{{-- אלמנטים אינטראקטיביים לא אמורים להתחיל גרירה --}}
<button wire:sort:ignore wire:click="delete({{ $task->id }})">Delete</button>
</li>
@endforeach
</ul>גרירה בין רשימות
<div class="board">
@foreach ($columns as $column)
<ul wire:sort="moveCard" wire:sort:group="board" wire:key="column-{{ $column->id }}">
@foreach ($column->cards as $card)
<li wire:sort:item="{{ $card->id }}" wire:key="card-{{ $card->id }}">
{{ $card->title }}
</li>
@endforeach
</ul>
@endforeach
</div>21. CSS וסקריפטים מקומיים לרכיב
מאחר שרכיב הוא עכשיו קובץ אחד, גם הסגנונות והסקריפטים שלו שייכים לאותו קובץ. Livewire מגביל את ה-CSS לרכיב, כך שהוא לא יכול לדלוף לשאר הדף.
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $plan = 'starter';
};
?>
<div class="card">
<h3 class="title">{{ ucfirst($plan) }}</h3>
<p class="price">{{ $this->price }} € / month</p>
</div>
<style>
/* Scoped to this component — no global .title collision */
.card { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
.title { font-size: 20px; font-weight: 700; }
.price { color: #2d6a4f; }
</style>סגנונות גלובליים במכוון
<style global>
:root { --brand: #f53004; }
</style>סקריפטים של רכיב
@script
<script>
const chart = new Chart(document.getElementById('sales'), {
type: 'line',
data: @json($chartData),
});
$wire.on('data-updated', ({ points }) => {
chart.data.datasets[0].data = points;
chart.update();
});
</script>
@endscript{{-- Load a third-party library once for the whole page --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets23. אינטגרציה עם JavaScript
Alpine.js עדיין מגיע עם Livewire, ו-$wire נשאר הגשר ממצב הרכיב
אל JavaScript.
<div x-data="{ open: false }">
<button x-on:click="open = !open">Details</button>
<div x-show="open">
<p x-text="$wire.title"></p>
<button x-on:click="$wire.title = 'New title'">Rename</button>
<button x-on:click="$wire.save()">Save</button>
<button x-on:click="await $wire.calculate(); open = false">Calculate</button>
</div>
</div>{{-- Two-way binding into Alpine --}}
<div x-data="{ query: $wire.entangle('search') }">
<input x-model="query">
</div>wire:ref — מתן שמות לאלמנטים ולרכיבים
<livewire:modal wire:ref="modal" />
<button x-on:click="$refs.modal.open()">Open the modal</button>#[Json] — החזרת נתונים ישירות ל-JavaScript
use Livewire\Attributes\Json;
new class extends Component {
#[Json]
public function searchSuggestions(string $term): array
{
return Product::search($term)->take(5)->pluck('name')->all();
}
};<input x-on:input.debounce.300ms="suggestions = await $wire.searchSuggestions($event.target.value)">Interceptors
גרסה 4 מחליפה את ה-hooks של v3, commit ו-request, ב-
interceptMessage() וב-interceptRequest().
document.addEventListener('livewire:init', () => {
Livewire.interceptRequest(({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload(); // the CSRF token expired
}
});
});
Livewire.interceptMessage(({ component, succeed }) => {
succeed(() => {
console.debug('component updated', component.name);
});
});
});// Programmatic access
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());התעלמות מתת-עץ
<div wire:ignore>
<select id="select2-field">…</select>
</div>
<div wire:ignore.self>…</div>24. בדיקות
Pest היא הדרך המומלצת לבדוק רכיבי Livewire 4. רכיבים מבוססי view יכולים להחזיק את הבדיקה שלהם ליד הרכיב, במקום בעץ תיקיות נפרד.
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.phpבדיקת רכיב מבוסס view
פנו לרכיב לפי השם שלו בכתיב נקודות, ולא לפי מחלקה:
use Livewire\Livewire;
it('renders', function () {
Livewire::test('post.create')
->assertStatus(200);
});
it('validates required fields', function () {
Livewire::test('post.create')
->set('title', '')
->set('body', 'too short')
->call('save')
->assertHasErrors(['title' => 'required']);
});
it('stores a valid post', function () {
Livewire::actingAs($user)
->test('post.create')
->set('title', 'A perfectly reasonable title')
->set('body', str_repeat('Body copy that is long enough. ', 3))
->call('save')
->assertHasNoErrors()
->assertDispatched('notify');
expect(Post::where('title', 'A perfectly reasonable title')->exists())->toBeTrue();
});רכיבים מבוססי מחלקה
use App\Livewire\Counter;
it('increments', function () {
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1);
});ה-assertions העיקריים
| מתודה | מה נבדק |
|---|---|
assertSet() / assertNotSet() | ערכי מאפיינים |
assertSee() / assertDontSee() | הפלט המרונדר |
assertViewHas() | נתונים שהועברו ל-view |
assertHasErrors() / assertHasNoErrors() | ולידציה |
assertDispatched() | אירועים |
assertRedirect() | הפניות |
assertUnauthorized() / assertForbidden() | הרשאות |
assertStatus() | סטטוס HTTP |
// Uploads
use Illuminate\Http\UploadedFile;
Livewire::test('avatar-uploader')
->set('avatar', UploadedFile::fake()->image('avatar.jpg', 400, 400))
->call('save')
->assertHasNoErrors();
// A component inside a page
$this->get('/dashboard')
->assertSeeLivewire('pages::dashboard');25. אבטחה וביצועים
רשימת בדיקות אבטחה
$this->authorize()בכל action שמשנה נתונים.- כל המזהים מסומנים ב-
#[Locked]. - מתודות עזר מוגדרות כ-
protectedאוprivate, כדי שהלקוח לא יוכל לקרוא להן. - כללי הוולידציה לא כוללים שדות פנימיים (
user_id,role,price). - אין tokens, מפתחות או מידע אישי מיותר במאפיינים ציבוריים — ה-snapshot עובר לדפדפן כטקסט גלוי.
- העלאות מוגבלות לפי סוג MIME וגודל.
- טפסי התחברות ויצירת קשר מאחורי rate limiter.
{!! !!}רק עבור markup מהימן או מחוטא.- Optimistic UI לעולם לא נחשב למקור האמת.
public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post);
$post->delete();
}ביצועים
| תסמין | הפתרון ב-v4 |
|---|---|
| עדכון של ווידג'ט מריץ מחדש כל שאילתה בדף | עטפו את האזורים ב-@island |
| markup ענק בדף | הוציאו קולקציות של מודלים מהמאפיינים הציבוריים; שלפו אותן ב-with() או ב-#[Computed] |
| בקשה על כל הקשה | wire:model.live.debounce.400ms |
| עומס מ-polling | poll בתוך island, והוסיפו .visible |
| round trip לשרת רק כדי לפתוח/לסגור פאנל | wire:show במקום action |
| הגדלת מונה מרנדרת מחדש את הדף | #[Renderless] |
| שאילתות N+1 | eager loading עם with(); Model::preventLazyLoading() בסביבת הפיתוח |
| ציור ראשון איטי בגלל בלוק כבד | @island(lazy: true) עם @placeholder |
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}26. פתרון תקלות ודף עזר מקוצר
הרכיב לא מרונדר בכלל
התגית לא סגורה עצמית. ב-v4, <livewire:my-component> לא מפיק כלום —
בשקט. כתבו <livewire:my-component />.
בפרודקשן הכול מחזיר 404, אבל מקומית זה עובד
ה-endpoint של Livewire עבר מ-/livewire/ ל-/livewire-{hash}/. כלל
firewall, חריגת WAF, כלל CDN או location של nginx שעדיין מקובעים לנתיב הישן יחסמו את
בקשות העדכון.
שדה הפסיק להסתנכרן אחרי השדרוג
wire:model.blur ו-.change שולטים עכשיו גם בסנכרון בצד הלקוח, ולא רק
ברשת. הוסיפו .live: wire:model.live.blur="title".
עטיפה הפסיקה לקלוט את ה-input שלה
Event bubbling כבוי כברירת מחדל. השתמשו ב-wire:model.deep.
ה-modifiers של wire:transition לא עושים כלום
.opacity, .scale ו-.duration.200ms הוסרו — v4 משתמש
ב-View Transitions API המובנה בדפדפן. הגדירו את האנימציה ב-CSS במקום.
island אף פעם לא מתעדכן
- ל-island אין
name, אבל ה-action מכוון ל-island בשם. wire:island="…"מצביע על שם שלא קיים בפלט המרונדר.- האזור תלוי במצב של ההורה שלא באמת השתנה — הוסיפו
always: true.
תוכן ה-slot קורא למתודה הלא נכונה
זה בכוונה: ה-markup של ה-slot מוערך בהקשר של ההורה. כדי לקרוא לרכיב הילד, עברו
דרך אירוע או דרך wire:ref.
רכיבי Volt הפסיקו לעבוד
Volt שולב בליבה. החליפו את Livewire\Volt\Component ב-Livewire\Component,
את Volt::route() ב-Route::livewire(), את Volt::test() ב-
Livewire::test(), והסירו את ה-service provider ואת החבילה של Volt.
דף עזר לדירקטיבות
| דירקטיבה | ייעוד | חדש ב-v4 |
|---|---|---|
wire:model | קישור שדה למאפיין | הסמנטיקה השתנתה |
wire:model.deep | קליטת אירועים מאלמנטים צאצאים | כן |
wire:click / wire:submit | קריאה ל-action | — |
wire:click.async | הרצת ה-action במקביל | כן |
wire:click.renderless | דילוג על הרינדור מחדש | כן |
wire:island | כיוון action ל-island | כן |
wire:island.append | הוספה ל-island במקום החלפה | כן |
wire:show | הצגה/הסתרה בצד הלקוח | כן |
wire:text | קישור תוכן טקסט בצד הלקוח | כן |
wire:bind | קישור ריאקטיבי של attribute | כן |
wire:sort | מיון מחדש ב-drag-and-drop | כן |
wire:intersect | פעולה כשהאלמנט נכנס ל-viewport | כן |
wire:ref | מתן שם לאלמנט או לרכיב עבור JS | כן |
wire:navigate | ניווט בסגנון SPA | — |
wire:navigate:scroll | שמירת הגלילה בקונטיינר | שם חדש |
wire:loading / wire:dirty / wire:offline | מצב הבקשה | — |
wire:poll | רענון תקופתי | מוגבל ל-island |
wire:key | זהות אלמנט בלולאה | smart keys מופעלים כברירת מחדל |
wire:ignore | החרגת תת-עץ מ-morphing | — |
דירקטיבות Blade
| דירקטיבה | ייעוד |
|---|---|
@island … @endisland | אזור שמתרנדר באופן עצמאי |
@placeholder … @endplaceholder | placeholder ל-island עצל |
<wire:slot name="…"> | תוכן של slot בעל שם |
@script … @endscript | JavaScript מקומי לרכיב |
@assets … @endassets | נכסים ברמת הדף שנטענים פעם אחת |
@persist … @endpersist | שמירת אלמנט בין מעברי ניווט |
<style> / <style global> | CSS מקומי / גלובלי של הרכיב |
Attributes של PHP
| Attribute | ייעוד |
|---|---|
#[Validate] | כלל ולידציה |
#[Locked] | חסימת כתיבה מה-frontend |
#[Computed] | ערך נגזר שנשמר במטמון |
#[Url] / #[Session] | שימור מצב |
#[On] | מאזין לאירועים |
#[Prop] | הגדרת prop שמגיע מההורה |
#[Reactive] / #[Modelable] | קישור הורה–ילד |
#[Async] | הרצת ה-action במקביל |
#[Renderless] | דילוג על רינדור מחדש |
#[Json] | החזרת נתונים ישירות ל-JavaScript |
#[Lazy] / #[Layout] / #[Title] | טעינה ומעטפת הדף |
פקודות Artisan
php artisan make:livewire post.create # single-file
php artisan make:livewire post.create --mfc # multi-file
php artisan make:livewire post.create --test # with a test
php artisan make:livewire pages::dashboard # namespaced
php artisan livewire:publish --config
php artisan optimize:clearמשאבים רשמיים
- התיעוד של Livewire: livewire.laravel.com/docs
- תיעוד השדרוג: livewire.laravel.com/docs/upgrading
- התיעוד של Laravel: laravel.com/docs
- Alpine.js: alpinejs.dev
אם יש סתירה בין התיעוד הזה למקור הרשמי, המקור הרשמי צודק. מצאתם שגיאה? ספרו לנו.