Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:53 +08:00
commit 36a2b87167
20 changed files with 3513 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
/**
* Zustand Store with Async Actions
*
* Use when:
* - Fetching data from APIs
* - Need loading/error states
* - Handling async operations
*
* Pattern: Actions can be async, just call set() when done
*
* Features:
* - Loading states
* - Error handling
* - Optimistic updates
* - Request cancellation
*
* NOTE: For server state (data fetching), consider TanStack Query instead!
* Zustand is better for client state. But this pattern works for simple cases.
*
* Learn more: See SKILL.md Common Patterns section
*/
import { create } from 'zustand'
interface User {
id: string
name: string
email: string
}
interface Post {
id: string
title: string
body: string
userId: string
}
interface AsyncStore {
// Data state
user: User | null
posts: Post[]
// Loading states
isLoadingUser: boolean
isLoadingPosts: boolean
isSavingPost: boolean
// Error states
userError: string | null
postsError: string | null
saveError: string | null
// Actions
fetchUser: (userId: string) => Promise<void>
fetchPosts: (userId: string) => Promise<void>
createPost: (post: Omit<Post, 'id'>) => Promise<void>
deletePost: (postId: string) => Promise<void>
reset: () => void
}
export const useAsyncStore = create<AsyncStore>()((set, get) => ({
// Initial state
user: null,
posts: [],
isLoadingUser: false,
isLoadingPosts: false,
isSavingPost: false,
userError: null,
postsError: null,
saveError: null,
// Fetch user
fetchUser: async (userId) => {
set({ isLoadingUser: true, userError: null })
try {
const response = await fetch(`https://api.example.com/users/${userId}`)
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`)
}
const user = await response.json()
set({ user, isLoadingUser: false })
} catch (error) {
set({
userError: (error as Error).message,
isLoadingUser: false,
user: null,
})
}
},
// Fetch posts
fetchPosts: async (userId) => {
set({ isLoadingPosts: true, postsError: null })
try {
const response = await fetch(`https://api.example.com/users/${userId}/posts`)
if (!response.ok) {
throw new Error(`Failed to fetch posts: ${response.statusText}`)
}
const posts = await response.json()
set({ posts, isLoadingPosts: false })
} catch (error) {
set({
postsError: (error as Error).message,
isLoadingPosts: false,
})
}
},
// Create post with optimistic update
createPost: async (post) => {
const tempId = `temp-${Date.now()}`
const optimisticPost = { ...post, id: tempId }
// Optimistic update
set((state) => ({
posts: [...state.posts, optimisticPost],
isSavingPost: true,
saveError: null,
}))
try {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
})
if (!response.ok) {
throw new Error(`Failed to create post: ${response.statusText}`)
}
const savedPost = await response.json()
// Replace optimistic post with real one
set((state) => ({
posts: state.posts.map((p) =>
p.id === tempId ? savedPost : p
),
isSavingPost: false,
}))
} catch (error) {
// Rollback optimistic update
set((state) => ({
posts: state.posts.filter((p) => p.id !== tempId),
saveError: (error as Error).message,
isSavingPost: false,
}))
}
},
// Delete post
deletePost: async (postId) => {
// Store original posts for rollback
const originalPosts = get().posts
// Optimistic update
set((state) => ({
posts: state.posts.filter((p) => p.id !== postId),
}))
try {
const response = await fetch(`https://api.example.com/posts/${postId}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error(`Failed to delete post: ${response.statusText}`)
}
} catch (error) {
// Rollback on error
set({
posts: originalPosts,
saveError: (error as Error).message,
})
}
},
// Reset
reset: () =>
set({
user: null,
posts: [],
isLoadingUser: false,
isLoadingPosts: false,
isSavingPost: false,
userError: null,
postsError: null,
saveError: null,
}),
}))
/**
* Usage in components:
*
* function UserProfile({ userId }: { userId: string }) {
* const user = useAsyncStore((state) => state.user)
* const isLoading = useAsyncStore((state) => state.isLoadingUser)
* const error = useAsyncStore((state) => state.userError)
* const fetchUser = useAsyncStore((state) => state.fetchUser)
*
* useEffect(() => {
* fetchUser(userId)
* }, [userId, fetchUser])
*
* if (isLoading) 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>
* )
* }
*
* function PostsList({ userId }: { userId: string }) {
* const posts = useAsyncStore((state) => state.posts)
* const isLoading = useAsyncStore((state) => state.isLoadingPosts)
* const error = useAsyncStore((state) => state.postsError)
* const fetchPosts = useAsyncStore((state) => state.fetchPosts)
* const deletePost = useAsyncStore((state) => state.deletePost)
*
* useEffect(() => {
* fetchPosts(userId)
* }, [userId, fetchPosts])
*
* if (isLoading) return <div>Loading posts...</div>
* if (error) return <div>Error: {error}</div>
*
* return (
* <ul>
* {posts.map((post) => (
* <li key={post.id}>
* <h3>{post.title}</h3>
* <p>{post.body}</p>
* <button onClick={() => deletePost(post.id)}>Delete</button>
* </li>
* ))}
* </ul>
* )
* }
*
* function CreatePostForm({ userId }: { userId: string }) {
* const [title, setTitle] = useState('')
* const [body, setBody] = useState('')
* const createPost = useAsyncStore((state) => state.createPost)
* const isSaving = useAsyncStore((state) => state.isSavingPost)
* const error = useAsyncStore((state) => state.saveError)
*
* const handleSubmit = async (e: FormEvent) => {
* e.preventDefault()
* await createPost({ title, body, userId })
* setTitle('')
* setBody('')
* }
*
* return (
* <form onSubmit={handleSubmit}>
* <input
* value={title}
* onChange={(e) => setTitle(e.target.value)}
* placeholder="Title"
* />
* <textarea
* value={body}
* onChange={(e) => setBody(e.target.value)}
* placeholder="Body"
* />
* <button type="submit" disabled={isSaving}>
* {isSaving ? 'Creating...' : 'Create Post'}
* </button>
* {error && <div>Error: {error}</div>}
* </form>
* )
* }
*
* ADVANCED: Request Cancellation with AbortController
*
* let abortController: AbortController | null = null
*
* fetchUserWithCancellation: async (userId) => {
* // Cancel previous request
* abortController?.abort()
* abortController = new AbortController()
*
* set({ isLoadingUser: true, userError: null })
*
* try {
* const response = await fetch(
* `https://api.example.com/users/${userId}`,
* { signal: abortController.signal }
* )
* // ... rest of fetch logic
* } catch (error) {
* if (error.name === 'AbortError') {
* // Request was cancelled
* return
* }
* // ... handle other errors
* }
* }
*/

41
templates/basic-store.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* Basic Zustand Store (JavaScript style, no TypeScript)
*
* Use when:
* - Prototyping quickly
* - Small applications
* - No TypeScript in project
*
* Learn more: See SKILL.md for TypeScript version
*/
import { create } from 'zustand'
// Create store with minimal setup
export const useStore = create((set) => ({
// State
count: 0,
user: null,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null }),
}))
/**
* Usage in component:
*
* function Counter() {
* const count = useStore((state) => state.count)
* const increment = useStore((state) => state.increment)
*
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*/

