DEV Community

Cover image for Zustand vs. Vaultrice? A Guide to Local and Shared State Management
Adriano Raiano
Adriano Raiano

Posted on • Originally published at vaultrice.com

Zustand vs. Vaultrice? A Guide to Local and Shared State Management

If you're a React developer, you've likely heard of (and probably love) Zustand. It's a minimalist, fast, and unopinionated state manager that has taken the ecosystem by storm. It's a fantastic tool for taming the complexity of client-side state.

But as our applications become more collaborative and cross-device, a question emerges: when is a client-side state manager not enough? This brings us to a new class of tools, like Vaultrice.

So, is it a "Zustand vs. Vaultrice" showdown? Not at all. The truth is, they aren't direct competitors. They're teammates, each designed to solve a different, specific problem. This guide will show you which one to reach for and how they can work together to build powerful, modern applications.


Part 1: The World Inside a Browser Tab - Understanding Zustand

Zustand's primary job is to manage state within a single, isolated application instance.

Think of Zustand as a whiteboard in a single office room. Everyone in that room can see the board, and it's a great way to keep track of what's going on—but only for that room. When you leave, the information is gone (unless you manually save it to localStorage). People in other rooms (other browser tabs or devices) have their own separate whiteboards.

It's the perfect tool for ephemeral UI state that doesn't need to be shared, like the open/closed state of a modal, the text in a search bar, or loading flags.

Example: A Zustand store for UI state

import { create } from 'zustand'

const useUIStore = create((set) => ({
  isSidebarOpen: false,
  toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
}))
Enter fullscreen mode Exit fullscreen mode

Part 2: The World Between Browsers - Understanding Vaultrice

Vaultrice's primary job is to manage state that needs to be shared and synchronized between different application instances (tabs, devices, users, and even domains).

Think of Vaultrice as a shared Google Doc. Everyone, no matter what room (tab), building (browser), or city (device) they are in, is looking at the exact same document. When one person types, everyone else sees the update in real-time.

It’s the perfect tool for state that is persistent, shared, and real-time. It achieves this through a powerful core SDK, @vaultrice/sdk.

A Look Under the Hood: The @vaultrice/sdk

Before we dive into the React hooks, it's important to understand that they are a convenience layer on top of a robust, framework-agnostic JavaScript SDK. The @vaultrice/sdk provides the foundational power and can be used in any JavaScript project (Node.js, React, Vue, Svelte, Deno, Bun, etc.).

It offers two primary API paradigms:

  • NonLocalStorage: A familiar, method-based API that feels just like localStorage (setItem, getItem) but is cloud-powered, asynchronous, and real-time.
  • SyncObject: A modern, reactive API that uses a JavaScript Proxy to make your shared state behave exactly like a local object (doc.title = 'New Title').

Beyond simple key-value storage, the core SDK also provides a superset of features that client-side managers don't offer, such as:

  • Presence API: To build "who's online" lists.
  • Offline-First Support: With an outbox pattern for automatic sync on reconnection.
  • End-to-End Encryption: For maximum data confidentiality.

The Game-Changer: A Complete Concurrency Strategy

This is where the distinction becomes profound. Imagine two users trying to modify a shared list at the same time. With a traditional read-modify-write pattern, one update could overwrite and erase the other. This is a classic race condition, and Zustand alone can't fix it for shared data.

Vaultrice solves this with a two-pronged strategy: Atomic Operations and Optimistic Concurrency Control (OCC).

1. Atomic Operations for Common Mutations

For common, specific updates, the @vaultrice/sdk provides server-side atomic operations that are guaranteed to be safe.

  • incrementItem() / decrementItem(): For safe counters.
  • push(): To atomically append an element to an array.
  • splice(): To atomically remove, replace, or insert elements in an array (works like Array.prototype.splice).
  • merge(): To shallow merge fields into an object.
  • setIn(): To safely set a value deep within a nested object.

2. Optimistic Concurrency Control (OCC) for Complex Updates

