# 异步逻辑与 API 请求

URL: https://caijiao.org/zustand/guide/async-actions
Source: docs/zustand/guide/async-actions.md
Description: 了解如何在 Zustand 中处理异步逻辑与 API 请求，包括基本异步 Actions、状态管理最佳实践、高级异步模式以及与其他库的结合使用。

在现代前端应用中，异步操作（如 API 请求）是必不可少的。[Zustand](../) 提供了简洁而强大的方式来处理异步逻辑，让你能够轻松管理异步状态。

## 基本异步 Actions

在 Zustand 中，你可以直接在 Store 中定义异步 Actions。这些 Actions 通常是返回 Promise 的函数。

### 定义异步 Action

```javascript
import { create } from 'zustand'

const useUserStore = create((set) => ({
  user: null,
  isLoading: false,
  error: null,
  
  // 异步 Action：获取用户信息
  fetchUser: async (userId) => {
    try {
      set({ isLoading: true, error: null })
      const response = await fetch(`https://api.example.com/users/${userId}`)
      if (!response.ok) {
        throw new Error('Failed to fetch user')
      }
      const user = await response.json()
      set({ user, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
}))
```

### 使用异步 Action

```jsx
function UserProfile({ userId }) {
  const { user, isLoading, error, fetchUser } = useUserStore()
  
  // 组件挂载时获取用户信息
  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>
  )
}
```

## 异步状态管理最佳实践

### 1. 总是管理加载状态

在异步操作期间，向用户显示加载指示器是一种良好的用户体验。

```javascript
const useStore = create((set) => ({
  data: null,
  isLoading: false, // 加载状态
  error: null,
  
  fetchData: async () => {
    set({ isLoading: true, error: null }) // 开始加载
    try {
      const data = await fetchDataFromAPI()
      set({ data, isLoading: false }) // 加载完成
    } catch (error) {
      set({ error: error.message, isLoading: false }) // 加载失败
    }
  },
}))
```

### 2. 妥善处理错误

确保捕获异步操作中的错误，并向用户提供有意义的反馈。

```javascript
const useStore = create((set) => ({
  data: null,
  isLoading: false,
  error: null, // 错误状态
  
  fetchData: async () => {
    set({ isLoading: true, error: null })
    try {
      const data = await fetchDataFromAPI()
      set({ data, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false }) // 设置错误信息
    }
  },
}))
```

### 3. 使用 get 获取当前状态

在异步 Action 中，你可以使用 `get` 参数获取当前状态。

```javascript
const useStore = create((set, get) => ({
  todos: [],
  
  // 异步 Action：添加任务
  addTodo: async (text) => {
    try {
      const newTodo = await createTodoAPI({ text })
      set((state) => ({
        todos: [...state.todos, newTodo]
      }))
    } catch (error) {
      console.error('Failed to add todo:', error)
    }
  },
  
  // 异步 Action：更新任务状态
  toggleTodo: async (id) => {
    try {
      // 使用 get 获取当前任务
      const todo = get().todos.find((todo) => todo.id === id)
      if (!todo) return
      
      const updatedTodo = await updateTodoAPI(id, { 
        completed: !todo.completed 
      })
      
      set((state) => ({
        todos: state.todos.map((todo) => 
          todo.id === id ? updatedTodo : todo
        )
      }))
    } catch (error) {
      console.error('Failed to toggle todo:', error)
    }
  },
}))
```

## 高级异步模式

### 1. 并行请求

你可以使用 `Promise.all` 来处理并行的异步请求。

```javascript
const useStore = create((set) => ({
  users: [],
  posts: [],
  isLoading: false,
  error: null,
  
  // 并行获取用户和帖子
  fetchData: async () => {
    set({ isLoading: true, error: null })
    try {
      const [users, posts] = await Promise.all([
        fetchUsersAPI(),
        fetchPostsAPI()
      ])
      set({ users, posts, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
}))
```

### 2. 串行请求

对于需要按顺序执行的异步请求，你可以使用 `await` 链式调用。

```javascript
const useStore = create((set) => ({
  user: null,
  userPosts: [],
  isLoading: false,
  error: null,
  
  // 先获取用户，再获取用户的帖子
  fetchUserWithPosts: async (userId) => {
    set({ isLoading: true, error: null })
    try {
      const user = await fetchUserAPI(userId)
      const userPosts = await fetchUserPostsAPI(userId)
      set({ user, userPosts, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
}))
```

### 3. 取消请求

对于可能被中断的异步请求，你可以使用 AbortController 来取消请求。

```javascript
const useStore = create((set) => {
  let abortController = null
  
  return {
    data: null,
    isLoading: false,
    error: null,
    
    fetchData: async () => {
      // 取消之前的请求
      if (abortController) {
        abortController.abort()
      }
      
      // 创建新的 AbortController
      abortController = new AbortController()
      
      set({ isLoading: true, error: null })
      try {
        const response = await fetch('https://api.example.com/data', {
          signal: abortController.signal // 关联 AbortController
        })
        const data = await response.json()
        set({ data, isLoading: false })
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Request aborted')
          return
        }
        set({ error: error.message, isLoading: false })
      }
    },
    
    // 取消请求的 Action
    cancelFetch: () => {
      if (abortController) {
        abortController.abort()
        set({ isLoading: false })
      }
    },
  }
})
```

## 与其他库结合使用

### 与 Axios 结合

[Axios](/axios/) 是一个流行的 HTTP 客户端库，你可以轻松地将它与 Zustand 结合使用。

```javascript
import axios from 'axios'
import { create } from 'zustand'

const usePostStore = create((set) => ({
  posts: [],
  isLoading: false,
  error: null,
  
  fetchPosts: async () => {
    set({ isLoading: true, error: null })
    try {
      const response = await axios.get('https://api.example.com/posts')
      set({ posts: response.data, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
  
  createPost: async (postData) => {
    set({ isLoading: true, error: null })
    try {
      const response = await axios.post('https://api.example.com/posts', postData)
      set((state) => ({
        posts: [...state.posts, response.data],
        isLoading: false
      }))
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
}))
```

### 与 TanStack Query 结合

TanStack Query（原 React Query）是一个强大的数据获取库，它可以与 Zustand 很好地结合使用。Zustand 管理应用状态，而 TanStack Query 管理服务器状态。

```javascript
// Zustand Store：管理应用状态
const useAppStore = create((set) => ({
  isLoggedIn: false,
  userPreferences: {},
  
  login: (user) => set({ isLoggedIn: true }),
  logout: () => set({ isLoggedIn: false }),
  updatePreferences: (preferences) => set({ userPreferences: preferences }),
}))

// TanStack Query：管理服务器状态
function PostsList() {
  const { data: posts, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('https://api.example.com/posts').then(res => res.json())
  })
  
  // 使用 Zustand 的状态
  const isLoggedIn = useAppStore(state => state.isLoggedIn)
  
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          {isLoggedIn && <button>Edit Post</button>}
        </div>
      ))}
    </div>
  )
}
```

## 最佳实践总结

1. **总是管理加载状态和错误状态**：提供良好的用户体验
2. **使用不可变更新**：确保状态更新的可预测性
3. **避免在组件中处理复杂的异步逻辑**：将异步逻辑封装在 Store 中
4. **考虑使用专业的数据获取库**：如 TanStack Query，处理缓存、重新获取等高级功能
5. **取消不必要的请求**：避免资源浪费和潜在的错误

在接下来的章节中，我们将学习 [中间件：Persist 持久化](middleware-persist.md)，了解如何在 Zustand 中持久化存储状态。
