In a Livewire view, why pass PHP data into an Alpine x-data expression with @js rather than echoing json_encode() output?
answer
- Laravel's Js::from under the hood
- hex-escaped quotes, tags and ampersands
- JSON.parse('...') for arrays and objects
- safe in attributes and script tags
- a render-time copy, not live state
basics
~20 s@js($slots) compiles to Laravel's Js::from(), which JSON-encodes with quotes, apostrophes, angle brackets and ampersands hex-escaped, so the value is a valid JavaScript expression inside an HTML attribute or script tag and cannot break out of it.
solid answer
~40 s`@js` is Laravel's Blade directive for printing PHP data as a JavaScript expression: it compiles to `Js::from($data)->toHtml()`. `Js` JSON-encodes with `JSON_HEX_TAG`, `JSON_HEX_APOS`, `JSON_HEX_AMP` and `JSON_HEX_QUOT`, prints strings in single quotes, and wraps arrays and objects in `JSON.parse('...')`. The output contains no raw `"`, `'`, `<` or `&`, so it is valid inside `x-data="..."` and inside a `<script>`, and a guest name like `</script>` or `" onmouseover="` cannot escape. Raw `{!! json_encode($slots) !!}` breaks the attribute on the first double quote and opens an XSS hole; `{{ json_encode() }}` happens to work in attributes but not in script tags. Either way, the value is fixed at render time; use `$wire.prop` for live state.
go deeper
Remember to print PHP data into Alpine with @js, and to quote string arguments such as UUIDs in $wire calls.
Explain what Js::from escapes, why arrays become JSON.parse('...'), and why raw or double-escaped json_encode output fails in one context or the other.
Separate render-time data passed with @js from live state read through $wire, and review views for over-shared data and unsafe raw echoes.
Set a team rule that PHP-to-JavaScript data crosses only through @js or $wire, and make it checkable in code review or static analysis.
## The problem: two languages in one attribute Alpine reads JavaScript from HTML attributes, for example `x-data="{ slots: [...] }"`. In a **Livewire** view the data often starts in PHP, such as the time slots available for a booking. Printing it means crossing three syntaxes at once: PHP values, JavaScript literals, and an HTML attribute delimited by double quotes. Getting any layer wrong either breaks the page or lets user-controlled text inject markup or script. ## What `@js` does `@js(expression)` is a Blade directive from Laravel itself, not from Livewire. The compiler turns it into: ```php <?php echo \Illuminate\Support\Js::from($slots)->toHtml() ?> ``` `Illuminate\Support\Js` then: 1. Converts enums to their values, `Arrayable` objects with `toArray()`, and `Jsonable` ones with `toJson()`. 2. JSON-encodes with the flags **`JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT`**, so `<`, `>`, `'`, `&` and `"` become `\u003C`-style escapes. 3. Prints a **string** as a single-quoted JavaScript string. 4. Prints an **array or object** as `JSON.parse('...')` around the escaped JSON (empty `[]` and `{}` are printed as-is); numbers, booleans and null print directly. The result contains no character that can close the HTML attribute or a `<script>` element, and it is a valid JavaScript expression in both places. ## Comparing the options | Blade output | In `x-data="..."` | In `<script>` | Risk | |---|---|---|---| | `@js($slots)` | Valid | Valid | None from the data | | `{!! json_encode($slots) !!}` | Breaks at the first `"` | Valid unless data contains `</script>` | XSS through attribute or tag break-out | | `{{ json_encode($slots) }}` | Works: the browser decodes `"` in attributes | Broken: `"` is not decoded in script | Confusing, context-dependent | | `{{ $uuid }}` without quotes | JS syntax error | JS syntax error | Classic UUID gotcha | The last row is the mistake the Livewire docs warn about: `$wire.cancel({{ $booking->uuid }})` renders an unquoted UUID. `$wire.cancel(@js($booking->uuid))` prints it as a properly quoted string. ## A render-time snapshot, not live state `@js` runs when Blade renders. Alpine evaluates `x-data` once when it initialises the element and does not re-read the attribute on later morphs. So: - Use `@js` for **initial or static data**: configuration, option lists, translated labels. - Use **`$wire.prop`** in Alpine for values that PHP changes during the component's life, such as the currently selected slot. - Do not duplicate: printing `@js($selectedSlot)` into `x-data` and also keeping `$selectedSlot` as a Livewire property creates the same two-copies problem entangling has. ## A booking example ```html <div x-data="{ slots: @js($availableSlots), open: false }"> <template x-for="slot in slots"> <button x-on:click="$wire.slot = slot.id; open = false" x-text="slot.label"></button> </template> </div> ``` The slot list comes in once through `@js`; the chosen slot goes into Livewire state through `$wire`. ## Inside a script tag and in `$wire` arguments The same escaping makes `@js` safe where plain echoes fail: - In a component script, `const slots = @js($availableSlots)` cannot be ended early by a slot label containing `</script>`, because `JSON_HEX_TAG` prints `<` as `<`. - In an action argument, `$wire.hold(@js($slot->code))` prints a quoted string whatever characters the code holds, where `{{ }}` would print it bare and `{!! !!}` would print it raw. - In an attribute, the hex escapes mean there is never a raw `"` to end `x-data="..."`. Because the output is a JavaScript **expression**, `@js` also composes: `x-data="{ slots: @js($slots), max: @js($maxGuests) }"` builds one object literal from several PHP values. ## What `@js` does not do - It does not filter data. Whatever you pass, the browser can read, so pass the three fields the UI needs, not whole models. - It does not make values reactive, as above. - It is unrelated to Livewire's `#[Js]` attribute, which marks a PHP method whose returned JavaScript runs on the client as a JavaScript action.
- A Livewire view prints x-data="{ selected: @js($slotId) }" and an action later changes $slotId. Why doesn't Alpine update?`@js` printed the value when Blade rendered, and Alpine evaluates `x-data` only when it initialises the element; later morphs do not re-run it. For values PHP changes, read `$wire.slotId` in Alpine expressions instead, which is reactive, and keep `@js` for initial or static data.
- What does @js(['a' => 1]) print, and why that form?It prints `JSON.parse('{\u0022a\u0022:1}')`: the JSON is hex-escaped so it holds no raw quotes, apostrophes, `<` or `&`, then wrapped in single quotes and `JSON.parse`. That keeps the output legal inside a double-quoted HTML attribute and inside a script tag.
saying these in an interview costs you the question
- @js is a Livewire directive that makes the value reactive.
- {!! json_encode() !!} is safe inside an Alpine attribute.
- {{ }} escaping makes json_encode output valid inside a script tag.
- Anything passed through @js is hidden from the browser.
- An unquoted {{ $uuid }} in a $wire call works like an integer id.