Skip to main content

Advanced Examples

Explore advanced patterns and real-world use cases with Zustic.

1. Combining Multiple Stores

Use multiple stores together for better organization:

// stores/userStore.ts
interface User {
id: string;
name: string;
email: string;
}

interface UserStore {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
}

export const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}));

// stores/todosStore.ts
interface Todo {
id: string;
text: string;
completed: boolean;
userId: string;
}

interface TodosStore {
todos: Todo[];
addTodo: (text: string, userId: string) => void;
removeTodo: (id: string) => void;
toggleTodo: (id: string) => void;
getUserTodos: (userId: string) => Todo[];
}

export const useTodosStore = create<TodosStore>((set, get) => ({
todos: [],
addTodo: (text, userId) =>
set((state) => ({
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text,
completed: false,
userId,
},
],
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((t) => t.id !== id),
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
),
})),
getUserTodos: (userId) =>
get().todos.filter((t) => t.userId === userId),
}));

// Component using both stores
function Dashboard() {
const user = useUserStore((state) => state.user);
const { addTodo, getUserTodos } = useTodosStore();
const userTodos = user ? getUserTodos(user.id) : [];

return (
<div>
<h1>Welcome, {user?.name}</h1>
<div>
<h2>Your Todos ({userTodos.length})</h2>
{userTodos.map((todo) => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
</div>
);
}

2. Complex State Updates

Handle complex state transformations:

interface Product {
id: string;
name: string;
price: number;
quantity: number;
}

interface ShopStore {
cart: Product[];
total: number;
addToCart: (product: Product) => void;
removeFromCart: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
calculateTotal: () => number;
}

export const useShopStore = create<ShopStore>((set, get) => ({
cart: [],
total: 0,

addToCart: (product) =>
set((state) => {
const existingItem = state.cart.find((p) => p.id === product.id);
let newCart;

if (existingItem) {
newCart = state.cart.map((p) =>
p.id === product.id
? { ...p, quantity: p.quantity + product.quantity }
: p
);
} else {
newCart = [...state.cart, product];
}

const total = newCart.reduce(
(sum, p) => sum + p.price * p.quantity,
0
);

return { cart: newCart, total };
}),

removeFromCart: (id) =>
set((state) => {
const newCart = state.cart.filter((p) => p.id !== id);
const total = newCart.reduce(
(sum, p) => sum + p.price * p.quantity,
0
);
return { cart: newCart, total };
}),

updateQuantity: (id, quantity) =>
set((state) => {
const newCart = state.cart.map((p) =>
p.id === id ? { ...p, quantity } : p
);
const total = newCart.reduce(
(sum, p) => sum + p.price * p.quantity,
0
);
return { cart: newCart, total };
}),

clearCart: () => set({ cart: [], total: 0 }),

calculateTotal: () => {
const state = get();
return state.cart.reduce((sum, p) => sum + p.price * p.quantity, 0);
},
}));

3. Computed Values & Derived State

Calculate derived values from your store state:

interface StatsStore {
scores: number[];
addScore: (score: number) => void;
getAverage: () => number;
getHighest: () => number;
getLowest: () => number;
getMedian: () => number;
}

export const useStatsStore = create<StatsStore>((set, get) => ({
scores: [],

addScore: (score) =>
set((state) => ({
scores: [...state.scores, score],
})),

getAverage: () => {
const { scores } = get();
if (scores.length === 0) return 0;
return scores.reduce((a, b) => a + b, 0) / scores.length;
},

getHighest: () => {
const { scores } = get();
return Math.max(...scores);
},

getLowest: () => {
const { scores } = get();
return Math.min(...scores);
},

getMedian: () => {
const { scores } = get();
const sorted = [...scores].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
},
}));

function Stats() {
const store = useStatsStore();
const average = store.getAverage();
const highest = store.getHighest();

return (
<div>
<p>Average: {average.toFixed(2)}</p>
<p>Highest: {highest}</p>
</div>
);
}

4. Next.js Integration

Using Zustic with Next.js:

// app/store/counterStore.ts
'use client';

import { create } from 'zustic';

interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}

export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));

