Search code examples
laravel-livewire

Why reactive array is not changed in blade file on user's action?


In laravel 10/livewire 3 app having function declared as I have a component for user's actions and I need when user clicked some action to change status of the reaction at once. I have a code:

class UserReaction extends Component
{
    use AppCommonTrait;
    public int $id;
    public string $dataType;

    #[Reactive]
    public array $reactionActions = [];

    public function mount(int $id, string $dataType)
    {
        ...
        $this->reactionActions = []; // collect all data in 1 array
        foreach ($reactionActions as $key => $label) {
            $voted = in_array($key, $userReactions);
            $reactionCount = 0;
            foreach ($totalUserReactions as $totalUserReaction) {
                if ($totalUserReaction['action'] === $key) {
                    $reactionCount = $totalUserReaction['reaction_count'];
                }
            }
            $this->reactionActions[$key] = [
                'key' => $key,
                'label' => Str::lower($label),
                'voted' => $voted, // if logged user has voted this action
                'reactionCount' => $reactionCount, // all users has voted this action
            ];
        }
    }

    public function render()
    { // all data passed as parameter
        return view('livewire.user-reaction', ['reactionActions' => $this->reactionActions]);
    }

    public function voteReaction($voted, $action)
    {
        $modelType = $this->getModelType($this->dataType);
        if ( ! $voted) { // to add reaction
            $reaction = Reaction::getByModelType($modelType)->getByModelId($this->id)->getByAction($action)->getByUserId(auth()->user()->id)->first();
            if (empty($reaction)) { // not to vote for the second time
                Reaction::create([
                    'model_id' => $this->id,
                    'model_type' => $modelType,
                    'user_id' => auth()->user()->id,
                    'action' => $action
                ]);
                foreach ($this->reactionActions as $key => $reactionAction) {
                    if($reactionAction['key'] === $action) {
                        $this->reactionActions[$key]['voted'] = true;
                        // TRY TO CHANGES VOTED STATUS OF AN ACTION - BUT I DO NOT SEE THE CHANGES IN BLADE FILE
                    }
                }
            }
        } else {  // to cancel reaction
            $reaction = Reaction::getByModelType($modelType)->getByModelId($this->id)->getByAction($action)->getByUserId(auth()->user()->id)->first();
            if ( ! empty($reaction)) {
                $reaction->delete();
            }
        }
    }

and in blade file :

<div class="flex gap-2">
    $reactionActions::{{ print_r($reactionActions, true) }}
//    I expected changed would be applied in an array above - but not
    <hr>
    <hr>
    <hr>
    @foreach($reactionActions as $index => $reactionAction)
        <button
            class="py-1.5 px-3 hover:text-green-600 hover:scale-105 hover:shadow text-center border rounded-md border-gray-400 h-8 text-sm flex items-center gap-1 lg:gap-2 news-user-reactions-item{{$reactionAction['voted'] ? '-voted' : ''}}">
            <img src="img/emoji/{{$reactionAction['label']}}.png" class="w-full" alt="{{ $reactionAction['label'] }}"
                 title="{{$reactionAction['voted'] ? 'You have already voted "' . $reactionAction['label'] . '" reaction. You can cancel your vote' : 'You can vote "' . $reactionAction['label'] . '" reaction'}}"  wire:click="voteReaction( '{{ $reactionAction['voted'] }}', '{{ $reactionAction['key'] }}' )" />
            <span>{{ $reactionAction['key'] }}:{{ $reactionAction['reactionCount'] }}</span>
        </button>
    @endforeach

</div>

I declared #[Reactive] for $reactionActions var - buit it did not help.

Which way is correct ?


Solution

  • #[Reactive] is used where a parent component wants to update a child component when a passed property is changed: this is not the case, so it is not needed.

    The problem is caused by the interaction between wire:click action:

     wire:click="voteReaction( '{{ $reactionAction['voted'] }}', '{{ $reactionAction['key'] }}' )
    

    and the the test performed in the voteReaction method:

    if($reactionAction['key'] === $action) {
    

    since $reactionAction['key'] contains a number and voteReaction() receives a string and you are applyng a strict type comparison with === , the test fails.

    Solution: apply a loose comparison using the == operator or pass the parameter without quotes.
    Note that also the first parameter, when the key 'voted' contains the false value, is printed as an empty string: true and false are printed as 1 and '' from the blade {{ }} operator, this can work but only with a loose comparison. A similar option can be casting the value to int:

    wire:click="voteReaction( {{ (int)$reactionAction['voted'] }}, {{ $reactionAction['key'] }})
    

    A more important observation: since in the mount() you are keying the array with the value of $key, the $action parameter corresponds to the array position, so the foreach:

    foreach ($this->reactionActions as $key => $reactionAction) {
       .....
    }
    

    can be substituted by a simple:

    $this->reactionActions[$action]['voted'] = true;