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 })),
}))
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 likelocalStorage
(setItem
,getItem
) but is cloud-powered, asynchronous, and real-time. -
SyncObject
: A modern, reactive API that uses a JavaScriptProxy
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 likeArray.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!');
}
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>
);
}
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 atomicpush
andsplice
. -
useNonLocalObject
: For managing objects with atomicmerge
andsetIn
. -
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.
Top comments (2)
Interesting... is there a way to query the previous updatedAt timestamp, or do I keep track of that myself?
each item has value, createdAt, updatedAt, expiresAt