214
templates/computed-store.ts Normal file
View File

@@ -0,0 +1,214 @@
/**
* Zustand Store with Computed/Derived Values
*
* Use when:
* - Need values calculated from state
* - Want to avoid storing redundant data
* - Need efficient memoization
*
* Pattern: Compute in selectors, not in store
*
* Benefits:
* - Cleaner state (only source data)
* - Automatic recomputation when dependencies change
* - Components only re-render when result changes
*
* Learn more: See SKILL.md Common Patterns section
*/
import { create } from 'zustand'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface CartItem {
productId: string
quantity: number
}
interface ComputedStore {
// Source state (stored)
products: Product[]
cart: CartItem[]
taxRate: number
discountPercent: number
// Actions
addProduct: (product: Product) => void
addToCart: (productId: string, quantity: number) => void
removeFromCart: (productId: string) => void
setTaxRate: (rate: number) => void
setDiscount: (percent: number) => void
}
export const useComputedStore = create<ComputedStore>()((set) => ({
// Initial state
products: [],
cart: [],
taxRate: 0.1, // 10%
discountPercent: 0,
// Actions
addProduct: (product) =>
set((state) => ({
products: [...state.products, product],
})),
addToCart: (productId, quantity) =>
set((state) => {
const existing = state.cart.find((item) => item.productId === productId)
if (existing) {
return {
cart: state.cart.map((item) =>
item.productId === productId
? { ...item, quantity: item.quantity + quantity }
: item
),
}
}
return {
cart: [...state.cart, { productId, quantity }],
}
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((item) => item.productId !== productId),
})),
setTaxRate: (rate) => set({ taxRate: rate }),
setDiscount: (percent) => set({ discountPercent: percent }),
}))
// ============================================================================
// COMPUTED/DERIVED SELECTORS (put these in separate file for reuse)
// ============================================================================
/**
* Selector: Get cart items with product details
*/
export const selectCartWithDetails = (state: ComputedStore) =>
state.cart.map((item) => {
const product = state.products.find((p) => p.id === item.productId)
return {
...item,
product,
subtotal: product ? product.price * item.quantity : 0,
}
})
/**
* Selector: Calculate subtotal (before tax and discount)
*/
export const selectSubtotal = (state: ComputedStore) =>
state.cart.reduce((sum, item) => {
const product = state.products.find((p) => p.id === item.productId)
return sum + (product ? product.price * item.quantity : 0)
}, 0)
/**
* Selector: Calculate discount amount
*/
export const selectDiscountAmount = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
return subtotal * (state.discountPercent / 100)
}
/**
* Selector: Calculate tax amount
*/
export const selectTaxAmount = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
const discountAmount = selectDiscountAmount(state)
const afterDiscount = subtotal - discountAmount
return afterDiscount * state.taxRate
}
/**
* Selector: Calculate final total
*/
export const selectTotal = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
const discountAmount = selectDiscountAmount(state)
const taxAmount = selectTaxAmount(state)
return subtotal - discountAmount + taxAmount
}
/**
* Selector: Get products by category
*/
export const selectProductsByCategory = (category: string) => (state: ComputedStore) =>
state.products.filter((product) => product.category === category)
/**
* Selector: Get in-stock products only
*/
export const selectInStockProducts = (state: ComputedStore) =>
state.products.filter((product) => product.inStock)
/**
* Selector: Count items in cart
*/
export const selectCartItemCount = (state: ComputedStore) =>
state.cart.reduce((sum, item) => sum + item.quantity, 0)
/**
* Usage in components:
*
* function CartSummary() {
* // Each selector only causes re-render when its result changes
* const subtotal = useComputedStore(selectSubtotal)
* const discount = useComputedStore(selectDiscountAmount)
* const tax = useComputedStore(selectTaxAmount)
* const total = useComputedStore(selectTotal)
* const itemCount = useComputedStore(selectCartItemCount)
*
* return (
* <div>
* <h3>Cart ({itemCount} items)</h3>
* <p>Subtotal: ${subtotal.toFixed(2)}</p>
* <p>Discount: -${discount.toFixed(2)}</p>
* <p>Tax: ${tax.toFixed(2)}</p>
* <h4>Total: ${total.toFixed(2)}</h4>
* </div>
* )
* }
*
* function ProductList() {
* const inStockProducts = useComputedStore(selectInStockProducts)
*
* return (
* <ul>
* {inStockProducts.map((product) => (
* <li key={product.id}>{product.name} - ${product.price}</li>
* ))}
* </ul>
* )
* }
*
* function CategoryFilter() {
* const electronicsProducts = useComputedStore(selectProductsByCategory('electronics'))
*
* return <div>{electronicsProducts.length} electronics</div>
* }
*
* PERFORMANCE TIP:
* For expensive computations, use useMemo in the component:
*
* function ExpensiveComputation() {
* const data = useComputedStore((state) => state.products)
*
* const result = useMemo(() => {
* // Expensive calculation
* return data.map(...).filter(...).reduce(...)
* }, [data])
*
* return <div>{result}</div>
* }
*/

