状态管理与 API 集成
约 646 字大约 2 分钟
布欧-Lewyon
2026-05-15
Zustand Store
// store/board-store.ts
import { create } from 'zustand'
import { arrayMove } from '@dnd-kit/sortable'
interface BoardState {
boards: Board[]
currentBoardId: string | null
// Actions
setBoards: (boards: Board[]) => void
setCurrentBoard: (id: string) => void
addColumn: (boardId: string, title: string) => void
addCard: (columnId: string, card: Card) => void
moveCard: (activeId: string, overId: string) => void
updateCard: (cardId: string, data: Partial<Card>) => void
deleteCard: (cardId: string) => void
}
export const useBoardStore = create<BoardState>((set, get) => ({
boards: [],
currentBoardId: null,
setBoards: (boards) => set({ boards }),
setCurrentBoard: (id) => set({ currentBoardId: id }),
addColumn: (boardId, title) => set(state => ({
boards: state.boards.map(b =>
b.id === boardId
? { ...b, columns: [...b.columns, { id: crypto.randomUUID(), title, cards: [] }] }
: b
),
})),
addCard: (columnId, card) => set(state => ({
boards: state.boards.map(board => ({
...board,
columns: board.columns.map(col =>
col.id === columnId
? { ...col, cards: [...col.cards, card] }
: col
),
})),
})),
moveCard: (activeId, overId) => {
const state = get()
// 查找源卡片和目标位置
for (const board of state.boards) {
for (const col of board.columns) {
const sourceIdx = col.cards.findIndex(c => c.id === activeId)
if (sourceIdx === -1) continue
// 查找目标位置(同列或他列)
for (const targetCol of board.columns) {
const targetIdx = targetCol.cards.findIndex(c => c.id === overId)
if (targetIdx === -1) continue
// 源列 ≠ 目标列 → 跨列移动
if (col.id !== targetCol.id) {
const [moved] = col.cards.splice(sourceIdx, 1)
targetCol.cards.splice(targetIdx, 0, moved)
} else {
col.cards = arrayMove(col.cards, sourceIdx, targetIdx)
}
return set({ boards: [...state.boards] })
}
}
}
},
updateCard: (cardId, data) => set(state => ({
boards: state.boards.map(board => ({
...board,
columns: board.columns.map(col => ({
...col,
cards: col.cards.map(c => c.id === cardId ? { ...c, ...data } : c),
})),
})),
})),
deleteCard: (cardId) => set(state => ({
boards: state.boards.map(board => ({
...board,
columns: board.columns.map(col => ({
...col,
cards: col.cards.filter(c => c.id !== cardId),
})),
})),
})),
}))API 集成
// api/client.ts
import axios from 'axios'
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
})
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})// api/boards.ts
import { api } from './client'
export const boardApi = {
getBoards: () => api.get<Board[]>('/boards').then(r => r.data),
getBoard: (id: string) => api.get<Board>(`/boards/${id}`).then(r => r.data),
createBoard: (title: string) => api.post<Board>('/boards', { title }),
updateBoard: (id: string, data: Partial<Board>) => api.put(`/boards/${id}`, data),
addCard: (columnId: string, card: Partial<Card>) =>
api.post(`/columns/${columnId}/cards`, card),
updateCard: (cardId: string, data: Partial<Card>) =>
api.put(`/cards/${cardId}`, data),
deleteCard: (cardId: string) => api.delete(`/cards/${cardId}`),
}React Query 集成
// hooks/useBoard.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { boardApi } from '../api/boards'
export function useBoards() {
return useQuery({
queryKey: ['boards'],
queryFn: boardApi.getBoards,
})
}
export function useBoard(id: string) {
return useQuery({
queryKey: ['boards', id],
queryFn: () => boardApi.getBoard(id),
enabled: !!id,
})
}
export function useAddCard() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ columnId, card }: { columnId: string; card: Partial<Card> }) =>
boardApi.addCard(columnId, card),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['boards'] })
},
})
}
export function useUpdateCard() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ cardId, data }: { cardId: string; data: Partial<Card> }) =>
boardApi.updateCard(cardId, data),
// 乐观更新
onMutate: async ({ cardId, data }) => {
await queryClient.cancelQueries({ queryKey: ['boards'] })
const previous = queryClient.getQueryData(['boards'])
queryClient.setQueryData(['boards'], (old: Board[]) =>
updateCardInBoards(old, cardId, data)
)
return { previous }
},
onError: (err, variables, context) => {
if (context?.previous) {
queryClient.setQueryData(['boards'], context.previous)
}
},
})
}搜索与过滤
// hooks/useSearch.ts
function useSearch(cards: Card[]) {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const filteredCards = useMemo(() => {
if (!debouncedQuery) return cards
const q = debouncedQuery.toLowerCase()
return cards.filter(c =>
c.title.toLowerCase().includes(q) ||
c.description.toLowerCase().includes(q)
)
}, [cards, debouncedQuery])
return { query, setQuery, filteredCards }
}小结
- Zustand 管理客户端状态(看板数据、拖拽、UI 状态)。
- React Query 管理服务端状态(API CRUD、缓存、乐观更新)。
- Zustand store 纯函数更新,可测试。
- React Query
invalidateQueries刷新列表,optimisticUpdate即时反馈。 - 搜索防抖(
useDebounce)+ 过滤(useMemo)。
上一节:项目初始化与 UI 组件 下一节:测试与部署