// app/components/Counter.tsx
'use client';

import { useCounterStore } from '@/app/store/counterStore';

export default function Counter() {
const { count, increment, decrement } = useCounterStore();

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}

// app/page.tsx
import Counter from './components/Counter';

export default function Home() {
return (
<main>
<h1>Counter App</h1>
<Counter />
</main>
);
}

5. React Native Usage

Using Zustic with React Native:

import { create } from 'zustic';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

interface ThemeStore {
isDark: boolean;
toggleTheme: () => void;
}

const useThemeStore = create<ThemeStore>((set) => ({
isDark: false,
toggleTheme: () =>
set((state) => ({ isDark: !state.isDark })),
}));

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 18,
marginBottom: 20,
},
});

function App() {
const { isDark, toggleTheme } = useThemeStore();

return (
<View
style={[
styles.container,
{ backgroundColor: isDark ? '#000' : '#fff' },
]}
>
<Text
style={[
styles.text,
{ color: isDark ? '#fff' : '#000' },
]}
>
{isDark ? 'Dark Mode' : 'Light Mode'}
</Text>
<TouchableOpacity onPress={toggleTheme}>
<Text style={{ color: isDark ? '#fff' : '#000' }}>
Toggle Theme
</Text>
</TouchableOpacity>
</View>
);
}

export default App;

6. Async Operations

Handling async state updates:

interface User {
id: string;
name: string;
email: string;
}

interface UserStore {
user: User | null;
loading: boolean;
error: string | null;
fetchUser: (id: string) => Promise<void>;
}

export const useUserStore = create<UserStore>((set) => ({
user: null,
loading: false,
error: null,

fetchUser: async (id: string) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Unknown error',
loading: false,
});
}
},
}));

function UserProfile({ userId }: { userId: string }) {
const { user, loading, error, fetchUser } = useUserStore();

React.useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;

return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}

7. Form Management

Complete form handling with Zustic:

interface FormState {
values: Record<string, string>;
errors: Record<string, string>;
touched: Record<string, boolean>;
setField: (name: string, value: string) => void;
setError: (name: string, error: string) => void;
setTouched: (name: string) => void;
reset: () => void;
}

const initialValues = {
email: '',
password: '',
name: '',
};

export const useFormStore = create<FormState>((set) => ({
values: initialValues,
errors: {},
touched: {},

setField: (name, value) =>
set((state) => ({
values: { ...state.values, [name]: value },
})),

setError: (name, error) =>
set((state) => ({
errors: { ...state.errors, [name]: error },
})),

setTouched: (name) =>
set((state) => ({
touched: { ...state.touched, [name]: true },
})),

reset: () =>
set({
values: initialValues,
errors: {},
touched: {},
}),
}));

function LoginForm() {
const { values, errors, touched, setField, setError, setTouched, reset } =
useFormStore();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

// Validate
if (!values.email) {
setError('email', 'Email is required');
return;
}
if (!values.password) {
setError('password', 'Password is required');
return;
}

// Submit
try {
await api.login(values);
reset();
} catch (error) {
setError('form', 'Login failed');
}
};

return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={(e) => setField('email', e.target.value)}
onBlur={() => setTouched('email')}
/>
{touched.email && errors.email && <span>{errors.email}</span>}

<input
name="password"
type="password"
value={values.password}
onChange={(e) => setField('password', e.target.value)}
onBlur={() => setTouched('password')}
/>
{touched.password && errors.password && <span>{errors.password}</span>}

<button type="submit">Login</button>
</form>
);
}

8. Middleware for Logging

Add logging middleware to track all state changes:

interface LogEntry {
timestamp: number;
action: string;
prevState: string;
nextState: string;
}