141
templates/devtools-store.ts Normal file
View File

@@ -0,0 +1,141 @@
/**
* Zustand Store with Redux DevTools Integration
*
* Use when:
* - Need debugging capabilities
* - Want to track state changes
* - Inspecting actions and state history
* - Time-travel debugging (with Redux DevTools Extension)
*
* Setup:
* 1. Install Redux DevTools Extension in browser
* 2. Use this template
* 3. Open DevTools → Redux tab
*
* Learn more: See SKILL.md for combining with other middleware
*/
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface Todo {
id: string
text: string
done: boolean
}
interface DevtoolsStore {
// State
todos: Todo[]
filter: 'all' | 'active' | 'completed'
// Actions
addTodo: (text: string) => void
toggleTodo: (id: string) => void
deleteTodo: (id: string) => void
setFilter: (filter: DevtoolsStore['filter']) => void
clearCompleted: () => void
}
export const useDevtoolsStore = create<DevtoolsStore>()(
devtools(
(set) => ({
// Initial state
todos: [],
filter: 'all',
// Actions with named actions for DevTools
addTodo: (text) =>
set(
(state) => ({
todos: [
...state.todos,
{ id: Date.now().toString(), text, done: false },
],
}),
undefined,
'todos/add', // Action name in DevTools
),
toggleTodo: (id) =>
set(
(state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
),
}),
undefined,
'todos/toggle',
),
deleteTodo: (id) =>
set(
(state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
}),
undefined,
'todos/delete',
),
setFilter: (filter) =>
set(
{ filter },
undefined,
'filter/set',
),
clearCompleted: () =>
set(
(state) => ({
todos: state.todos.filter((todo) => !todo.done),
}),
undefined,
'todos/clearCompleted',
),
}),
{
name: 'TodoStore', // Store name in DevTools
enabled: process.env.NODE_ENV === 'development', // Optional: only in dev
},
),
)
/**
* Usage in component:
*
* function TodoList() {
* const todos = useDevtoolsStore((state) => state.todos)
* const filter = useDevtoolsStore((state) => state.filter)
* const toggleTodo = useDevtoolsStore((state) => state.toggleTodo)
* const deleteTodo = useDevtoolsStore((state) => state.deleteTodo)
*
* const filteredTodos = todos.filter((todo) => {
* if (filter === 'active') return !todo.done
* if (filter === 'completed') return todo.done
* return true
* })
*
* return (
* <ul>
* {filteredTodos.map((todo) => (
* <li key={todo.id}>
* <input
* type="checkbox"
* checked={todo.done}
* onChange={() => toggleTodo(todo.id)}
* />
* <span>{todo.text}</span>
* <button onClick={() => deleteTodo(todo.id)}>Delete</button>
* </li>
* ))}
* </ul>
* )
* }
*
* DevTools Features:
* - See all actions in order
* - Inspect state before/after each action
* - Time-travel through state changes
* - Export/import state for debugging
* - Track which components caused updates
*/

