zustand-state
State management with Zustand for Remix/React applications. Covers store creation, TypeScript patterns, persistence, and Shopify-specific state patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
State management with Zustand for Remix/React applications. Covers store creation, TypeScript patterns, persistence, and Shopify-specific state patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Create, critique, regenerate, or validate Shopify App Store app logos/icons using Shopify's current app icon guidance. Use when the user asks for a Shopify app logo, Shopify App Store icon, app listing icon, Dev Dashboard app icon, branded square logo, logo prompt, icon validation, or review-safe visual direction for Shopify app submission.
Create, critique, or regenerate Shopify App Store feature images for app listings using Shopify's current App Store media guidance and the $imagegen skill for actual image generation/editing. Use when the user asks for a Shopify app feature image, App Store listing hero image, marketing image, listing media, image prompt, image validation, or review-safe visual direction for Shopify app submission.
Create comprehensive Shopify App Store listing content following official best practices. Use when users need to write or improve their app listing for the Shopify App Store, including app introduction, app details, features, app card subtitle, search terms, SEO content (title tag, meta description), and testing instructions. Also applicable when preparing an app submission for Shopify review.
Generate and maintain changelogs following Keep a Changelog format. Analyzes git commits, categorizes changes, and produces well-structured release notes.
Guide for implementing Shopify's Billing API in Remix apps using @shopify/shopify-app-remix. Covers subscriptions, one-time purchases, usage-based billing, discounts, and the project's billing implementation patterns.
Comprehensive code investigation and audit tool. Discovers all project features, then dispatches parallel subagents to analyze issues, risks, dead code, missing functionality, and redundancies. Produces a prioritized risk report. Use this skill when the user asks to "investigate code", "audit project", "find risks", "check code quality", "analyze codebase", "what's wrong with this code", "project health check", "code review entire project", "find dead code", "find redundant code", or any request for a thorough codebase analysis.
| name | zustand-state |
| description | State management with Zustand for Remix/React applications. Covers store creation, TypeScript patterns, persistence, and Shopify-specific state patterns. |
Zustand is a lightweight, fast state management solution perfect for Shopify Remix apps. It's simpler than Redux, more flexible than Context, and works seamlessly with TypeScript.
npm install zustand
// app/stores/app-store.ts
import { create } from 'zustand';
interface AppState {
// State
isLoading: boolean;
selectedProductIds: string[];
// Actions
setLoading: (loading: boolean) => void;
selectProduct: (id: string) => void;
deselectProduct: (id: string) => void;
clearSelection: () => void;
}
export const useAppStore = create<AppState>((set) => ({
// Initial state
isLoading: false,
selectedProductIds: [],
// Actions
setLoading: (loading) => set({ isLoading: loading }),
selectProduct: (id) => set((state) => ({
selectedProductIds: [...state.selectedProductIds, id]
})),
deselectProduct: (id) => set((state) => ({
selectedProductIds: state.selectedProductIds.filter(pid => pid !== id)
})),
clearSelection: () => set({ selectedProductIds: [] }),
}));
// app/components/ProductList.tsx
import { useAppStore } from '~/stores/app-store';
export function ProductList() {
// Select only what you need (prevents unnecessary re-renders)
const selectedIds = useAppStore((state) => state.selectedProductIds);
const selectProduct = useAppStore((state) => state.selectProduct);
return (
<ResourceList
items={products}
renderItem={(product) => (
<ResourceItem
id={product.id}
selected={selectedIds.includes(product.id)}
onClick={() => selectProduct(product.id)}
/>
)}
/>
);
}
// app/stores/slices/product-slice.ts
import { StateCreator } from 'zustand';
export interface ProductSlice {
products: Product[];
selectedProductId: string | null;
setProducts: (products: Product[]) => void;
selectProduct: (id: string | null) => void;
}
export const createProductSlice: StateCreator<ProductSlice> = (set) => ({
products: [],
selectedProductId: null,
setProducts: (products) => set({ products }),
selectProduct: (id) => set({ selectedProductId: id }),
});
// app/stores/slices/ui-slice.ts
import { StateCreator } from 'zustand';
export interface UISlice {
isSidebarOpen: boolean;
activeModal: string | null;
toggleSidebar: () => void;
openModal: (modalId: string) => void;
closeModal: () => void;
}
export const createUISlice: StateCreator<UISlice> = (set) => ({
isSidebarOpen: true,
activeModal: null,
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
openModal: (modalId) => set({ activeModal: modalId }),
closeModal: () => set({ activeModal: null }),
});
// app/stores/index.ts
import { create } from 'zustand';
import { ProductSlice, createProductSlice } from './slices/product-slice';
import { UISlice, createUISlice } from './slices/ui-slice';
type StoreState = ProductSlice & UISlice;
export const useStore = create<StoreState>()((...args) => ({
...createProductSlice(...args),
...createUISlice(...args),
}));
// app/stores/selectors.ts
import { useStore } from './index';
import { useShallow } from 'zustand/react/shallow';
// Single value selector
export const useSelectedProductId = () =>
useStore((state) => state.selectedProductId);
// Multiple values with shallow compare (prevents unnecessary re-renders)
export const useProductState = () =>
useStore(
useShallow((state) => ({
products: state.products,
selectedId: state.selectedProductId,
}))
);
// Derived state
export const useSelectedProduct = () =>
useStore((state) =>
state.products.find(p => p.id === state.selectedProductId)
);
// app/stores/settings-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark';
itemsPerPage: number;
setTheme: (theme: 'light' | 'dark') => void;
setItemsPerPage: (count: number) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
itemsPerPage: 25,
setTheme: (theme) => set({ theme }),
setItemsPerPage: (count) => set({ itemsPerPage: count }),
}),
{
name: 'app-settings', // localStorage key
storage: createJSONStorage(() => localStorage),
// Only persist specific fields
partialize: (state) => ({
theme: state.theme,
itemsPerPage: state.itemsPerPage
}),
}
)
);
// app/stores/persisted-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// Prevent hydration mismatch
const storage = createJSONStorage(() => {
if (typeof window === 'undefined') {
return {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
}
return localStorage;
});
export const usePersistedStore = create<State>()(
persist(
(set) => ({
// ... state and actions
}),
{
name: 'my-store',
storage,
skipHydration: true, // Manual hydration control
}
)
);
// In root.tsx or layout
useEffect(() => {
usePersistedStore.persist.rehydrate();
}, []);
// app/stores/shop-store.ts
import { create } from 'zustand';
interface ShopInfo {
id: string;
name: string;
domain: string;
plan: string;
currency: string;
}
interface ShopState {
shop: ShopInfo | null;
isAuthenticated: boolean;
setShop: (shop: ShopInfo) => void;
clearShop: () => void;
}
export const useShopStore = create<ShopState>((set) => ({
shop: null,
isAuthenticated: false,
setShop: (shop) => set({ shop, isAuthenticated: true }),
clearShop: () => set({ shop: null, isAuthenticated: false }),
}));
// app/stores/bulk-operations-store.ts
import { create } from 'zustand';
type OperationStatus = 'idle' | 'running' | 'completed' | 'failed';
interface BulkOperation {
id: string;
type: 'product-update' | 'inventory-sync' | 'price-change';
status: OperationStatus;
progress: number;
totalItems: number;
processedItems: number;
errors: string[];
}
interface BulkOperationsState {
operations: Map<string, BulkOperation>;
activeOperationId: string | null;
startOperation: (id: string, type: BulkOperation['type'], totalItems: number) => void;
updateProgress: (id: string, processedItems: number) => void;
completeOperation: (id: string) => void;
failOperation: (id: string, error: string) => void;
clearOperation: (id: string) => void;
}
export const useBulkOperationsStore = create<BulkOperationsState>((set, get) => ({
operations: new Map(),
activeOperationId: null,
startOperation: (id, type, totalItems) => set((state) => {
const operations = new Map(state.operations);
operations.set(id, {
id,
type,
status: 'running',
progress: 0,
totalItems,
processedItems: 0,
errors: [],
});
return { operations, activeOperationId: id };
}),
updateProgress: (id, processedItems) => set((state) => {
const operations = new Map(state.operations);
const op = operations.get(id);
if (op) {
operations.set(id, {
...op,
processedItems,
progress: Math.round((processedItems / op.totalItems) * 100),
});
}
return { operations };
}),
completeOperation: (id) => set((state) => {
const operations = new Map(state.operations);
const op = operations.get(id);
if (op) {
operations.set(id, { ...op, status: 'completed', progress: 100 });
}
return {
operations,
activeOperationId: state.activeOperationId === id ? null : state.activeOperationId
};
}),
failOperation: (id, error) => set((state) => {
const operations = new Map(state.operations);
const op = operations.get(id);
if (op) {
operations.set(id, {
...op,
status: 'failed',
errors: [...op.errors, error]
});
}
return { operations };
}),
clearOperation: (id) => set((state) => {
const operations = new Map(state.operations);
operations.delete(id);
return { operations };
}),
}));
// app/stores/resource-picker-store.ts
import { create } from 'zustand';
interface SelectedResource {
id: string;
title: string;
handle?: string;
images?: { originalSrc: string }[];
}
interface ResourcePickerState {
isOpen: boolean;
resourceType: 'Product' | 'Collection' | 'Customer' | null;
selectedResources: SelectedResource[];
maxSelectable: number;
openPicker: (type: ResourcePickerState['resourceType'], max?: number) => void;
closePicker: () => void;
setSelectedResources: (resources: SelectedResource[]) => void;
clearSelection: () => void;
}
export const useResourcePickerStore = create<ResourcePickerState>((set) => ({
isOpen: false,
resourceType: null,
selectedResources: [],
maxSelectable: 1,
openPicker: (type, max = 1) => set({
isOpen: true,
resourceType: type,
maxSelectable: max
}),
closePicker: () => set({ isOpen: false }),
setSelectedResources: (resources) => set({
selectedResources: resources,
isOpen: false,
}),
clearSelection: () => set({ selectedResources: [] }),
}));
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useStore = create<State>()(
devtools(
(set) => ({
// ... state and actions
}),
{
name: 'ShopifyAppStore',
enabled: process.env.NODE_ENV === 'development',
}
)
);
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface State {
products: Product[];
updateProduct: (id: string, updates: Partial<Product>) => void;
}
export const useStore = create<State>()(
immer((set) => ({
products: [],
updateProduct: (id, updates) => set((state) => {
const product = state.products.find(p => p.id === id);
if (product) {
Object.assign(product, updates); // Direct mutation is OK with immer
}
}),
}))
);
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
export const useStore = create<State>()(
devtools(
persist(
immer((set) => ({
// ... state and actions
})),
{ name: 'app-store' }
),
{ name: 'ShopifyAppStore' }
)
);
// app/stores/__tests__/app-store.test.ts
import { act, renderHook } from '@testing-library/react';
import { useAppStore } from '../app-store';
describe('useAppStore', () => {
beforeEach(() => {
// Reset store before each test
useAppStore.setState({
isLoading: false,
selectedProductIds: [],
});
});
it('should select a product', () => {
const { result } = renderHook(() => useAppStore());
act(() => {
result.current.selectProduct('gid://shopify/Product/123');
});
expect(result.current.selectedProductIds).toContain('gid://shopify/Product/123');
});
it('should clear selection', () => {
useAppStore.setState({ selectedProductIds: ['1', '2', '3'] });
const { result } = renderHook(() => useAppStore());
act(() => {
result.current.clearSelection();
});
expect(result.current.selectedProductIds).toHaveLength(0);
});
});
// BAD - Server data should come from loaders
const useStore = create((set) => ({
products: [], // Server data
fetchProducts: async () => {
const res = await fetch('/api/products');
set({ products: await res.json() });
},
}));
// GOOD - Use Remix loaders for server data
// Use Zustand only for UI state
export async function loader() {
return json({ products: await getProducts() });
}
// BAD - Creates new store on every render
function MyComponent() {
const useLocalStore = create((set) => ({ count: 0 }));
// ...
}
// GOOD - Define stores outside components
const useLocalStore = create((set) => ({ count: 0 }));
function MyComponent() {
const count = useLocalStore((s) => s.count);
// ...
}
// BAD - Re-renders on ANY state change
const state = useStore();
// GOOD - Select only what you need
const count = useStore((s) => s.count);
const { count, increment } = useStore(useShallow((s) => ({
count: s.count,
increment: s.increment
})));
setState for setup