You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Redux Toolkit结合Apollo创建客户端,通过client方法执行GraphQL操作?

用Redux Toolkit结合GraphQL客户端(client.mutate/client.query)实现查询与变更

核心思路

放弃React的useQuery/useMutation Hooks,改用Redux Toolkit的createAsyncThunk封装GraphQL客户端的client.query和client.mutate方法,将异步操作的加载、成功、失败状态统一交由Redux Store管理,同时针对登录场景单独处理令牌获取逻辑。


步骤1:基础配置

先安装必要依赖:

npm install @reduxjs/toolkit @apollo/client graphql

初始化GraphQL客户端

以Apollo Client为例,创建可全局访问的客户端实例:

// src/graphql/client.js
import { ApolloClient, InMemoryCache } from '@apollo/client';

export const client = new ApolloClient({
  uri: '你的GraphQL接口地址',
  cache: new InMemoryCache(),
});

配置Redux Store

将GraphQL客户端通过extraArgument传入Thunk,方便在异步操作中调用:

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import { client } from '../graphql/client';
import authReducer from '../features/auth/authSlice';
import dataReducer from '../features/data/dataSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer,
    data: dataReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      thunk: {
        extraArgument: client,
      },
    }),
});

步骤2:登录Mutation(获取令牌)

创建Auth Slice,封装登录异步Thunk,仅在用户触发登录时调用client.mutate获取令牌:

// src/features/auth/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { gql } from '@apollo/client';

// 定义登录GraphQL Mutation
const LOGIN_MUTATION = gql`
  mutation Login($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
      user {
        id
        name
      }
    }
  }
`;

// 封装登录异步操作
export const login = createAsyncThunk(
  'auth/login',
  async ({ email, password }, { extra: client, rejectWithValue }) => {
    try {
      const { data } = await client.mutate({
        mutation: LOGIN_MUTATION,
        variables: { email, password },
      });
      // 持久化令牌,避免刷新丢失
      localStorage.setItem('authToken', data.login.token);
      // 更新客户端授权头,后续请求自动携带令牌
      client.setHeaders({
        authorization: `Bearer ${data.login.token}`,
      });
      return data.login;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    token: localStorage.getItem('authToken') || null,
    loading: false,
    error: null,
  },
  reducers: {
    logout: (state) => {
      state.user = null;
      state.token = null;
      localStorage.removeItem('authToken');
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(login.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(login.fulfilled, (state, action) => {
        state.loading = false;
        state.user = action.payload.user;
        state.token = action.payload.token;
      })
      .addCase(login.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { logout } = authSlice.actions;
export default authSlice.reducer;

登录组件调用示例

// src/components/Login.js
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { login } from '../features/auth/authSlice';

export default function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const dispatch = useDispatch();
  const { loading, error } = useSelector((state) => state.auth);

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch(login({ email, password }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="邮箱"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="密码"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? '登录中...' : '登录'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </form>
  );
}

步骤3:通用GraphQL查询与变更封装

针对其他业务场景,同样用createAsyncThunk封装client.query和client.mutate:

示例:用户列表查询与创建

// src/features/data/dataSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { gql } from '@apollo/client';

// 查询用户列表的GraphQL Query
const GET_USERS_QUERY = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

// 封装查询异步操作
export const fetchUsers = createAsyncThunk(
  'data/fetchUsers',
  async (_, { extra: client, rejectWithValue }) => {
    try {
      const { data } = await client.query({
        query: GET_USERS_QUERY,
        fetchPolicy: 'network-only', // 根据需求选择缓存策略
      });
      return data.users;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// 创建用户的GraphQL Mutation
const CREATE_USER_MUTATION = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

// 封装创建用户异步操作
export const createUser = createAsyncThunk(
  'data/createUser',
  async (userData, { extra: client, rejectWithValue }) => {
    try {
      const { data } = await client.mutate({
        mutation: CREATE_USER_MUTATION,
        variables: userData,
      });
      return data.createUser;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const dataSlice = createSlice({
  name: 'data',
  initialState: {
    users: [],
    loading: false,
    error: null,
  },
  extraReducers: (builder) => {
    builder
      // 处理用户列表查询
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      })
      // 处理创建用户
      .addCase(createUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(createUser.fulfilled, (state, action) => {
        state.loading = false;
        state.users.push(action.payload);
      })
      .addCase(createUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export default dataSlice.reducer;

用户列表组件调用示例

// src/components/UserList.js
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers, createUser } from '../features/data/dataSlice';

export default function UserList() {
  const dispatch = useDispatch();
  const { users, loading, error } = useSelector((state) => state.data);

  useEffect(() => {
    dispatch(fetchUsers());
  }, [dispatch]);

  const handleAddUser = () => {
    dispatch(createUser({ name: '新用户', email: 'new@example.com' }));
  };

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了:{error}</div>;

  return (
    <div>
      <button onClick={handleAddUser}>添加用户</button>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name} - {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

关键注意点

  • 令牌持久化:登录后将令牌存入localStorage,客户端初始化时自动读取并设置授权头,避免刷新后丢失登录状态。
  • 缓存策略:根据业务需求选择client.query的fetchPolicy,比如cache-first优先使用缓存,network-only强制请求接口。
  • 错误处理:在Thunk中用rejectWithValue传递错误信息到Slice,统一管理错误状态。

内容的提问来源于stack exchange,提问作者Busenur Adıbelli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 02:01:00