109
templates/nextjs-store.ts Normal file
View File

@@ -0,0 +1,109 @@
/**
* Next.js SSR-Safe Zustand Store with Hydration Handling
*
* Use when:
* - Using persist middleware in Next.js
* - Getting hydration mismatch errors
* - Need SSR compatibility
*
* This template solves:
* - "Text content does not match server-rendered HTML"
* - "Hydration failed" errors
* - Flashing content during hydration
*
* Learn more: See SKILL.md Issue #1 for detailed explanation
*/
'use client' // Required for Next.js App Router
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface User {
id: string
name: string
}
interface NextJsStore {
// State
_hasHydrated: boolean // CRITICAL: Track hydration status
count: number
user: User | null
// Actions
setHasHydrated: (hydrated: boolean) => void
increment: () => void
setUser: (user: User | null) => void
reset: () => void
}
export const useNextJsStore = create<NextJsStore>()(
persist(
(set) => ({
// Initial state
_hasHydrated: false, // Start false
count: 0,
user: null,
// Hydration action
setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
// Regular actions
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null }),
}),
{
name: 'nextjs-storage',
// CRITICAL: Call setHasHydrated when rehydration completes
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true)
},
},
),
)
/**
* Usage in Next.js component:
*
* 'use client'
*
* import { useNextJsStore } from './store'
*
* function Counter() {
* const hasHydrated = useNextJsStore((state) => state._hasHydrated)
* const count = useNextJsStore((state) => state.count)
* const increment = useNextJsStore((state) => state.increment)
*
* // Show loading state during hydration
* if (!hasHydrated) {
* return <div>Loading...</div>
* }
*
* // Now safe to render with persisted state
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*
* Alternative: Skip hydration check if flashing is acceptable
*
* function CounterNoLoadingState() {
* const count = useNextJsStore((state) => state.count)
* const increment = useNextJsStore((state) => state.increment)
*
* // Will show default value (0) until hydration, then correct value
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*/

