in my parent component I fetch a bunch of tweets and I pass them to my feed component.
The parent component could be fetching all tweets or tweets that belong to a particular user or any tweet query actually.
In the feed component you can delete and edit tweets. at first when an edit was made the feed wouldn't update until refresh so I dispatched an event that the parent component could listen to and refresh the list of tweets.
That didn't work so I realized that props aren't reactive like you might think they are. I added the Reactive
attribute but got the following error Cannot mutate reactive prop livewire
.
This is obviously because I am trying to mutate the tweet list (by deleting/editing a tweet) that is reactive. what should I do in this case?
An instance where I get the error is when I try and delete a tweet (effectivly mutating the reactive prop) which happens in the deleteTweet
method
Code for the feed (child)
class Feed extends Component
{
#[Reactive]
public Collection $tweets;
public $editing = null;
public function editTweet($tweet)
{
$this->editing = $tweet;
}
public function deleteTweet(Tweet $tweet)
{
$this->authorize('delete', $tweet);
$tweet->delete();
$this->dispatch('tweet-deleted');
}
#[On('edit-cancelled')]
#[On('tweet-edited')]
public function cancelEdit()
{
$this->editing = null;
}
protected $listeners = ['tweet-created' => '$refresh'];
public function goto_userpage($user)
{
return redirect(url("/u/{$user}"));
}
public function render()
{
return view('livewire.tweets.feed');
}
}
Blade for feed (child):
<div class="mt-6 bg-white shadow-sm rounded-lg divide-y">
@foreach ($tweets->sortByDesc('created_at') as $tweet)
<div class="p-6 flex space-x-2" wire:key="{{ $tweet->id }}">
// SVG FOR ICON GOES HERE
<div class="flex-1">
<div class="flex justify-between items-center">
<div>
<span class="text-gray-800">{{ $tweet->user->name }}</span>
<a wire:click="goto_userpage('{{ $tweet->user->username }}')"
class="text-gray-600 cursor-pointer underline"><span>@</span>{{ $tweet->user->username }}</a>
<small
class="ml-2 text-sm text-gray-600">{{ $tweet->created_at->format('j M Y, g:i a') }}</small>
@unless ($tweet->created_at->eq($tweet->updated_at))
<small class="text-sm text-gray-600"> · {{ __('edited') }}</small>
@endunless
</div>
</div>
@if ($tweet->id == $editing)
<livewire:tweets.editing-form :tweetID="$editing" />
@else
<p class="mt-4 text-lg text-gray-900">{{ $tweet->message }}</p>
@endif
</div>
@if ($tweet->user->is(Auth::user()))
<x-dropdown>
<x-slot name="trigger">
<button>Open Menu</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link wire:click='deleteTweet({{ $tweet }})'
class="cursor-pointer">{{ __('Delete') }} </x-dropdown-link>
<x-dropdown-link wire:click='editTweet({{ $tweet->id }})'
class="cursor-pointer">{{ __('Edit') }} </x-dropdown-link>
</x-slot>
</x-dropdown>
@endif
</div>
@endforeach
</div>
Code for parent
class Tweets extends Component
{
public $tweets;
public function mount()
{
$this->fetch();
}
#[On('tweet-created')]
#[On('tweet-deleted')]
public function fetch()
{
$this->tweets = Tweet::with('user')->get();
}
public function test()
{
dd('test');
}
#[Layout('layouts.app')]
public function render()
{
return view('livewire.tweets');
}
}
Blade for parent:
<div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
<livewire:tweets.create />
<livewire:tweets.feed :$tweets />
</div>
The problem is that the #[Reactive] attribute can be set to allow a child component to be updated when the related property changes in the parent component.
Instead, when a change is detected in the child component, the error is generated.
To solve your problem I suggest a different approach: you can move the @foreach loop into the "Tweets" component and through some events (more or less as you already did) force the update of the higher level components from those of lower level.
The "Feed" component will only handle a single record.
Class Feed:
class Feed extends Component
{
public $tweet;
public $editing = null;
public function editTweet($tweet)
{
$this->editing = $tweet;
}
public function deleteTweet($tweet)
{
$this->authorize('delete', $tweet);
$this->tweet->delete();
$this->dispatch('tweet-deleted');
}
#[On('edit-cancelled')]
public function cancelEdit()
{
$this->editing = null;
}
#[On('tweet-edited')]
public function refeshData()
{
$this->tweet = Tweet::find($this->tweet->id);
$this->editing = null;
}
public function goto_userpage($user)
{
return redirect(url("/u/{$user}"));
}
public function render()
{
return view('livewire.tweets.feed');
}
}
Blade for feed:
<div class="p-6 flex space-x-2">
{{-- from here same as yours --}}
</div>
Class Tweets:
class Tweets extends Component
{
public $tweets;
public function mount()
{
$this->fetch();
}
#[On('tweet-created')]
#[On('tweet-deleted')]
public function fetch()
{
$this->tweets = Tweet::with('user')->latest()->get();
}
#[Layout('layouts.app')]
public function render()
{
return view('livewire.tweets');
}
}
Blade for tweets:
<div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
<livewire:tweets.create />
<div class="mt-6 bg-white shadow-sm rounded-lg divide-y">
@foreach ($tweets as $tweet)
<livewire:tweets.feed :$tweet wire:key="{{ 'tweet-' . $tweet->id }}" />
@endforeach
</div>
</div>
I've also moved the sorting of the data in the fetch() method of the Tweets class