const loggerMiddleware = (set, get) => (next) => async (partial) => {
const prevState = get();
const timestamp = Date.now();

console.log('🔵 Action:',
typeof partial === 'function' ? 'function' : Object.keys(partial)
);
console.log('📊 Previous state:', prevState);

await next(partial);

const nextState = get();
console.log('✅ Updated state:', nextState);
console.log('⏱️ Time:', new Date(timestamp).toISOString());
console.log('---');
};

const useStore = create(
(set, get) => ({
count: 0,
name: '',
increment: () => set((state) => ({ count: state.count + 1 })),
setName: (name: string) => set({ name }),
}),
[loggerMiddleware]
);

9. Middleware for Persistence

Save state to localStorage automatically:

const persistMiddleware = (key: string) => (set, get) => (next) => async (partial) => {
await next(partial);

// Save to localStorage after each update
const state = get();
try {
localStorage.setItem(key, JSON.stringify(state));
console.log('💾 State persisted');
} catch (error) {
console.error('Failed to persist state:', error);
}
};

const useCounterStore = create(
(set, get) => {
// Load from localStorage on init
const saved = localStorage.getItem('counter-store');
const initialState = saved ? JSON.parse(saved) : { count: 0 };

return {
...initialState,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
};
},
[persistMiddleware('counter-store')]
);

10. Middleware for Validation

Validate state before updates:

const validateMiddleware = (set, get) => (next) => async (partial) => {
const state = get();
const updates = typeof partial === 'function' ? partial(state) : partial;

// Validate updates
if ('age' in updates && updates.age < 0) {
console.warn('❌ Age cannot be negative');
return;
}

if ('email' in updates && !updates.email.includes('@')) {
console.warn('❌ Invalid email format');
return;
}

// Valid, proceed with update
await next(partial);
console.log('✅ Validation passed');
};

const useUserStore = create(
(set, get) => ({
email: 'test@example.com',
age: 25,
setEmail: (email: string) => set({ email }),
setAge: (age: number) => set({ age }),
}),
[validateMiddleware]
);

// Usage:
// useUserStore.setAge(-5); // ❌ Blocked
// useUserStore.setEmail('invalid'); // ❌ Blocked
// useUserStore.setAge(30); // ✅ Allowed

11. Combining Multiple Middleware

Chain multiple middleware together:

const timeoutMiddleware = (ms: number) => (set, get) => (next) => async (partial) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Update timeout')), ms)
);

try {
await Promise.race([next(partial), timeout]);
} catch (error) {
console.error('⏱️ Update timed out:', error);
}
};

const debugMiddleware = (set, get) => (next) => async (partial) => {
console.log('🐛 Debug: action triggered');
await next(partial);
};

const useStore = create(
(set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
[
loggerMiddleware,
validateMiddleware,
persistMiddleware('app-store'),
debugMiddleware,
timeoutMiddleware(5000),
]
);

12. Using get with Derived State

Compute values using the current state:

interface CartStore {
items: { id: string; price: number; quantity: number }[];
addItem: (item: any) => void;
removeItem: (id: string) => void;
getTotal: () => number;
getItemCount: () => number;
getSummary: () => string;
}

const useCartStore = create<CartStore>((set, get) => ({
items: [],

addItem: (item) =>
set((state) => ({
items: [...state.items, item],
})),

removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),

// Compute total price
getTotal: () => {
const state = get();
return state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},

// Compute item count
getItemCount: () => {
const state = get();
return state.items.reduce((sum, item) => sum + item.quantity, 0);
},

// Get formatted summary
getSummary: () => {
const state = get();
const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const count = state.items.reduce((sum, item) => sum + item.quantity, 0);
return `${count} items - $${total.toFixed(2)}`;
},
}));

// Usage:
function CartSummary() {
const store = useCartStore();
return <div>{store.getSummary()}</div>;
}

These examples showcase the flexibility and power of Zustic for managing various types of application state. Pick the patterns that fit your use case!