1
votes

I am using Apollo 2.0 to manage my graphQL API calls and to handle the global state of my react application.

I am trying to create a login screen where a user enters their username and password, this gets sent to my API to authenticate and upon success, I want to then set the global state of isLoggedIn to true.

So far, I am able to set the global state with one mutation which utilises the @client declaration so it is only concerned with local state. I have another mutation which makes the graphQL API call and validates username / password and then returns success / error responses.

I want to be able to set isLoggedIn once the API call mutation has completed or failed.

My client has the following default state and resolvers set like so:

const httpLink = new HttpLink({
  uri: '/graphql',
  credentials: 'same-origin'
});

const cache = new InMemoryCache();

const stateLink = withClientState({
  cache,
  resolvers: {
    Mutation: {
      updateLoggedInStatus: (_, { isLoggedIn }, { cache }) => {
        const data = {
          loggedInStatus: {
            __typename: 'LoggedInStatus',
            isLoggedIn
          },
        };
        cache.writeData({ data });
        return null;
      },
    },
  },
  defaults: {
    loggedInStatus: {
      __typename: 'LoggedInStatus',
      isLoggedIn: false,
    },
  },
});

const link = ApolloLink.from([stateLink, httpLink])

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

export default client

Then in my Login component I have the following mutations and queries which I pass as a HOC with the help of compose:

const UPDATE_LOGGED_IN_STATUS = gql`
  mutation updateLoggedInStatus($isLoggedIn: Boolean) {
    updateLoggedInStatus(isLoggedIn: $isLoggedIn) @client
  }`

const AUTHENTICATE = gql`
  mutation authenticate($username: String!, $password: String!) {
    auth(username: $username, password: $password) {
      username
      sales_channel
      full_name
      roles
    }
  }`

const GET_AUTH_STATUS = gql`
  query {
    loggedInStatus @client {
      isLoggedIn
    }
  }`

export default compose(
  graphql(GET_AUTH_STATUS, {
    props: ({ data: { loading, error, loggedInStatus } }) => {
      if (loading) {
        return { loading };
      }

      if (error) {
        return { error };
      }

      return {
        loading: false,
        loggedInStatus
      };
    },
  }),
  graphql(UPDATE_LOGGED_IN_STATUS, {
    props: ({ mutate }) => ({
      updateLoggedInStatus: isLoggedIn => mutate({ variables: { isLoggedIn } }),
    }),
  }),
  graphql(AUTHENTICATE, {
    props: ({ mutate }) => ({
      authenticate: (username, password) => mutate({ variables: { username, password } }),
    }),
  })
)(withRouter(Login));

So as you can see I have this.props.authenticate(username, password) which is used when the login form is submitted.

Then I have the this.props.updateLoggedInStatus(Boolean) which I am able to update the client cache / state.

How do I combine these so that I can call authenticate() and if it's successful, set the loggedInStatus and if it fails, set a hasErrored or errorMessage flag of sorts?

Thanks in advance.

EDIT:

I have attempted to handle updating the state within the callback of my mutation.

// Form submission handler
onSubmit = async ({ username, password }) => {
    this.setState({loading: true})
    this.props.authenticate(username, password)
      .then(res => {
        this.setState({loading: false})
        this.props.updateLoggedInStatus(true)
      })
      .catch(err => {
        this.setState({loading: false, errorMessage: err.message})
        console.log('err', err)
      })
  }

Is there a better way of doing it than this? It feels very convoluted having to wait for the call back. I would have thought I could map the response to my cache object via my resolver?

1

1 Answers

1
votes

I think the way you're currently handling it (calling authenticate and then updateLoggedInStatus) is about as clean and simple as you're going to get with apollo-link-state. However, using apollo-link-state for this is probably overkill in the first place. It would probably be simpler to derive logged-in status from Apollo's cache instead. For example, you could have a HOC like this:

import client from '../wherever/client'

const withLoggedInUser = (Component) => {
  const user = client.readFragment({
  id: 'loggedInUser', 
  fragment: gql`
    fragment loggedInUser on User { # or whatever your type is called
      username
      sales_channel
      full_name
      roles
      # be careful about what fields you list here -- even if the User
      # is in the cache, missing fields will result in an error being thrown
    }
  `
  })
  const isLoggedIn = !!user
  return (props) => <Component {...props} user={user} isLoggedIn={isLoggedIn}/>
}

Notice that I use loggedInUser as the key. That means we also have to utilize dataIdFromObject when configuring the InMemoryCache:

import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory'

const cache = new InMemoryCache({
  dataIdFromObject: object => {
    switch (object.__typename) {
      case 'User': return 'loggedInUser'
      // other types you don't want the default behavior for
      default: return defaultDataIdFromObject(object);
    }
  }
})