DEV Community

Cover image for 💾 Persisting and Sharing Your Application’s State (Local, URL, and Beyond)
Prabhunandan M
Prabhunandan M

Posted on

💾 Persisting and Sharing Your Application’s State (Local, URL, and Beyond)

Ever noticed how some web apps "remember" exactly where you left off—even after closing the tab? Or how sharing a link takes you straight to a filtered view or specific product? That’s state persistence in action!

TL;DR

  • 🧠 Ephemeral state? → Use memory
  • 🔗 Shareable views? → Use URL
  • 🔒 Sensitive, persistent state? → Backend
  • 💾 Preferences, themes? → localStorage

In React applications, it's common to manage dynamic UI states like filters, pagination, scroll positions or layout preferences. But if this state gets lost on refresh or revisit, the user experience suffers.
So, how do we make our apps "remember"? Below are four key strategies.


1. 💾 The Browser’s Built-in Memory: localStorage & IndexedDB

Use browser storage for client-side persistence:

  • Local Storage

    • A simple key-value store.
    • Ideal for small, non-sensitive data (e.g., theme preference).
    • Easy to use and survives across sessions.
  • IndexedDB

    • A low-level, more powerful solution for structured or large data.
    • Useful for offline caching or storing user-generated content.

✅ When to use:

  • Caching local data
  • Remembering user preferences
  • Anything that doesn’t need sharing or linking

2. 🧠 Ephemeral State: In-Memory References

Some states are only needed for a single session and can vanish on refresh.

  • Managed using:
    • React's local component state
    • Context API
    • Global state libraries (Redux, Zustand, etc.)
    • Can create custom hook to manage those states

These are fast and responsive, but volatile.

✅ When to use:

  • Temporary UI states
  • Form values before submission
  • Toggles and modals

3. ☁️ Backend-Persisted State

