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 %}
{% 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” %}
{% if view == “grid” %}
{% else %}
{% 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 %}
```
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 %}
```
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:
- URL load — Browser navigates to a URL
- Controller executes — Server processes the request, populates
current_request.params - Template renders — Liquid template runs, creating local assigns, reading/writing session
- HTML sent — Page HTML (including context data) sent to browser
- Page displayed — Browser renders HTML
- 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
assignorcapture -
Is it an optional parameter to a snippet? → Use
defaulttag 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
Related articles and references
- Session tag reference — detailed session tag syntax and examples
- Context tag reference — detailed context tag syntax and examples
- Default tag reference — detailed default tag syntax and examples
- Render tag reference — how to pass parameters to snippets
- Component tag reference — rendering dynamic components
- Liquid session variables — comprehensive session variable guide
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.