119
templates/persist-store.ts Normal file
View File

@@ -0,0 +1,119 @@
/**
* Persistent Zustand Store (localStorage/sessionStorage)
*
* Use when:
* - State needs to survive page reloads
* - User preferences (theme, language, etc.)
* - Shopping carts, draft forms
*
* Features:
* - Automatic save to localStorage
* - Automatic restore on load
* - Migration support for schema changes
* - Partial persistence (only save some fields)
*
* Learn more: See SKILL.md Issue #1 for Next.js hydration handling
*/
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface User {
id: string
name: string
email: string
}
interface PersistedStore {
// State
theme: 'light' | 'dark' | 'system'
language: string
user: User | null
preferences: {
notifications: boolean
emailUpdates: boolean
}
// Actions
setTheme: (theme: PersistedStore['theme']) => void
setLanguage: (language: string) => void
setUser: (user: User | null) => void
updatePreferences: (prefs: Partial<PersistedStore['preferences']>) => void
reset: () => void
}
const initialState = {
theme: 'system' as const,
language: 'en',
user: null,
preferences: {
notifications: true,
emailUpdates: false,
},
}
export const usePersistedStore = create<PersistedStore>()(
persist(
(set) => ({
...initialState,
// Actions
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
setUser: (user) => set({ user }),
updatePreferences: (prefs) =>
set((state) => ({
preferences: { ...state.preferences, ...prefs },
})),
reset: () => set(initialState),
}),
{
name: 'app-storage', // unique name in localStorage
// Optional: use sessionStorage instead
// storage: createJSONStorage(() => sessionStorage),
// Optional: only persist specific fields
// partialize: (state) => ({
// theme: state.theme,
// language: state.language,
// preferences: state.preferences,
// // Don't persist user (privacy)
// }),
// Optional: version and migration for schema changes
version: 1,
migrate: (persistedState: any, version) => {
if (version === 0) {
// Migration from version 0 to 1
// Example: rename field
persistedState.language = persistedState.lang || 'en'
delete persistedState.lang
}
return persistedState
},
},
),
)
/**
* Usage in component:
*
* function ThemeToggle() {
* const theme = usePersistedStore((state) => state.theme)
* const setTheme = usePersistedStore((state) => state.setTheme)
*
* return (
* <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
* Toggle theme
* </button>
* )
* }
*
* NEXT.JS WARNING:
* For Next.js with SSR, see nextjs-store.ts template for hydration handling!
*/

217
templates/slices-pattern.ts Normal file
View File

