Search code examples
node.jstypescriptnext.jsazure-pipelinesapollo-client

How to set environment variable for ApolloClient that should be server side rendered for CI/CD


I have the following apolloClient

/**
 * Initializes an ApolloClient instance. For configuration values refer to the following page
 * https://www.apollographql.com/docs/react/api/core/ApolloClient/#the-apolloclient-constructor
 *
 * @returns ApolloClient
 */
const createApolloClient = (authToken: string | null) => {
  const httpLinkHeaders = {
    ...(authToken && { Authorization: `Bearer ${authToken}` })
  };
  console.log('CREATING APOLLO CLIENT WITH HEADERS >>>>', httpLinkHeaders);

  console.log(
    'Graph Env Variable URL >>>>>',
    publicRuntimeConfig.GRAPHQL_URL
  );

  

  return new ApolloClient({
    name: 'client',
    ssrMode: typeof window === 'undefined',
    link: createHttpLink({
      uri: publicRuntimeConfig.GRAPHQL_URL,
      credentials: 'same-origin',
      headers: httpLinkHeaders
    }),
    cache: new InMemoryCache()
  });
};

/**
 * Initializes the apollo client with data restored from the cache for pages that fetch data
 * using either getStaticProps or getServerSideProps methods
 *
 * @param accessToken
 * @param initialState
 *
 * @returns ApolloClient
 */
export const initializeApollo = (
  accessToken: string,
  initialState = null,
  forceNewInstane = false
): ApolloClient<NormalizedCacheObject> => {
  // Regenerate client?
  if (forceNewInstane) {
    apolloClient = null;
  }

  const _apolloClient = apolloClient || createApolloClient(accessToken);

  // for pages that have Next.js data fetching methods that use Apollo Client,
  // the initial state gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract();

    // Restore the cache using the data passed from
    // getStaticProps/getServerSideProps combined with the existing cached data
    _apolloClient.cache.restore({ ...existingCache, ...initialState });
  }

  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient;

  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;
  return _apolloClient;
};

/**
 * Hook to initialize the apollo client only when state has changed.
 *
 * @param initialState
 *
 * @returns
 */
export const useApollo = (
  initialState: any
): ApolloClient<NormalizedCacheObject> => {
  return useMemo(() => {
    if (process.browser) {
      const accessToken = extractCookie(document.cookie, 'access_token');
      return initializeApollo(accessToken, initialState);
    }

    // document is not present and we can't retrieve the token but ApolloProvider requires to pass a client
    return initializeApollo(null, initialState);
  }, [initialState]);
};

That is initialized in the _app.tsx file like so

const updateApolloWithNewToken = useCallback(
    (accessToken: string) => {
      // Initialize apollo client with new access token
      setClient(
        initializeApollo(accessToken, pageProps.initialApolloState, true)
      );
      // Show the dashboard
      router.replace('/dashboard');
    },
    [router]
  );

With the following Next Config

const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');

module.exports = (phase, { defaultConfig }) => {
  console.log('Phase >>>>', phase);
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    console.log('RETURNING DEVELOPMENT CONFIGURATION...');

    return {
      publicRuntimeConfig: {
        
        GRAPHQL_URL: process.env.GRAPHQL_URL
      }
    };
  }

  console.log('RETURNING PRODUCTION CONFIGURATION...');

  console.log('GRAPHQL_URL', process.env.GRAPHQL_URL);

  return {
    publicRuntimeConfig: {
      
      GRAPHQL_URL: process.env.GRAPHQL_URL
    }
  };
};

This is my _app.tsx

function MyApp({ Component, pageProps }: AppProps) {
  // Grab the apollo client instance with state hydration from the pageProps
  const router = useRouter();
  const apolloClient = useApollo(pageProps.initialApolloState);
  const [client, setClient] = useState(apolloClient);

  
  React.useEffect(() => {
    // Refresh token on browser load or page regresh
    handleAcquireTokenSilent();

    // We also set up an interval of 5 mins to check if token needs to be refreshed
    const refreshTokenInterval = setInterval(() => {
      handleAcquireTokenSilent();
    }, REFRESH_TOKEN_SILENTLY_INTERVAL);

    return () => {
      clearInterval(refreshTokenInterval);
    };
  }, []);

  
  const updateApolloWithNewToken = useCallback(
    (accessToken: string) => {
      // Initialize apollo client with new access token
      setClient(
        initializeApollo(accessToken, pageProps.initialApolloState, true)
      );
      // Show the dashboard
      router.replace('/dashboard');
    },
    [router]
  );

  return pageProps.isAuthenticated || pageProps.shouldPageHandleUnAuthorize ? (
    <ApolloProvider client={client}>
      <ThemeProvider theme={theme}>
        <SCThemeProvider theme={theme}>
          <StylesProvider injectFirst>
            <Component
              {...pageProps}
              updateAuthToken={updateApolloWithNewToken}
            />
          </StylesProvider>
        </SCThemeProvider>
      </ThemeProvider>
    </ApolloProvider>
  ) : (
    <UnAuthorize />
  );
}

/**
 * Fetches the Me query so that pages/components can grab it from the cache in the
 * client.
 *
 * Note: This disables the ability to perform automatic static optimization, causing
 * every page in the app to be server-side rendered.
 */
MyApp.getInitialProps = async (appContext: AppContext) => {
  const appProps = await App.getInitialProps(appContext);
  const req = appContext.ctx.req;

  // Execute Me query and handle all scenarios including, unauthorized response, caching data so pages can grab
  // data from the cache
  return await handleMeQuery(appProps, req);
};

export default MyApp;

My problem is that when I run yarn build I get a server error generating 500 page. I know it is because when creating the Apollo Client it doesn't have access to the publicRuntimeConfig, it seems like Next is trying to build the ApolloClient when I run yarn build, I am using getInitialProps and getServerSideProps so I just want to access all the env variables on runtime not on build, because we want one build for our pipeline.

All other env variables in my app that are using publicRuntimeConfig are working, I tested by removing the env vars on build and adding them back on start and the app functioned as normal.

If there isn't a way to do this with apollo client, what would be reccomended to be able to pass different uri's as env variables on start of the app not on build for Apollo Client or alternate solution?

Thanks for any help ahead of time

So I don't know if I have explained the problem well enough.

Basically the graphql URL is different depending on the environment it is in in development, staging, and production however they are supposed to use the same build so I need to access the GRAPHQL_URL via a runtime variable, but in my current setup it is just undefined.


Solution

  • First, there is redundant code and inefficiency. Primarily the hooks updateApolloWithNewToken and useApollo but also in the way you inject the accessToken.

    I would recommend throwing the ApolloClient in it's own separate file and using it's configurable links for your use-case.

    However, the real problem most likely lies in the initialization of the client and your attempt at memoizing the client.

    First, try updating the following,

        link: createHttpLink({
          uri: publicRuntimeConfig.GRAPHQL_URL,
          credentials: 'same-origin',
          headers: httpLinkHeaders
        }),
    

    to

      link: createHttpLink({
        // ...your other stuff
        uri: () => getConfig().publicRuntimeConfig.GRAPHQL_URL
      })
    

    If that doesn't work right away, I would recommend trying with the outmost minimal example.

    Create a client you export, a provider and a component that uses it (without using state, useEffect or anything else). We can go from there if that still doesn't work.