如何在MobX Store中通过Apollo useQuery获取GraphQL数据?
Hey there, I totally get where you're coming from—mixing Apollo's hook-based data fetching with MobX stores can feel like trying to fit a square peg into a round hole, especially when you need fine-grained control over your graph-based state. Let’s break down practical, clean solutions that keep GraphQL in the picture while giving you the MobX flexibility you want.
First, let’s address why your initial ideas hit roadblocks:
- 方案1: Using React Hooks directly in a MobX store constructor/methods violates React’s rules of Hooks (Hooks can only run in component bodies or custom Hooks). This will throw errors and is not sustainable.
- 方案2: Passing data from components to stores via
useEffectcreates a messy, roundabout flow that puts too much responsibility on components and breaks clean separation of concerns.
Solution 1: Use Apollo Client’s Non-Hook API in MobX Stores
Apollo Client isn’t limited to Hooks—it has vanilla JavaScript methods like client.query() and client.mutate() that work anywhere, including MobX stores. This lets your store take full ownership of data fetching, state updates, and loading/error handling.
Example Implementation:
import { makeAutoObservable } from "mobx"; class ProjectStore { isLoading = false; project = null; tasks = {}; error = null; #apolloClient; // Private property to hold Apollo Client instance constructor(projectId, apolloClient) { makeAutoObservable(this); this.#apolloClient = apolloClient; this.fetchProject(projectId); // Initialize data on store creation } async fetchProject(projectId) { this.isLoading = true; this.error = null; try { const { data } = await this.#apolloClient.query({ query: PROJECT_AND_ROOT_TASK_QUERY, variables: { projectId }, }); this.project = data?.project; } catch (err) { this.error = err.message; console.error("Failed to fetch project:", err); } finally { this.isLoading = false; } } async fetchTasks(taskIds) { this.isLoading = true; this.error = null; try { const { data } = await this.#apolloClient.query({ query: TASKS_QUERY, variables: { taskIds }, }); // Transform data into your preferred structure (e.g., map by ID) data?.tasks.forEach(task => { this.tasks[task.id] = task; }); } catch (err) { this.error = err.message; console.error("Failed to fetch tasks:", err); } finally { this.isLoading = false; } } } // Usage in a component: // import { useApolloClient } from '@apollo/client'; // import { useObserver } from 'mobx-react-lite'; // function ProjectComponent({ projectId }) { // const client = useApolloClient(); // const projectStore = useMemo(() => new ProjectStore(projectId, client), [projectId, client]); // return useObserver(() => ( // <div> // {projectStore.isLoading && <div>Loading...</div>} // {projectStore.error && <div>Error: {projectStore.error}</div>} // {projectStore.project && <h1>{projectStore.project.name}</h1>} // <button onClick={() => projectStore.fetchTasks(['task-1', 'task-2'])}>Load Tasks</button> // </div> // )); // }
Why This Works:
- Your store becomes the single source of truth for both state and data fetching logic.
- Components only need to initialize the store and render its state (via
useObserverfor reactivity). - You retain all GraphQL benefits (type safety, query validation) while getting MobX’s flexible state manipulation.
Solution 2: Custom Hooks to Bridge Apollo and MobX
If you want to keep using Apollo’s hook features (like automatic cache updates when data changes elsewhere), create a custom Hook that syncs Apollo’s query results to your MobX store. This keeps data fetching in the React component tree (where Hooks belong) while letting MobX handle state organization.
Example Implementation:
// Custom Hook for syncing Apollo data to MobX import { useQuery, useApolloClient } from "@apollo/client"; import { useEffect } from "react"; import { makeAutoObservable } from "mobx"; class ProjectStore { isLoading = false; project = null; tasks = {}; error = null; constructor() { makeAutoObservable(this); } setProject(project) { this.project = project; } setLoading(isLoading) { this.isLoading = isLoading; } setError(error) { this.error = error; } addTasks(tasks) { tasks.forEach(task => { this.tasks[task.id] = task; }); } } // Custom Hook to connect Apollo queries to the store function useProjectStoreSync(projectId, store) { const { loading, data, error } = useQuery(PROJECT_AND_ROOT_TASK_QUERY, { variables: { projectId }, }); useEffect(() => { store.setLoading(loading); if (error) store.setError(error.message); if (data?.project) store.setProject(data.project); }, [loading, data, error, store]); } // Usage in a component: // import { useObserver } from 'mobx-react-lite'; // function ProjectComponent({ projectId }) { // const projectStore = useMemo(() => new ProjectStore(), []); // const client = useApolloClient(); // useProjectStoreSync(projectId, projectStore); // const handleLoadTasks = async (taskIds) => { // projectStore.setLoading(true); // try { // const { data } = await client.query({ query: TASKS_QUERY, variables: { taskIds } }); // projectStore.addTasks(data.tasks); // } catch (err) { // projectStore.setError(err.message); // } finally { // projectStore.setLoading(false); // } // }; // return useObserver(() => ( // <div> // {projectStore.isLoading && <div>Loading...</div>} // {projectStore.error && <div>Error: {projectStore.error}</div>} // {projectStore.project && <h1>{projectStore.project.name}</h1>} // <button onClick={() => handleLoadTasks(['task-1', 'task-2'])}>Load Tasks</button> // </div> // )); // }
Why This Works:
- You get Apollo’s automatic cache synchronization (e.g., if a mutation updates project data,
useQuerywill refetch and sync to the store). - The store still handles state organization and mutations, keeping components focused on rendering.
Key Best Practices
- Inject Apollo Client: Always pass the global Apollo Client instance to your store (don’t create a new one inside the store) to maintain shared cache consistency.
- Centralize Data Transformation: Handle graph-to-state conversions (like array-to-map) inside the store, not components.
- Track Loading/Error States: Keep loading and error states in the store so components can react to them consistently.
I’ve been in your shoes before—switching from Apollo’s opinionated layer to MobX’s flexibility can feel overwhelming at first, but once you nail this bridge, it’s a powerful combo for complex state workflows.
内容的提问来源于stack exchange,提问作者Scott




