Initial commit
This commit is contained in:
309
templates/async-actions-store.ts
Normal file
309
templates/async-actions-store.ts
Normal 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
41
templates/basic-store.ts
Normal 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
214
templates/computed-store.ts
Normal 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
141
templates/devtools-store.ts
Normal 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
109
templates/nextjs-store.ts
Normal 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
119
templates/persist-store.ts
Normal 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
217
templates/slices-pattern.ts
Normal 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
|
||||
*/
|
||||
84
templates/typescript-store.ts
Normal file
84
templates/typescript-store.ts
Normal 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>
|
||||
* }
|
||||
*/
|
||||
Reference in New Issue
Block a user