{"title":"Liquid state and persistence","slug":"liquid-state-and-persistence","url":"https://support.storeconnect.com/articles/liquid-state-and-persistence","url_markdown":"https://support.storeconnect.com/articles/liquid-state-and-persistence.md","subtitle":null,"summary":"Choose the right storage mechanism for your data: session variables for  visitor state across pages, context for component-local state, defaults for  optional parameters, and local assigns for template-only values. This guide  covers use cases, security, and examples for each.","type":"Developer_Documentation","video_url":"","keywords":"liquid, state, persistence, session variables, context, default tag, local variables, parameter passing, security, storeconnect","last_modified":"2026-08-27T23:41:32+0000","body_markdown":"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.\n\n## Storage mechanisms at a glance\n\n| Mechanism | Scope | Lifetime | Visibility | Use Case |\n|-----------|-------|----------|-----------|----------|\n| **Local assigns** | Single template | One request | Nowhere (calc only) | Temporary calculations, loop variables |\n| **Default parameters** | Single snippet | One request | Within snippet only | Optional parameters with fallbacks |\n| **URL parameters** | One request | Single click | In `current_request.params` | Filters, sorting, pagination links |\n| **Context variables** | Single component | Component lifecycle | Component only | Component state, interaction tracking |\n| **Session variables** | Visitor, entire store | Session duration | All pages, all requests | Preferences, history, lightweight state |\n\n## Local assigns — temporary template calculations\n\nUse `assign` or `capture` to create variables that exist only within a single template for calculations or temporary storage.\n\n\n```liquid\n\n{% assign product_count = collection.products | size %}\n{% assign total_price = 0 %}\n{% for item in cart.items %}\n  {% assign total_price = total_price | plus: item.price %}\n{% endfor %}\n\n\u003cp\u003eCart total: {{ total_price | money }}\u003c/p\u003e\n```\n\n\n**When to use:**\n- Calculations (totals, counts, derived values)\n- Loop variables and counters\n- Conditional logic dependencies\n- Values that only this template needs\n\n**When NOT to use:**\n- Cross-page state (won't be available on the next page)\n- Visitor preferences (lost on page load)\n- Anything that needs to persist\n\n## Default parameters in snippets\n\nUse 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.\n\n\n```liquid\n\n{# In snippets/product-card.liquid #}\n{% default title: \"Untitled\" %}\n{% default show_price: false %}\n{% default currency: \"USD\" %}\n\n\u003cdiv class=\"card\"\u003e\n  \u003ch3\u003e{{ title }}\u003c/h3\u003e\n  {% if show_price %}\u003cp\u003e{{ price | money }}\u003c/p\u003e{% endif %}\n\u003c/div\u003e\n```\n\n\n**When to use:**\n- Defining optional parameters for a snippet\n- Providing sensible defaults for reusable components\n- Making parameters optional instead of required\n- Self-documenting code (defaults show expected parameters)\n\n**When NOT to use:**\n- Overriding values passed by the caller (don't do this)\n- Creating global state (defaults are local to the snippet)\n- One-time calculations (use `assign` instead)\n\n## URL parameters — request-scoped filtering\n\nUse `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.\n\n\n```liquid\n\n{# Current URL: /products?category=shoes\u0026sort=price #}\n\n{% assign category = current_request.params.category | default: \"all\" %}\n{% assign sort = current_request.params.sort | default: \"popularity\" %}\n\n\u003ch1\u003e{{ category | capitalize }} (sorted by {{ sort }})\u003c/h1\u003e\n\n{% if category == \"shoes\" %}\n  {# Show shoe products #}\n{% endif %}\n```\n\n\n**When to use:**\n- Filtering products by category or tag\n- Sorting results (price, popularity, newest)\n- Pagination links and page numbers\n- One-time query strings that don't need to persist\n\n**When NOT to use:**\n- Storing sensitive data in URLs (visible in browser history)\n- Anything that should persist after navigation (use session instead)\n- Relying on parameters to be present (always use defaults)\n\n**Security note:** Never trust URL parameters without validation. Treat them as untrusted user input.\n\n## Context variables — component-local state\n\nUse 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.\n\n\n```liquid\n\n{# In a component template #}\n{% context is_expanded: false %}\n{% context selected_tab: \"overview\" %}\n\n\u003cbutton type=\"button\" data-toggle=\"panel\"\u003e\n  {% if context.is_expanded %}Show Less{% else %}Show More{% endif %}\n\u003c/button\u003e\n\n{% if context.is_expanded %}\n  \u003cdiv class=\"expanded-content\"\u003e\n    {# Component content here #}\n  \u003c/div\u003e\n{% endif %}\n```\n\n\n**When to use:**\n- Component interaction state (expanded/collapsed, selected tab)\n- Component-local UI state (hover state, focus tracking)\n- Temporary state within a single component\n- Avoiding variable name collisions between components\n\n**When NOT to use:**\n- State that needs to be visible outside the component (use session)\n- Persisting state across page requests (use session)\n- Parent-child communication (use render parameters)\n- Sensitive data (context values are sent to the browser)\n\n**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.\n\n## Session variables — visitor state across pages\n\nUse 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.\n\n\n```liquid\n\n{# On the product page #}\n{% session last_viewed_product: product.id %}\n{% session last_viewed_product_name: product.name %}\n```\n\n\n\n```liquid\n\n{# On the home page #}\n{% if session_variables[\"last_viewed_product\"] %}\n  \u003cp\u003eYou last viewed: {{ session_variables[\"last_viewed_product_name\"] }}\u003c/p\u003e\n{% endif %}\n```\n\n\n**When to use:**\n- Visitor preferences (items per page, view preference, sort order)\n- Recently viewed items or history\n- Temporary filters or search state\n- Lightweight visitor state that needs to cross pages\n\n**When NOT to use:**\n- Sensitive data (passwords, payment info, customer IDs)\n- Data that should persist beyond the session (use Salesforce)\n- Large amounts of data (sessions have size limits)\n- Anything regulated or privacy-sensitive\n\n**Security:** Session variables are NOT encrypted. They are readable in cookies and visible in network requests. **Only store non-sensitive, visitor-preference data.**\n\n### Session security rules\n\n**Never store:**\n- Passwords or authentication tokens\n- Credit card numbers or payment information\n- Social security numbers or government IDs\n- Personal health information\n- Personal identifiable information (PII)\n- API keys or secrets\n- Any data covered by privacy law (GDPR, CCPA, etc.)\n\n**OK to store:**\n- Color or theme preference\n- Grid vs. list view preference\n- Items per page preference\n- Sort order preference\n- Last viewed product ID\n- Selected filter values\n- Non-sensitive form state\n\n## Form state — validation and dynamic fields\n\nWhen 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:\n\n### Single-page forms with validation\n\nFor forms on a single page that validate and show errors, store state in local assigns or the form object:\n\n\n```liquid\n\n{% form custom_form_name, class: \"product-form\" %}\n  {% assign title = form.fields[\"title\"] %}\n  \u003cinput type=\"text\" name=\"{{ title.name }}\" value=\"{{ title.value }}\"\u003e\n  {% if title.errors != blank %}\n    {% for message in title.errors.messages %}\n      \u003cspan class=\"error\"\u003e{{ message }}\u003c/span\u003e\n    {% endfor %}\n  {% endif %}\n\n  \u003cbutton type=\"submit\"\u003eSave\u003c/button\u003e\n{% endform %}\n```\n\n\n### Multi-step forms with page submissions\n\nFor multi-step forms that submit to the server and reload the page, use session variables to preserve entered data across steps:\n\n\n```liquid\n\n{# Step 1: Collect email #}\n{% if current_request.params.step == \"1\" %}\n  {% session form_email: current_request.params.email %}\n{% endif %}\n\n{# Step 2: Collect address #}\n{% if current_request.params.step == \"2\" %}\n  \u003cp\u003eEmail: {{ session_variables[\"form_email\"] }}\u003c/p\u003e\n  \u003cinput type=\"email\" name=\"address\"\u003e\n{% endif %}\n```\n\n\n### Dynamic fields within a component\n\nFor dynamic fields that appear/disappear based on user interaction, use context variables:\n\n\n```liquid\n\n{# In a component #}\n{% context show_advanced_options: false %}\n\n\u003cbutton data-toggle=\"advanced\"\u003eShow More Options\u003c/button\u003e\n\n{% if context.show_advanced_options %}\n  \u003cinput name=\"advanced_field_1\"\u003e\n  \u003cinput name=\"advanced_field_2\"\u003e\n{% endif %}\n```\n\n\n## URL parameters vs. session vs. context\n\nChoose based on who needs to see the value and for how long:\n\n### Use URL parameters when:\n- Filtering or sorting products (`?category=shoes\u0026sort=price`)\n- Navigating to a specific page or section\n- The state can be bookmarked or shared in a link\n- The state is temporary (just this request)\n\n**Example:** `/products?color=blue\u0026size=large\u0026page=2`\n\n### Use session when:\n- The visitor has a preference they want to remember\n- The preference should carry across the entire store\n- The state is lightweight and non-sensitive\n- The state should persist while they shop\n\n**Example:** Visitor selected \"Show 20 per page\" and that preference applies to all collection pages they visit.\n\n### Use context when:\n- The state is local to a single component\n- The state does not affect other components\n- The state is temporary (component lifecycle only)\n- The state is used for interaction tracking\n\n**Example:** An accordion panel is open or closed; other panels don't know or care about this state.\n\n## Common patterns and examples\n\n### Pattern 1: Remember collection view preference\n\nVisitor chooses grid vs. list view and wants that preference to stick:\n\n\n```liquid\n\n{# When visitor clicks a view preference button #}\n{% if current_request.params.set_view %}\n  {% session collection_view: current_request.params.set_view %}\n{% endif %}\n\n{# Get the stored preference or default to grid #}\n{% assign view = session_variables[\"collection_view\"] | default: \"grid\" %}\n\n\u003cdiv class=\"view-options\"\u003e\n  \u003cbutton {% if view == \"grid\" %}active{% endif %}\u003eGrid\u003c/button\u003e\n  \u003cbutton {% if view == \"list\" %}active{% endif %}\u003eList\u003c/button\u003e\n\u003c/div\u003e\n\n{% if view == \"grid\" %}\n  \u003cdiv class=\"grid\"\u003e\n    {# Grid layout #}\n  \u003c/div\u003e\n{% else %}\n  \u003cdiv class=\"list\"\u003e\n    {# List layout #}\n  \u003c/div\u003e\n{% endif %}\n```\n\n\n### Pattern 2: Track filter selections on a collection\n\nVisitor applies filters and you want to remember them while they browse, but clear them on next session:\n\n\n```liquid\n\n{# When visitor applies filters #}\n{% if current_request.params.apply_filters %}\n  {% session active_filters: current_request.params.filters %}\n{% endif %}\n\n{# Retrieve filters or start with empty #}\n{% assign filters = session_variables[\"active_filters\"] | default: empty_array %}\n\n{# Display active filters #}\n{% for filter in filters %}\n  \u003cspan class=\"active-filter\"\u003e{{ filter }} \u003ca href=\"?clear=true\"\u003e×\u003c/a\u003e\u003c/span\u003e\n{% endfor %}\n```\n\n\n### Pattern 3: Multi-step checkout with session persistence\n\nGuide the customer through checkout steps, preserving entered data:\n\n\n```liquid\n\n{% assign current_step = current_request.params.step | default: \"1\" %}\n\n{% case current_step %}\n  {% when \"1\" %}\n    {# Step 1: Shipping address #}\n    {% if current_request.method == \"POST\" %}\n      {% session checkout_email: current_request.params.email %}\n      {% session checkout_address: current_request.params.address %}\n    {% endif %}\n    \u003cform method=\"post\"\u003e\n      \u003cinput type=\"email\" name=\"email\" value=\"{{ session_variables[\"checkout_email\"] | default: \"\" }}\"\u003e\n      \u003cinput type=\"text\" name=\"address\" value=\"{{ session_variables[\"checkout_address\"] | default: \"\" }}\"\u003e\n      \u003cbutton type=\"submit\"\u003eNext\u003c/button\u003e\n    \u003c/form\u003e\n\n  {% when \"2\" %}\n    {# Step 2: Payment — show previously entered email #}\n    \u003cp\u003eShipping to: {{ session_variables[\"checkout_email\"] }}\u003c/p\u003e\n    \u003cp\u003eAddress: {{ session_variables[\"checkout_address\"] }}\u003c/p\u003e\n    \u003cform method=\"post\"\u003e\n      \u003cinput type=\"text\" name=\"card_number\" placeholder=\"Card number\"\u003e\n      \u003cbutton type=\"submit\"\u003eComplete Order\u003c/button\u003e\n    \u003c/form\u003e\n{% endcase %}\n```\n\n\n### Pattern 4: Component with toggle state\n\nAn expandable FAQ component where each item can be open or closed:\n\n\n```liquid\n\n{# In a component #}\n{% context is_open: false %}\n\n\u003cdiv class=\"faq-item\"\u003e\n  \u003cbutton class=\"question\" data-toggle=\"answer\"\u003e\n    {{ title }}\n    {% if context.is_open %}▼{% else %}▶{% endif %}\n  \u003c/button\u003e\n\n  {% if context.is_open %}\n    \u003cdiv class=\"answer\"\u003e\n      {{ content }}\n    \u003c/div\u003e\n  {% endif %}\n\u003c/div\u003e\n```\n\n\n### Pattern 5: Dashboard with multiple widget states\n\nA dashboard where each widget can be expanded independently:\n\n\n```liquid\n\n{# In the dashboard template #}\n\n{% for widget in widgets %}\n  {# Each widget is a component with its own context #}\n  {% render \"widgets/card\", widget: widget %}\n{% endfor %}\n```\n\n\n\n```liquid\n\n{# In components/widgets/card.liquid #}\n{% context is_expanded: false %}\n\n\u003cdiv class=\"widget-card\"\u003e\n  \u003cdiv class=\"widget-header\"\u003e\n    \u003ch3\u003e{{ widget.title }}\u003c/h3\u003e\n    \u003cbutton class=\"expand-btn\" data-toggle=\"expand\"\u003e\n      {% if context.is_expanded %}−{% else %}+{% endif %}\n    \u003c/button\u003e\n  \u003c/div\u003e\n\n  {% if context.is_expanded %}\n    \u003cdiv class=\"widget-content\"\u003e\n      {{ widget.content }}\n    \u003c/div\u003e\n  {% endif %}\n\u003c/div\u003e\n```\n\n\n## Security considerations\n\n### Session variables: minimal, non-sensitive only\n\nSession variables are accessible in:\n- Browser cookies\n- Network requests\n- Browser developer tools\n- Any JavaScript on the page\n\n**Never store sensitive data** like passwords, payment info, or personal IDs. Store only display preferences and non-sensitive history.\n\n### Context variables: non-sensitive, minimal only\n\nContext variables are:\n- Sent to the browser as part of component state\n- Visible in network requests and browser inspector\n- Limited in size (don't store large objects)\n\nKeep context variables minimal and non-sensitive. Use for UI state and interaction tracking only.\n\n### URL parameters: untrusted input\n\nAlways validate and sanitize URL parameters:\n\n\n```liquid\n\n{# Unsafe #}\n\u003cp\u003e{{ current_request.params.user_message }}\u003c/p\u003e\n\n{# Safer: use filters and validate #}\n\u003cp\u003e{{ current_request.params.user_message | strip_html | escape }}\u003c/p\u003e\n```\n\n\n### Local assigns and defaults: safe (not exposed)\n\nLocal variables and defaults are never exposed outside the template. They are safe for any data type, including sensitive information.\n\n## Execution timeline\n\nUnderstanding when each mechanism is evaluated helps you choose the right one:\n\n1. **URL load** — Browser navigates to a URL\n2. **Controller executes** — Server processes the request, populates `current_request.params`\n3. **Template renders** — Liquid template runs, creating local assigns, reading/writing session\n4. **HTML sent** — Page HTML (including context data) sent to browser\n5. **Page displayed** — Browser renders HTML\n6. **User interaction** — If component, context may update; if redirect, go to step 1\n\n## Decision tree\n\n**I need to store data — where should I put it?**\n\n- Is it needed only once, for a calculation in this template?\n  → **Use `assign` or `capture`**\n\n- Is it an optional parameter to a snippet?\n  → **Use `default` tag at the top of the snippet**\n\n- Is it from the current URL (filters, sorting, pagination)?\n  → **Use `current_request.params`**\n\n- Is it state within a single component?\n  → **Use `context`**\n\n- Is it a visitor preference or history that should persist across pages?\n  → **Use `session` (non-sensitive only)**\n\n- Is it sensitive data that should never leave the server?\n  → **Store in Salesforce, never in client-side storage**\n\n## Related articles and references\n\n- **[Session tag reference](session-tag-reference)** — detailed session tag syntax and examples\n- **[Context tag reference](context-tag-reference)** — detailed context tag syntax and examples\n- **[Default tag reference](default-tag-reference)** — detailed default tag syntax and examples\n- **[Render tag reference](render-tag-reference)** — how to pass parameters to snippets\n- **[Component tag reference](component-tag-reference)** — rendering dynamic components\n- **[Liquid session variables](liquid-session-variables)** — comprehensive session variable guide"}