Skip to main content

Simple State Management: Making State Easy with Zustic

· 5 min read
Zustic Team
Lightweight State Management Library

State management doesn't have to be complicated. In fact, most applications don't need the complexity of Redux. Let's explore how to manage state simply and effectively with Zustic.

The State Management Journey

As React developers, we typically go through these stages:

Stage 1: useState (Simple Apps)

function Counter() {
const [count, setCount] = useState(0)

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}

Works great for single components!

Stage 2: Prop Drilling (Growing Apps)

function App() {
const [count, setCount] = useState(0)

return (
<Parent count={count} setCount={setCount}>
<Child count={count} setCount={setCount} />
</Parent>
)
}

Passing props through many components gets tedious.

Stage 3: Context API (Medium Apps)

const CountContext = createContext()

function App() {
const [count, setCount] = useState(0)

return (
<CountContext.Provider value={{ count, setCount }}>
<Child />
</CountContext.Provider>
)
}

function Child() {
const { count, setCount } = useContext(CountContext)
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

Works, but requires providers everywhere and causes unnecessary re-renders.

Stage 4: State Management Library (Complex Apps)

This is where Zustic shines! ✨

Why Zustic is the Simplest Solution

Concept 1: Global Store

Think of your app state in one place:

import { create } from 'zustic'

// Define your store
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))

// Use anywhere - no providers!
function Counter() {
const { count, increment, decrement } = useCountStore()

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

That's it! No providers, no context, no complexity.

Concept 2: Multiple Stores

Keep your stores organized:

// User store
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}))

// UI store
const useUIStore = create((set) => ({
isMenuOpen: false,
toggleMenu: () => set((state) => ({ isMenuOpen: !state.isMenuOpen })),
}))

// Cart store
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
}))

// Use them independently!
function App() {
const { user } = useUserStore()
const { isMenuOpen } = useUIStore()
const { items } = useCartStore()

return (
<div>
<h1>Hello {user?.name}</h1>
{isMenuOpen && <Menu />}
<p>Cart items: {items.length}</p>
</div>
)
}

Concept 3: Simple State Updates

Updating state is straightforward:

const useStore = create((set) => ({
// Initial state
count: 0,
user: null,
todos: [],

// Simple updates
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user }),

// Adding to array
addTodo: (todo) => set((state) => ({
todos: [...state.todos, todo]
})),

// Removing from array
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(t => t.id !== id)
})),

// Complex updates
updateTodo: (id, updates) => set((state) => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, ...updates } : t
)
})),
}))

Concept 4: Accessing State Outside Components

Sometimes you need state outside React:

const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))

// In components
function Counter() {
const count = useStore((state) => state.count)
return <div>{count}</div>
}

// Outside components - use get()
useStore.setState({ count: 10 })
console.log(useStore.getState()) // { count: 10 }

Real-World Examples

Example 1: Todo App

type TodoStore = {
todos: Array<{ id: number; text: string; done: boolean }>
addTodo: (text: string) => void
toggleTodo: (id: number) => void
removeTodo: (id: number) => void
clearDone: () => void
}

const useTodoStore = create<TodoStore>((set) => ({
todos: [],

addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: Date.now(),
text,
done: false,
}]
})),

toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
})),

removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),

clearDone: () => set((state) => ({
todos: state.todos.filter(todo => !todo.done)
})),
}))

// Use in component
function TodoApp() {
const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore()
const [input, setInput] = useState('')

return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add todo..."
/>
<button onClick={() => {
addTodo(input)
setInput('')
}}>
Add
</button>

<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}

Example 2: Authentication

type AuthStore = {
user: null | { id: number; email: string; name: string }
isLoading: boolean
login: (email: string, password: string) => Promise<void>
logout: () => void
}

const useAuthStore = create<AuthStore>((set) => ({
user: null,
isLoading: false,

login: async (email, password) => {
set({ isLoading: true })
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
const user = await response.json()
set({ user, isLoading: false })
} catch (error) {
set({ isLoading: false })
throw error
}
},

logout: () => set({ user: null }),
}))

// Use in component
function LoginForm() {
const { login, isLoading } = useAuthStore()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')

const handleSubmit = async (e) => {
e.preventDefault()
try {
await login(email, password)
// User logged in!
} catch (error) {
console.error('Login failed:', error)
}
}

return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
type="password"
/>
<button disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
)
}

Key Takeaways

Simple to understand - Just JavaScript functions ✅ No boilerplate - No actions, reducers, or providers ✅ TypeScript friendly - Full type inference ✅ Flexible - Works with async, nested state, arrays ✅ Fast - Only re-renders when subscribed state changes ✅ Powerful - Add middleware for logging, persistence, validation

State Management Decision Tree

Is your app small?
├─ Yes → useState is fine
└─ No → Zustic!

Do you need complex patterns?
├─ Yes → Zustic + middleware
└─ No → Zustic basic

Do you need enterprise features?
├─ Yes → Consider Redux (but try Zustic first!)
└─ No → Zustic!

Conclusion

State management should be simple. Zustic proves that you don't need complexity to have powerful, scalable state management. In fact, simple is better.

Start with Zustic today and enjoy:

  • ✅ 5-minute learning curve
  • ✅ Zero dependencies
  • ✅ ~500B bundle size
  • ✅ Perfect for teams

Get started: Zustic Installation


Found this helpful? Check out our other blog posts on middleware and React state management!