Apollo local state - querying for object w unknown keys

Viewed 1418

I'm trying to add Apollo to an app. I'll be using it for fetching data, and want to follow best practices and use the new API (as of Apollo 2.5) to store local UI state as well.

I've been looking at these docs, but their UI state is simpler than the one I want to implement.

I want to port over a pattern I've been using in Redux apps, where the values of many UI elements are stored centrally. In Redux, I've had a reducer object which stored my UI values, and looked like this:

{
  dropdowns: {
    someDropdown: [{ index: 0, value: 'Selected Value' }]
  },
  checkboxes: {
    someCheckbox: true
  }
}

As in, different types of elements were represented as objects for which each key was the unique ID of a UI element, and the value varied based on the element type.

I'm also open to a flattened version of the above, where there are no sub-categorizations by UI element type.

I also had a separate reducer storing the state of some UI elements, eg:

{
  openDropdownId: 'someDropdown',
  openModalId: null
}

Crucially, it seems, this data includes objects whose keys are not predictable. I'm both unsure how to type this using gql and unsure how to fetch this data efficiently (see bottom of question).

I've tried reproducing this with Apollo Client, but have run into a ton of errors. Here's my client setup:

For, say, a dropdown, the idea is that states here stores open/closed state , whereas values stores info about the selected value.

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';

const typeDefs = gql`
  extend type UI {
    states: Object
    values: Object
  }
`;

const defaults = {
  ui: {
    __typename: 'UI',
    states: {},
    values: {}
  }
};

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: {},
  typeDefs
});

console.log({ defaults });
cache.writeData({ data: defaults });

export default client;

Then I use that client like so:

<ApolloProvider client={client}>
  <App />
</ApolloProvider>

And here's an example of a query that throws an error:

const QUERY = gql`
  {
    ui @client {
      states
      values
    }
  }
`;

The errors I'm seeing are:

  • Uncaught Invariant Violation: Missing selection set for object of type undefined returned for query field states

That error shows up in the Query component.

I also get two warnings (probably one for states and one for values) that:

  • Missing field __typename in {}

Do I need to structure my data differently to query it like this?


Follow-up question: If a component has a UID, it'd be nice to be able to query its values like:

const QUERY = gql`
  {
    ui @client {
      states {
        ${uid}
      }
      values {
        ${uid}
      }
    }
  }
`;

Instead of getting all UI values and then finding the appropriate one in the component.

That throws an error, however:

Syntax Error: Invalid number, expected digit but got: "d".

Is there a way to perform such a query, or a better way to handle all this? Although this data structure might not be idiomatic, it does seem that Apollo should support this for filtered queries, no?

1 Answers

I've also been experimenting with local state management in apollo, so the following is somewhat contrived and very obscure, but it works.

Putting it bluntly — everything that's not a scalar or on array needs to have a type.

So this:

const defaults = {
  ui: {
    __typename: 'UI',
    states: {},
    values: {}
  }
};

Needs to become, for example, this:

const defaults = {
  ui: {
    __typename: 'UI',
    states: {
      __typename: 'UIStates'
    },
    values: {
      __typename: 'UIValues'
    }
  }
};

And in your query MUST reference a scalar or an array, so naturally your query won't work:

const QUERY = gql`
  {
    ui @client {
      states
      values
    }
  }
`;

You need to go deeper and target the scalars or arrays:

const QUERY = gql`
  {
    ui @client {
      states {
        foo
      }
      values {
        bar
      }
    }
  }
`;

The rest depends on your data.

If you need to filter a query — your query would look something like this (the first one returns all items, the next one filters an item by an id):

const TEST_CLIENT_FILTER_QUERY = gql`
  {
    bzz @client {
      id
      title
    }
    bzz(id: $id) @client {
      id
      title
    }
  }
`

And you will need a resolver for that too. This one is very dirty, I'm not sure it's the apollo-graphql way, but it works for my purposes for now.

    resolvers: {
      Query: {
        bzz: (root, variables, context) => {
          const key = context.getCacheKey({ __typename: 'bzz', id: variables.id })
          return context.cache.extract()[key]
        },
      },
    },

These are my defaults for such a query:

  cache.writeData({
    data: {
      bzz: [
        {
          __typename: 'bzz',
          id: 1,
          title: 'bzz 1',
        },
        {
          __typename: 'bzz',
          id: 2,
          title: 'bzz 2',
        },
      ],
    },
  })
Related