For critical or user-specific configurations that need to persist securely across sessions or devices:
Store the full state in your backend and then attach a unique ID to the URL as a query parameter (e.g., https://yourwebapp.com/report?id=a1b2c3d4). This gives you a clean, short, and shareable URL while keeping the state secure and manageable on the server.

  • Serialize complex state (e.g., JSON)
  • Store it on the backend (e.g., database)
  • Fetch and rehydrate on login or load

✅ When to use:

  • User dashboards
  • Reports or configurations
  • Multi-device access and secure persistence

4. 🌐 URL-Based State: Query Parameters for Deep Linking

One of the most powerful and user-friendly options: store relevant state directly in the URL.

Example: https://yourapp.com/products?category=electronics&sort=price

Benefits:

🔗 Deep Linking: Bookmark or share exact UI views

📤 Shareability: Share search results, filters, etc.

⏪ Browser History: Supports back/forward navigation

⚡ SSR Compatibility: Enables server-side rendered apps to hydrate with correct state

✅ When to Use URL Query Parameters

  • Filters, sort, and pagination
  • Search queries
  • Any UI state that should be:
    • Shareable
    • Bookmarkable
    • Navigable via browser history

🛠️ Optional: Encode State Before Adding to URL

If you don’t want to store raw values or complex objects directly in the URL, you can encode them to keep URLs cleaner and more secure.

🔄 Encoding Strategies for Storing State in the URL

This section outlines various techniques to encode application state into URLs — useful for sharing views, restoring filters, or deep linking.

🧩 JSON + encodeURIComponent()

This is the standard and most readable approach for encoding complex state objects directly into the URL. It's built into JavaScript and doesn't require any external libraries.

Best for:

  • Quick, readable encoding
  • Safely handling nested objects and arrays
  • Small to medium-sized state

Example:

const state = { filters: ['electronics'], sort: 'price' };

const encoded = encodeURIComponent(JSON.stringify(state));
// Result: "%7B%22filters%22%3A%5B%22electronics%22%5D%2C%22sort%22%3A%22price%22%7D"
Enter fullscreen mode Exit fullscreen mode

📦 LZ-String Compression (Advanced)

For very large or deeply nested JSON objects, the resulting URL can become excessively long.

lz-string is a popular library that compresses this data into a compact, Base64-safe string, significantly reducing the URL length.

✅ Best For

  • Reducing URL length for large state objects
  • Complex dashboards or report configurations
  • Maintaining a clean URL for massive payloads

💡 Example

import {
  compressToEncodedURIComponent,
  decompressFromEncodedURIComponent
} from 'lz-string';

const state = {
  filters: ['electronics', 'home-appliances'],
  sort: 'price',
  page: 2,
  extraData: {
    nested: true,
    values: Array(50).fill({ key: 'value' })
  }
};
// Compress the state
const compressed = compressToEncodedURIComponent(JSON.stringify(state));
// Result: a short, URL-safe string

// Decompress and parse it back
const decompressed = JSON.parse(decompressFromEncodedURIComponent(compressed));
Enter fullscreen mode Exit fullscreen mode

🧱 Base64 Encoding

Base64 encoding is another method to represent binary data as a string.

While it provides a compact representation, it's important to note that Base64 is not natively URL-safe.

You'll need to use encodeURIComponent() in conjunction with it.

✅ Best For

  • Simple obfuscation
  • Compact payloads
  • When you need to encode binary data

💡 Example

const state = { userSettings: { theme: 'dark' } };

// Encode a JSON object to a URL-safe Base64 string
const base64 = encodeURIComponent(btoa(JSON.stringify(state)));
// Result: "%7B%22userSettings%22%3A%7B%22theme%22%3A%22dark%22%7D%7D"

// Decode it back to a JSON object
const decoded = JSON.parse(atob(decodeURIComponent(base64)));
Enter fullscreen mode Exit fullscreen mode

🔐 JWT (JSON Web Token) — Tokenization Strategy

JWTs are not just for authentication.

They are compact, signed tokens that can carry verifiable state, making them ideal for a "tokenization" strategy.

Instead of storing raw state in the URL, you sign it and pass the token.

✅ Best For

  • Stateless server communication
  • Tamper-proof payloads (signature ensures data integrity)
  • Signed or time-bound configurations

⚠️ Important Considerations

  • 🔒 Security: Never include secrets unless the JWT is encrypted (JWE)
  • 📏 Size: Keep the payload small to avoid bloated URLs
  • 🚫 Visibility: JWTs are visible in browser history and logs, so avoid sensitive data

💡 Example (using jsonwebtoken)

import jwt from 'jsonwebtoken';

const state = { dashboardId: 'D-123', filters: { status: 'open' } };
const secret = 'your-super-secret-key';

// Create a signed token from your state object
const token = jwt.sign(state, secret, { expiresIn: '1h' });
// Result: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXNoYm9hcmRJZCI6IkQtMTIzI...

// Construct the URL
const url = `https://yourapp.com/view?state=${token}`;
Enter fullscreen mode Exit fullscreen mode

Note : While sharable URLs have clear benefits, a major tradeoff is the risk of excessively long URL strings.


🧠 Decision-Making Flow: Choosing the Right Strategy

Use this hierarchy to decide the best approach for your application state:

Use Case Recommended Method
Public/shareable views (e.g., filtered search results) 🌐 URL Query Parameters
Cross-device persistence of sensitive configs ☁️ Backend Storage
Simple, private user preferences 💾 Local Storage
Temporary state for current session 🧠 In-Memory State

💡 Tip: A hybrid approach also works well. store simple state (that can be encoded/decoded easily) in the URL, and keep complex or sensitive state (like user-specific preferences or large objects) in your backend


⚙️ Advanced Tips for URL-Based State

If you use query parameters to persist state in the URL, keep the following in mind:

🔄 Encoding/Decoding

For structured or complex state:

// Encode
const encoded = encodeURIComponent(JSON.stringify(state));

// Decode
const decoded = JSON.parse(decodeURIComponent(encoded));

🔁 Listen to popstate

React to browser navigation by listening to the popstate event to rehydrate the UI state when URLs change (e.g., via the browser's Back/Forward buttons):

window.addEventListener("popstate", () => {
  // Re-sync your application's state with the updated URL
});
Enter fullscreen mode Exit fullscreen mode

🔒 Security Note: Never Store Sensitive Data in URLs

URL parameters are exposed in:

  • Browser history
  • Server logs
  • Network traffic

❌ Never store:

  • Passwords
  • API keys
  • Access tokens
  • Sensitive user data

✅ Always:

  • Treat all URL data as untrusted input
  • Sanitize and validate before using
  • Protect against XSS and injection attacks

📌 Summary

There is no one-size-fits-all approach. Instead, combine strategies based on:

  • 🎯 User experience goals
  • 🔐 Data sensitivity
  • 🔗 Shareability requirements
  • 📶 Offline capabilities

A thoughtfully layered state management strategy will improve your app’s:

  • 🧠 Usability
  • 🛠️ Maintainability
  • 🚀 Performance

That’s a wrap!

Managing app state isn’t just about storing values—it’s about choosing the right tool for the job. From URLs to JWTs to localStorage, combining strategies makes your app robust and shareable.

Got questions or tips? Drop them in the comments.

Top comments (0)