@@ -0,0 +1,217 @@
/**
* Zustand Slices Pattern (Modular Store Architecture)
*
* Use when:
* - Store is getting large (100+ lines)
* - Multiple developers working on store
* - Need to organize state by domain/feature
*
* Benefits:
* - Each slice is independent and testable
* - Slices can access each other's state/actions
* - Easy to add/remove features
* - Better code organization
*
* Learn more: See SKILL.md Issue #5 for TypeScript complexity handling
*/
import { create, StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
// ============================================================================
// SLICE 1: User Management
// ============================================================================
interface User {
id: string
name: string
email: string
}
interface UserSlice {
user: User | null
isAuthenticated: boolean
setUser: (user: User) => void
logout: () => void
}
const createUserSlice: StateCreator<
UserSlice & CartSlice & NotificationSlice, // Combined type
[['zustand/devtools', never]], // Middleware mutators
[], // Chained middleware
UserSlice // This slice's type
> = (set) => ({
user: null,
isAuthenticated: false,
setUser: (user) =>
set(
{ user, isAuthenticated: true },
undefined,
'user/setUser',
),
logout: () =>
set(
{ user: null, isAuthenticated: false },
undefined,
'user/logout',
),
})
// ============================================================================
// SLICE 2: Shopping Cart
// ============================================================================
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartSlice {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
clearCart: () => void
// Access other slices
eatFish: () => void // Example: cross-slice action
}
const createCartSlice: StateCreator<
UserSlice & CartSlice & NotificationSlice,
[['zustand/devtools', never]],
[],
CartSlice
> = (set, get) => ({
items: [],
addItem: (item) =>
set(
(state) => ({
items: [...state.items, item],
}),
undefined,
'cart/addItem',
),
removeItem: (id) =>
set(
(state) => ({
items: state.items.filter((item) => item.id !== id),
}),
undefined,
'cart/removeItem',
),
clearCart: () =>
set(
{ items: [] },
undefined,
'cart/clearCart',
),
// Example: Action that accesses another slice
eatFish: () =>
set(
(state) => ({
// Access notification slice
notifications: state.notifications + 1,
}),
undefined,
'cart/eatFish',
),
})
// ============================================================================
// SLICE 3: Notifications
// ============================================================================
interface NotificationSlice {
notifications: number
messages: string[]
addNotification: (message: string) => void
clearNotifications: () => void
}
const createNotificationSlice: StateCreator<
UserSlice & CartSlice & NotificationSlice,
[['zustand/devtools', never]],
[],
NotificationSlice
> = (set) => ({
notifications: 0,
messages: [],
addNotification: (message) =>
set(
(state) => ({
notifications: state.notifications + 1,
messages: [...state.messages, message],
}),
undefined,
'notifications/add',
),
clearNotifications: () =>
set(
{ notifications: 0, messages: [] },
undefined,
'notifications/clear',
),
})
// ============================================================================
// COMBINE SLICES
// ============================================================================
export const useAppStore = create<UserSlice & CartSlice & NotificationSlice>()(
devtools(
(...a) => ({
...createUserSlice(...a),
...createCartSlice(...a),
...createNotificationSlice(...a),
}),
{ name: 'AppStore' },
),
)
/**
* Usage in components:
*
* function UserProfile() {
* const user = useAppStore((state) => state.user)
* const logout = useAppStore((state) => state.logout)
*
* if (!user) return <div>Not logged in</div>
*
* return (
* <div>
* <p>Welcome, {user.name}!</p>
* <button onClick={logout}>Logout</button>
* </div>
* )
* }
*
* function Cart() {
* const items = useAppStore((state) => state.items)
* const clearCart = useAppStore((state) => state.clearCart)
*
* return (
* <div>
* <h2>Cart ({items.length} items)</h2>
* <button onClick={clearCart}>Clear</button>
* </div>
* )
* }
*
* ORGANIZATION TIP:
* In larger projects, put each slice in its own file:
*
* src/store/
* ├── index.ts (combines slices)
* ├── userSlice.ts
* ├── cartSlice.ts
* └── notificationSlice.ts
*/

View File

@@ -0,0 +1,84 @@
/**
* TypeScript Zustand Store (Recommended for production)
*
* Use when:
* - Production applications
* - Need type safety
* - Want IDE autocomplete
*
* CRITICAL: Notice the double parentheses `create<T>()()` - required for TypeScript
*
* Learn more: See SKILL.md for middleware and advanced patterns
*/
import { create } from 'zustand'
// Define state interface
interface User {
id: string
name: string
email: string
}
// Define store interface (state + actions)
interface AppStore {
// State
count: number
user: User | null
isLoading: boolean
// Actions
increment: () => void
decrement: () => void
reset: () => void
setUser: (user: User) => void
clearUser: () => void
setLoading: (loading: boolean) => void
}
// Create typed store with double parentheses
export const useAppStore = create<AppStore>()((set) => ({
// Initial state
count: 0,
user: null,
isLoading: false,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0, user: null }),
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
setLoading: (loading) => set({ isLoading: loading }),
}))
/**
* Usage in component:
*
* function Counter() {
* // Select single value (component only re-renders when count changes)
* const count = useAppStore((state) => state.count)
* const increment = useAppStore((state) => state.increment)
*
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*
* function UserProfile() {
* const user = useAppStore((state) => state.user)
* const setUser = useAppStore((state) => state.setUser)
*
* if (!user) return <div>No user</div>
*
* return <div>Hello, {user.name}!</div>
* }
*/