Giulia Lage
Giulia Lage

Reputation: 535

How to use the global data from React inside Apollo client's initialization?

When it comes to state centralization I know how to use the context api and Redux. But to recover that state we always have to be inside a react component.

What is the best strategy to access a global state/variable inside a common function that is not inside a react component?

In the environment variables is not an option because this value is changed after the application runs. And I didn't want to put in cookies or local storage for security reasons.

Index.ts

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import apolloClient from './services/apollo';

import { PersonalTokenProvider } from './providers/personal-token';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <PersonalTokenProvider>
      <ApolloProvider client={apolloClient}>
        <App />
      </ApolloProvider>
    </PersonalTokenProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

PresonalToken context provider

import React, { useState } from 'react';

interface ProviderProps {
  children: JSX.Element[] | JSX.Element;
}

export const PersonalTokenContext = React.createContext({});

export const PersonalTokenProvider: React.FC<ProviderProps> = (
  props: ProviderProps,
) => {
  const [token, setToken] = useState<string | null>(null);

  const { children } = props;

  return (
    <PersonalTokenContext.Provider value={{ token, setToken }}>
      {children}
    </PersonalTokenContext.Provider>
  );
};

apollo client config

import { useContext } from 'react';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { PersonalTokenContext } from '../providers/personal-token';

//cant do this
const {token} = useContext(PersonalTokenContext)

const httpLink = new HttpLink({
  uri: 'https://api.github.com/graphql',
  headers: {
    authorization: `Bearer ${token}`,
  },
});

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

export default client;

Upvotes: 5

Views: 2580

Answers (3)

Emile Bergeron
Emile Bergeron

Reputation: 17430

Pure React Apollo client initialization

There are multiple ways to simulate a singleton to manage the Apollo client from within React. Here's one way using useRef to always have the latest token when making GraphQL queries and useMemo to only create the client once.

import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
  ApolloProvider
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

// The name here doesn't really matters.
export default function CustomApolloProvider(props) {
  const { token } = useContext(PersonalTokenContext);
  const tokenRef = useRef();

  // Whenever the token changes, the component re-renders, thus updating the ref.
  tokenRef.current = token;

  // Ensure that the client is only created once.
  const client = useMemo(() => {
    const authLink = setContext((_, { headers }) => ({
      headers: {
        ...headers,
        authorization: tokenRef.current ? `Bearer ${tokenRef.current}` : '',
      }
    }));

    const httpLink = createHttpLink({
      uri: 'https://api.github.com/graphql',
    });

    return new ApolloClient({
      link: authLink.concat(httpLink),
      cache: new InMemoryCache(),
    });
  }, [])

  return <ApolloProvider client={client} {...props} />;
}

Then in the app:

    <PersonalTokenProvider>
      <CustomApolloProvider>
        <App />
      </CustomApolloProvider>
    </PersonalTokenProvider>

Pros:

  • Totally inside of React, which means it could use other hooks and data that changes from different places, like the locale code from the translation lib, etc.
  • One client per mounted application, which means, if the application needs to be unmounted, this solution would ensure proper cleanup.
  • Easy to add unit/integration tests

Cons:

  • A little more complex to put in place.
  • If not properly setup, multiple Apollo clients could end up being created, losing the previous cache, etc.

Using localStorage

The Apollo documentation suggests using the local storage to manage the authentication token.

import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const httpLink = createHttpLink({
  uri: '/graphql',
});

const authLink = setContext((_, { headers }) => {
  // get the authentication token from local storage if it exists
  const token = localStorage.getItem('token');
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

Pros:

  • Simple to add to your existing implementation
  • There's ever only one client created for the entire lifetime of the app
  • The local storage is a good place to store global data across tabs, refresh, etc.

Cons:

  • Lives outside of React, so the app wouldn't re-render when the token changes, etc.
  • Could be harder/complex to unit test.

Using module scoped variable

Using a simple variable at the root of the module would be enough, you wouldn't even need the token context anymore.

import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
  makeVar
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

// module scoped var for the token:
let token;

// custom module setter:
export const setToken = (newToken) => token = newToken;

const httpLink = createHttpLink({
  uri: '/graphql',
});

// Apollo link middleware gets called for every query.
const authLink = setContext((_, { headers }) => ({
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
));

export const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

Pros:

  • Simple to add to your existing implementation
  • There's ever only one client created for the entire lifetime of the app

Cons:

  • Lives outside of React, so the app wouldn't re-render when the token changes, etc.
  • Could be harder/complex to unit test
  • Lost when the user refreshes the page, or closes the app.

Reactive vars to manage the token

juanireyes suggested Apollo Reactive variables, but they're meant for a particular use-case, which is totally unnecessary to manage the token globally like we want here. It is similar to the module scope variable suggestion above, but with extra steps.

Upvotes: 3

DrunkOldDog
DrunkOldDog

Reputation: 748

If you are trying to use Apollo I would personally encourage you to use the updated library: @apollo/client. Then you can use Reactive Variables to access the state from multiple places. Then you can try in your provider file something like this to access the token variable:

import React, { useState } from 'react';
import { makeVar } from '@apollo/client';

interface ProviderProps {
  children: JSX.Element[] | JSX.Element;
}

export const tokenVar = makeVar<string | null>(null);

export const PersonalTokenContext = React.createContext({});

export const PersonalTokenProvider: React.FC<ProviderProps> = (
  props: ProviderProps,
) => {
  const [token, setToken] = useState<string | null>(null);

  useEffect(() => {
   tokenVar(token)
  }, [token]);

  const { children } = props;

  return (
    <PersonalTokenContext.Provider value={{ token, setToken }}>
      {children}
    </PersonalTokenContext.Provider>
  );
};

And finally you can access the token value from everywhere calling tokenVar() or using the useReactiveVar hook.

Upvotes: 3

Arthur Strini
Arthur Strini

Reputation: 34

You can access the content of the Redux store from outside of a component. I know two ways of doing so:

getState

Import the store from the file where you declare it, and access the whole state with the getState method:

import { store } from '../myReduxConfig.js';

const myFunc = () => {
  const reduxData = store.getState();
}

subscribe

If you need the function to run again on redux store changes, import the store from the file where you declare it, and subscribe your function to it:

import { store } from '../myReduxConfig.js';

store.subscribe(myFunc);

const myFunc = () => {
  const reduxData = store.getState();
}

Upvotes: -1

Related Questions