What if your update is more complex than a simple push or merge? For these cases, Vaultrice provides OCC. When you setItem, you can provide the updatedAt timestamp of the data you originally read. The server will only apply your update if the data hasn't changed since you read it.

// 1. Fetch the item and its timestamp
const item = await nls.getItem('project-details');

// 2. User makes complex changes in a form...
const newDetails = { ...item.value, name: 'New Project Name', status: 'active' };

// 3. Submit the update with the original timestamp
try {
  await nls.setItem('project-details', newDetails, { updatedAt: item.updatedAt });
} catch (error) {
  // This will fail if another user has changed the details in the meantime.
  // Now you can handle the conflict gracefully (e.g., refetch and merge).
  console.error('Conflict detected!');
}
Enter fullscreen mode Exit fullscreen mode

This comprehensive approach gives you the tools to handle any concurrent scenario, from simple counters to complex object mutations.

The "Better Together" Pattern: Local UI + Shared Global State

The most powerful approach is to use both tools for what they do best. This isn't a "versus" battle; it's a partnership.

Use Zustand for: Local, ephemeral UI state.
Use Vaultrice for: Shared, persistent global state.

Let's build a Collaborative Todo List that perfectly illustrates this pattern and uses the atomic push action with the help of @vaultrice/react.

import { create } from 'zustand'
import { useNonLocalArray } from '@vaultrice/react'

// 1. Zustand Store for LOCAL UI state
// This manages the input field's text, which is local to each user.
const useTodoStore = create((set) => ({
  newTodoText: '',
  setNewTodoText: (text) => set({ newTodoText: text }),
}))

// --- The Collaborative Component ---
function CollaborativeTodoList() {
  // 2. Get local state and actions from our Zustand store
  const { newTodoText, setNewTodoText } = useTodoStore()

  // 3. Get shared, real-time state from Vaultrice
  // The 'todos' array is synced between all users.
  const [todos, { push }, error, isLoading] = useNonLocalArray('todo-list-project', 'tasks', {
    credentials: { /* Your Vaultrice Credentials */ }
  })

  const handleAddTodo = (e) => {
    e.preventDefault();
    if (!newTodoText.trim()) return;

    // 4. Use the atomic 'push' action to update the shared state
    // This is a safe, real-time operation that prevents race conditions.
    push({ id: Date.now(), text: newTodoText, completed: false });

    // 5. Clear the local input state
    setNewTodoText('');
  };

  if (isLoading) return <div>Loading tasks...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Add a new task..."
        />
        <button type="submit">Add</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is the perfect separation of concerns. Zustand handles the temporary, local input state, while Vaultrice handles the persistent, shared array of todos with a race-condition-free push operation.

The Full React Ecosystem: @vaultrice/react

The useNonLocalArray hook is just one part of a comprehensive React library designed to make using Vaultrice feel native. The @vaultrice/react package includes a full suite of specialized hooks for every use case:

  • useNonLocalState: For managing a single, simple value.
  • useNonLocalCounter: For atomic increment/decrement operations.
  • useNonLocalArray: For managing lists with atomic push and splice.
  • useNonLocalObject: For managing objects with atomic merge and setIn.
  • useMessaging: For real-time chat and presence features.

Conclusion

So, Zustand or Vaultrice? The answer is a definitive "both."

  • For state that lives and dies in a single browser tab, Zustand is a phenomenal choice.
  • For state that needs to be shared, synced, and persisted across tabs, devices, and users, Vaultrice is the solution.

By understanding their different roles, you can combine them to write cleaner, more powerful, and truly collaborative applications.

Get Started with Vaultrice for Free

Top comments (2)

Collapse
 
ahrjarrett profile image
andrew jarrett

For these cases, Vaultrice provides OCC. When you setItem, you can provide the updatedAt timestamp of the data you originally read. The server will only apply your update if the data hasn't changed since you read it.

Interesting... is there a way to query the previous updatedAt timestamp, or do I keep track of that myself?

Collapse
 
adrai profile image
Adriano Raiano

each item has value, createdAt, updatedAt, expiresAt