Accessing Apollo client in an Astro + React project without ApolloProvider

Hello,

I’m using Astro.build with React and Apollo. I’m trying to find a way to access the client through the component tree. My top-level layout component is an .astro component and I tried something like this:

<CartWrapper client:load>
    <Cart client:load />
    <PromoBar client:load />
    <Header />
    <slot />
    <Footer />
</CartWrapper>

…with my <CartWrapper /> component:

// ^^ Apollo client code

const CartWrapper = ({ children }) => {
    return <ApolloProvider client={client}>{children}</ApolloProvider>
}
export default CartWrapper

Because Astro loads each dynamic component asynchronously as an island, the “context” or client of the Apollo Provider is not accessible down the component tree. If I try I get this error:

Could not find “client” in the context or passed in as an option. Wrap the root component in an <ApolloProvider> , or pass an ApolloClient instance in via options.

I’ve tried to find examples of passing an ApolloClient instance in via options but there doesn’t seem to be any documentation about this.

My questions are these:

  1. Is there another way to use a single client throughout my component tree outside of the provider? For example, could I load this into React context?
  2. What does ‘pass an ApolloClient instance in via options’ mean and how does this work?
  3. Any other alternatives for sharing client + client context across an app, specifically with Astro.build?

Thanks in advance!

Quick follow up on this. If I console.log my client instance it is just an object but I can’t find any docs on how to use the client without the Provider.

Is it possible?

Another follow up…tried the useApolloClient hook but again, this does not work without the parent components being wrapped in the provider.

I think you need pass the client to the hook like this:

// customer useQuery  for Astro
import { apolloClient } from 'your/apollclient/path';
import { atom } from 'nanostores';
export const apolloClientStore = atom(apolloClient);

export const useAstroQuery=(QUERY_STRING,options)=>{
      const client = useStore(apolloClientStore)
     return useQuery(QUERY_STRING,{
         ...options,
         client
    })
}

2 Likes

Nice implementation @Zhaorong_Sun. I ended up using the top-level await in Astro for this project but this is great to know for a future project.

I’ll leave this open in case anyone else has some examples/ideas they can share.

1 Like

sure, this is just a quick implementation, not fully tested. hope Astro will provide some official plugin.

Here is my little workaround:

file path: src/helpers/apollo.ts

import { useMutation, ApolloClient, InMemoryCache, useQuery, useSubscription, useLazyQuery } from '@apollo/client'

import { link } from 'graphql-links/link'

export const client = new ApolloClient({
  cache: new InMemoryCache(),
  link
})

export const useAstroMutation: typeof useMutation = (query, options) => {
  return useMutation(query, { ...options, client })
}

export const useAstroQuery: typeof useQuery = (query, options) => {
  return useQuery(query, { ...options, client })
}

export const useLazyAstroQuery: typeof useLazyQuery = (query, options) => {
  return useLazyQuery(query, { ...options, client })
}

export const useAstroSubscription: typeof useSubscription = (query, options) => {
  return useSubscription(query, { ...options, client })
}

and you use those custom hooks anywhere you need it.

1 Like

2023 This is how I got it to work with ApolloProvider:

Simply create your React entry point as a component:

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import App from '../sections/App';

const client = new ApolloClient({
  uri: 'https://flyby-router-demo.herokuapp.com/',
  cache: new InMemoryCache(),
});

export default function ReactApp() {
  return (
    <div>
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>,
    </div>
  );
}

Then create your App component which is encapsulated above:

import { useQuery, gql } from '@apollo/client';

const GET_LOCATIONS = gql`
  query GetLocations {
    locations {
      id
      name
      description
      photo
    }
  }
`;

function DisplayLocations() {
  const { loading, error, data } = useQuery(GET_LOCATIONS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error : {error.message}</p>;

  return data.locations.map(({ id, name, description, photo }) => (
    <div key={id}>
      <h3>{name}</h3>
      <img width="400" height="250" alt="location-reference" src={`${photo}`} />
      <br />
      <b>About this location:</b>
      <p>{description}</p>
      <br />
    </div>
  ));
}

export default function App() {
  return (
    <div>
      <h2>My first Apollo app 🚀</h2>
      <DisplayLocations />
    </div>
  );
}

Use the ReactApp entry point in an Astro page like this:

---
import Layout from '~/layouts/Default.astro';
import ReactApp from '~/components/common/ReactApp';
---

<Layout>
  <div class="p-8 md:p-12 max-w-4xl mx-auto">
    <h1>AI Assistant</h1>
    <div id="root"></div>
    <ReactApp client:load />
  </div>
</Layout>