Skip to content
Log in

Liquid state and persistence

On this page

When building Liquid templates, you need to decide where to store data: in the template’s local scope, in a component’s isolated context, across pages in a visitor’s session, or passed via parameters. This guide explains when to use each approach and shows practical examples for common scenarios.

Storage mechanisms at a glance

Mechanism Scope Lifetime Visibility Use Case
Local assigns Single template One request Nowhere (calc only) Temporary calculations, loop variables
Default parameters Single snippet One request Within snippet only Optional parameters with fallbacks
URL parameters One request Single click In current_request.params Filters, sorting, pagination links
Context variables Single component Component lifecycle Component only Component state, interaction tracking
Session variables Visitor, entire store Session duration All pages, all requests Preferences, history, lightweight state

Local assigns — temporary template calculations

Use assign or capture to create variables that exist only within a single template for calculations or temporary storage.

```liquid

{% assign product_count = collection.products | size %} {% assign total_price = 0 %} {% for item in cart.items %} {% assign total_price = total_price | plus: item.price %} {% endfor %}

Cart total: {{ total_price | money }}

```

When to use: - Calculations (totals, counts, derived values) - Loop variables and counters - Conditional logic dependencies - Values that only this template needs

When NOT to use: - Cross-page state (won’t be available on the next page) - Visitor preferences (lost on page load) - Anything that needs to persist

Default parameters in snippets

Use the default tag at the top of a snippet to provide fallback values for parameters not passed by the caller. This creates self-documenting, reusable snippets.

```liquid

{# In snippets/product-card.liquid #} {% default title: “Untitled” %} {% default show_price: false %} {% default currency: “USD” %}

{{ title }}

{% if show_price %}

{{ price | money }}

{% endif %}

```

When to use: - Defining optional parameters for a snippet - Providing sensible defaults for reusable components - Making parameters optional instead of required - Self-documenting code (defaults show expected parameters)

When NOT to use: - Overriding values passed by the caller (don’t do this) - Creating global state (defaults are local to the snippet) - One-time calculations (use assign instead)

URL parameters — request-scoped filtering

Use current_request.params to read query string parameters from the current URL. These parameters appear only in the current request and do not persist to the next page.

```liquid

{# Current URL: /products?category=shoes&sort=price #}

{% assign category = current_request.params.category default: “all” %}
{% assign sort = current_request.params.sort default: “popularity” %}

{{ category | capitalize }} (sorted by {{ sort }})

{% if category == “shoes” %} {# Show shoe products #} {% endif %} ```

When to use: - Filtering products by category or tag - Sorting results (price, popularity, newest) - Pagination links and page numbers - One-time query strings that don’t need to persist

When NOT to use: - Storing sensitive data in URLs (visible in browser history) - Anything that should persist after navigation (use session instead) - Relying on parameters to be present (always use defaults)

Security note: Never trust URL parameters without validation. Treat them as untrusted user input.

Context variables — component-local state

Use the context tag within a component template to store state that is local to that component and does not appear on other components or in the parent template.

```liquid

{# In a component template #} {% context is_expanded: false %} {% context selected_tab: “overview” %}

{% if context.is_expanded %}

{# Component content here #}

{% endif %} ```

When to use: - Component interaction state (expanded/collapsed, selected tab) - Component-local UI state (hover state, focus tracking) - Temporary state within a single component - Avoiding variable name collisions between components

When NOT to use: - State that needs to be visible outside the component (use session) - Persisting state across page requests (use session) - Parent-child communication (use render parameters) - Sensitive data (context values are sent to the browser)

Lifetime: Context variables exist for the lifetime of the component. They reset if the component re-renders unless you use JavaScript to persist the state.

Session variables — visitor state across pages

Use the session tag to store data that persists for a visitor across multiple page requests. A session is tied to a browser and lasts until the session expires or the browser is closed.

```liquid

{# On the product page #} {% session last_viewed_product: product.id %} {% session last_viewed_product_name: product.name %} ```

```liquid

{# On the home page #} {% if session_variables[“last_viewed_product”] %}

You last viewed: {{ session_variables["last_viewed_product_name"] }}

{% endif %} ```

When to use: - Visitor preferences (items per page, view preference, sort order) - Recently viewed items or history - Temporary filters or search state - Lightweight visitor state that needs to cross pages

When NOT to use: - Sensitive data (passwords, payment info, customer IDs) - Data that should persist beyond the session (use Salesforce) - Large amounts of data (sessions have size limits) - Anything regulated or privacy-sensitive

Security: Session variables are NOT encrypted. They are readable in cookies and visible in network requests. Only store non-sensitive, visitor-preference data.

Session security rules

Never store: - Passwords or authentication tokens - Credit card numbers or payment information - Social security numbers or government IDs - Personal health information - Personal identifiable information (PII) - API keys or secrets - Any data covered by privacy law (GDPR, CCPA, etc.)

OK to store: - Color or theme preference - Grid vs. list view preference - Items per page preference - Sort order preference - Last viewed product ID - Selected filter values - Non-sensitive form state

Form state — validation and dynamic fields

When building forms, you often need to store validation errors, field state, or dynamic field data. Here’s how to choose where to store form state:

Single-page forms with validation

For forms on a single page that validate and show errors, store state in local assigns or the form object:

```liquid

{% form custom_form_name, class: “product-form” %} {% assign title = form.fields[“title”] %} {% if title.errors != blank %} {% for message in title.errors.messages %} {{ message }} {% endfor %} {% endif %}

{% endform %} ```

Multi-step forms with page submissions

For multi-step forms that submit to the server and reload the page, use session variables to preserve entered data across steps:

```liquid

{# Step 1: Collect email #} {% if current_request.params.step == “1” %} {% session form_email: current_request.params.email %} {% endif %}

{# Step 2: Collect address #} {% if current_request.params.step == “2” %}

Email: {{ session_variables["form_email"] }}

{% endif %} ```

Dynamic fields within a component

For dynamic fields that appear/disappear based on user interaction, use context variables:

```liquid

{# In a component #} {% context show_advanced_options: false %}

{% if context.show_advanced_options %} {% endif %} ```

URL parameters vs. session vs. context

Choose based on who needs to see the value and for how long:

Use URL parameters when:

  • Filtering or sorting products (?category=shoes&sort=price)
  • Navigating to a specific page or section
  • The state can be bookmarked or shared in a link
  • The state is temporary (just this request)

Example: /products?color=blue&size=large&page=2

Use session when:

  • The visitor has a preference they want to remember
  • The preference should carry across the entire store
  • The state is lightweight and non-sensitive
  • The state should persist while they shop

Example: Visitor selected “Show 20 per page” and that preference applies to all collection pages they visit.

Use context when:

  • The state is local to a single component
  • The state does not affect other components
  • The state is temporary (component lifecycle only)
  • The state is used for interaction tracking

Example: An accordion panel is open or closed; other panels don’t know or care about this state.

Common patterns and examples

Pattern 1: Remember collection view preference

Visitor chooses grid vs. list view and wants that preference to stick:

```liquid

{# When visitor clicks a view preference button #} {% if current_request.params.set_view %} {% session collection_view: current_request.params.set_view %} {% endif %}

{# Get the stored preference or default to grid #} {% assign view = session_variables[“collection_view”] | default: “grid” %}

<button {% if view == "grid" %}active{% endif %}>Grid</button> <button {% if view == "list" %}active{% endif %}>List</button>

{% if view == “grid” %}

{# Grid layout #}

{% else %}

{# List layout #}

{% endif %} ```

Pattern 2: Track filter selections on a collection

Visitor applies filters and you want to remember them while they browse, but clear them on next session:

```liquid

{# When visitor applies filters #} {% if current_request.params.apply_filters %} {% session active_filters: current_request.params.filters %} {% endif %}

{# Retrieve filters or start with empty #} {% assign filters = session_variables[“active_filters”] | default: empty_array %}

{# Display active filters #} {% for filter in filters %} {{ filter }} × {% endfor %} ```

Pattern 3: Multi-step checkout with session persistence

Guide the customer through checkout steps, preserving entered data:

```liquid

{% assign current_step = current_request.params.step default: “1” %}

{% case current_step %} {% when “1” %} {# Step 1: Shipping address #} {% if current_request.method == “POST” %} {% session checkout_email: current_request.params.email %} {% session checkout_address: current_request.params.address %} {% endif %} <form method="post"> <input type=”email” name=”email” value=”{{ session_variables[“checkout_email”] | default: “” }}”> <input type=”text” name=”address” value=”{{ session_variables[“checkout_address”] | default: “” }}”> </form>

{% when “2” %} {# Step 2: Payment — show previously entered email #} <p>Shipping to: {{ session_variables[“checkout_email”] }}</p> <p>Address: {{ session_variables[“checkout_address”] }}</p> <form method="post"> </form> {% endcase %} ```

Pattern 4: Component with toggle state

An expandable FAQ component where each item can be open or closed:

```liquid

{# In a component #} {% context is_open: false %}

{% if context.is_open %}
{{ content }}
{% endif %}

```

Pattern 5: Dashboard with multiple widget states

A dashboard where each widget can be expanded independently:

```liquid

{# In the dashboard template #}

{% for widget in widgets %} {# Each widget is a component with its own context #} {% render “widgets/card”, widget: widget %} {% endfor %} ```

```liquid

{# In components/widgets/card.liquid #} {% context is_expanded: false %}

{{ widget.title }}

{% if context.is_expanded %}
{{ widget.content }}
{% endif %}

```

Security considerations

Session variables: minimal, non-sensitive only

Session variables are accessible in: - Browser cookies - Network requests - Browser developer tools - Any JavaScript on the page

Never store sensitive data like passwords, payment info, or personal IDs. Store only display preferences and non-sensitive history.

Context variables: non-sensitive, minimal only

Context variables are: - Sent to the browser as part of component state - Visible in network requests and browser inspector - Limited in size (don’t store large objects)

Keep context variables minimal and non-sensitive. Use for UI state and interaction tracking only.

URL parameters: untrusted input

Always validate and sanitize URL parameters:

```liquid

{# Unsafe #}

{{ current_request.params.user_message }}

{# Safer: use filters and validate #}

{{ current_request.params.user_message | strip_html | escape }}

```

Local assigns and defaults: safe (not exposed)

Local variables and defaults are never exposed outside the template. They are safe for any data type, including sensitive information.

Execution timeline

Understanding when each mechanism is evaluated helps you choose the right one:

  1. URL load — Browser navigates to a URL
  2. Controller executes — Server processes the request, populates current_request.params
  3. Template renders — Liquid template runs, creating local assigns, reading/writing session
  4. HTML sent — Page HTML (including context data) sent to browser
  5. Page displayed — Browser renders HTML
  6. User interaction — If component, context may update; if redirect, go to step 1

Decision tree

I need to store data — where should I put it?

  • Is it needed only once, for a calculation in this template? → Use assign or capture

  • Is it an optional parameter to a snippet? → Use default tag at the top of the snippet

  • Is it from the current URL (filters, sorting, pagination)? → Use current_request.params

  • Is it state within a single component? → Use context

  • Is it a visitor preference or history that should persist across pages? → Use session (non-sensitive only)

  • Is it sensitive data that should never leave the server? → Store in Salesforce, never in client-side storage

Was this article helpful?

Was this article helpful?