How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Seed JavaScript state from a template without a second request

Data the template already rendered belongs in the page, not in a second request. Encode it into a JSON script tag, parse it once at start-up, and keep per-visitor data out of cached pages.

Context

Context. A script that needs data the template has already rendered should get it from the page, not from a second request. A request made on load waits for the HTML, then the script, then its own round trip, and the page shows the wrong numbers until it returns. Put the data in the page as JSON inside a <script type="application/json"> tag: it is data, not code, so it needs no CSP nonce, and the script reads it synchronously. The one rule is about caching: only seed what is the same for every visitor, because a cached page is served to everyone.

Steps

  1. Build the data in the template as a value: an array in Smarty, a hash in Twig, with only the fields the script uses.
  2. Write it with json_encode into a <script type="application/json"> tag with an id.
  3. In the script, read the tag's text and parse it once, at start-up.
  4. If the data is per visitor (a cart, a name, a CSRF token) and the page is cached, seed it into a fragment instead: dynamic fragments.

Code

A product listing handing its products to a client-side filter:

Template code
{$seed = []}
{foreach $products as $product}
  {$seed[] = ['id' => $product->getId(), 'name' => $product->getName(), 'path' => $product->getFullPath()]}
{/foreach}
<script type="application/json" id="listing-data">{$seed|json_encode}</script>
{% set seed = [] %}
{% for product in products %}
  {% set seed = seed|merge([{'id': product.getId(), 'name': product.getName(), 'path': product.getFullPath()}]) %}
{% endfor %}
<script type="application/json" id="listing-data">{{ seed|json_encode }}</script>
JavaScript
// listing.js
const products = JSON.parse(document.getElementById('listing-data').textContent);
renderFilters(products);   // no request on load

Why this and not that

Not an inline <script> assigning a global: it needs the page's nonce, and any quote or </script> in the data has to be escaped by hand; json_encode inside a JSON script tag handles both. Not data- attributes for anything larger than a value or two: they turn a list into string parsing. Not a fetch on load "to keep the template simple": the template already paid for the query, and the visitor pays again for the round trip.

Go deeper

Was this page helpful?

Your answer goes to the docs team.