5

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;
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
Giulia Lage
  • 485
  • 1
  • 5
  • 17
  • fetch state from a database? ‍♂️ – smac89 Aug 23 '21 at 01:27
  • 1
    I think you are missing `useSelector`, https://react-redux.js.org/api/hooks ? – Horst Aug 23 '21 at 01:33
  • 1
    Dude, I'm playing with github graphQL api, and I wanna make the user insert the personal token, and it has to be available to ApolloClient after that. I dont think create a data base just to save this is a option. – Giulia Lage Aug 23 '21 at 01:34
  • You mentioned Apollo, which [client could be managed within a custom provider component, which would have access to anything React related](https://stackoverflow.com/a/68546204/1218980). – Emile Bergeron Aug 23 '21 at 01:36
  • @EmileBergeron yes, but the token that goes under authorization is entered by the user via the form, and I need to pass that back to the client – Giulia Lage Aug 23 '21 at 01:41
  • 1
    The form component would [update a token context](https://stackoverflow.com/a/51573816/1218980), and the custom apollo provider (with a middleware link) would consume that context to set the proper headers. – Emile Bergeron Aug 23 '21 at 01:43
  • @Host makes sense.. I'm actually going to take a look at how UseSelector REALLY works, haha thanks – Giulia Lage Aug 23 '21 at 01:44
  • You do not really need Redux for this, the React context would be enough. Apollo manages so much stuff (client cache, refetching, state, etc) that we ended up removing Redux from our toolset. – Emile Bergeron Aug 23 '21 at 01:46
  • 1
    @EmileBergeron I really don't want to use redux just for that, I'll update my question so you can see how I'm doing, I think I get the point of middleware – Giulia Lage Aug 23 '21 at 01:48

3 Answers3

3

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.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
3

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.

DrunkOldDog
  • 718
  • 5
  • 12
-1

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();
}