DEV Community

gshah2020
gshah2020

Posted on • Updated on • Originally published at gshahdev.com

Full Stack GraphQL

[Starter-Repo]

What is Graphql?

a "spec" that describes a declarative query language that your clients can use to ask an API for the exact data they want. This is achieved by creating a strongly typed Schema for your API, ultimate flexibility in how your API can resolve data, and client queries validated against your Schema.

Core Concepts

Graphql can be quite different based on how it is handled on the server-side vs. in client-side applications.

Server Side

On the server side graphql is about creating a graph based on the shapes of some objects that you'd like to expose to the web in the form of an api. This allows your users to query that graph in whatever manner they need. You have no control of what data is fetched when, the client dictates that at the time of making a request.

Client Side

Client side graphql operations mainly revolve around the fetching the exact data needed by various application. Clients make requests for data using queries and mutations which allow clients to ask for specific data and even manipulate that data.

Server-Side Config

Schema

typeDefs - since graphql is a strongly typed framework, it requires a strict typeDef of each part of your data.

resolvers - similiar to controllers in a REST api, and are responsible to retrieving data from a data-source. Whether that is a database, in memory, cache or even other apis.

query definitions - queries are are another type in graphql which the client side will primarily use in order to format their request body to meet the needs and demands of their particular application and use-case.

mutation definitions - similar to handers from a REST api, and are functions used for returning values for fields that exist on Types in a Schema. Resolver execution is dependent on the incoming client query.

composition - the ability to compose different apis together underneath a single graphql umbrella.

schema - all of the above components are combined to form a graphql schema, which is a similar to a snapshot of the data your api handles and instructions on how it will handle that data when responding to specific requests.


fragments - similar to partials in some front-end rendering libraries, fragments allow the required logic for queries and mutations to not overly repeat itself. When working with same/similar data there can be a lot of re-use, and fragments facilitate that ability.

Creating a Schema

Schemas are created using (SDL ) Schema Definition Language. Primarily this is used to define our graphql types, such as our queries, resolvers, and mutations.

☝️ Schemas can also be programmatically created using language constructs. In javascript these are funtions that return our schema.

  • Types - a construct defining a shape with fields
  • Fields - keys on a Type that have a name and value type
  • Scalars - primitive value type built into GraphQL
  • Query - type that defines operations clients can perform based on schema
  • Mutation - type that defines ouw clients can modify or create data

GraphQL Hello World Server

Minimal ApolloServer

// converts a template string into a graphql schema
const gql = require("graphql-tag"); 
// https://tinyurl.com/lxpwd2z

// graphql server
const { ApolloServer } = require("apollo-server"); 
// https://tinyurl.com/y3akd5kh

// schema definitons:
const typeDefs = gql`
  type User { # describes the shape of the User type
    # non-nullable fields ('!')
    email: String!
    avatar: String!
    friends: [User!]! # array of users, should always return an array of users
  }

  type Msg {
    title: "String!"
  }

  type Query { # used to define data requested from api
    me: User!
    msg: Msg!
  }
`;

const resolvers = {
  //creates a 1to1 mapping for each query defined above
  Query: {
    me() {
      // resolver for the 'me' query
      return {
        // hard-coded resolved dummy data
        email: "myemail@email.com",
        avatar: "https://source.unsplash.com/random",
        friends: [],
      };
    },
    msg() {
      return {
        title: "\"this is a default message\","
      };
    },
  },
};

// server is instantiated wtih typeDefs and resolvers
const server = new ApolloServer({
  typeDefs,
  resolvers,
  ctx() { // creates a context object on our server:
    return { models }; // allows resolvers and muations to access models
  },
});


server.listen(4000).then(() => console.log("server running on port 4000"));

Enter fullscreen mode Exit fullscreen mode

As we see above the minimum things we need to build a graphql server:

  • A Query Type
    • with at least one field
  • And a resolver for any field queries

Which is basically saying we need a Schema with atleast one query and resolver present.

image-20201120123614234

image-20201120123630142

Resolver Signature

fieldName: (parent, args, context, info) => data;
Enter fullscreen mode Exit fullscreen mode
Argument Description
parent This is the return value of the resolver for this field's parent (the resolver for a parent field always executes before the resolvers for that field's children).
args This object contains all GraphQL arguments provided for this field.
context This object is shared across all resolvers that execute for a particular operation. Use this to share per-operation state, such as authentication information and access to data sources.
info This contains information about the execution state of the operation (used only in advanced cases).

Resolver Arguments

@param : initialValue (aka - "parent", "_")

Query: {
  demo(initialValue, )
}
Enter fullscreen mode Exit fullscreen mode

initial value contains the parent resolvers context, this is because queries are executed in heirachical succesion, so the child query retains access to the parent query's context. and that is accessible as initialValue from any child query.

☝️ NOTE: top-level queries will not have any relationships above them as no other queries get resolved before them. So initialValue will be null for all top-level queries.

Query: {
  demo(_, )
}

⚠️ You may see this represented as an underscore: ( _ ). This simply tells us that this function is ignoring this argument, and makes no use of it.

@param : arguments (aka - "args", "_ _")

Query: {
  demo(_, arguments)
}
Enter fullscreen mode Exit fullscreen mode

arguments - contains any arguments that get passed into our mutations and queries, if any when there are any.

☝️NOTE: When the query or mutation doesn't make use of the arguments object it can also be represented with two underscores: (_ _)

❗️ IMPORTANT: expected arguments must be defind in the Schema, graphql will not have the ability to handle any unknown arguments, it will only act on arguments that is has prior knowledge of how to handle.

  • Arguments can be added to any field (if necessary)
  type Query {
      pets(type: String!): [Pet]
  }

Here the pets query takes an argument that is a String, which sets the type of the pet.

This can be accessed from our resolvers as:

Query: {
  pets(_, { type }, ctx) {
    return [/* pets */]
  }
}
  • Must be either an Input or Scalar Type.

@param : context (aka - "ctx")

Query: {
  demo(_, __, context)
}
Enter fullscreen mode Exit fullscreen mode

An object exposed manually through our apollo server which allows us to access things like our models and database from our resolvers.

☝️ Context must be instantiated on the server along with resolvers and typeDefs:

// api/src/server.js

const server = new ApolloServer({
  typeDefs,
  resolvers,
  ctx() {
    return { models };
  },
});

@param : info (aka - "ast")

Contains the abstract syntax tree of the current model. Contains information about the operation's execution state, including the field name, the path to the field from the root, and more.

⚠️ This param is rarely used.

Input Types

Input Types are field definitions that can be re-used to populate the arguments necessary for different resolvers.

💡 Think of them as Types for arguments.

  • All field values of input types must be either other Input Types or Scalars
// api/src/schema.js
input PetInput {
  name: String # optional
  type: String # optional
}

This can be used to populate the input field arguments for our pets resolver definition:

// api/src/schema.js

type Query {
  pets(input: PetInput): [Pet]!
}

And now this will be available on our pets resolver as input:

// api/src/resolvers.js

Query: {
    me(parent, args, context) {
      console.log(parent, args, context);
      return {
        id: "oajsdfkla",
        username: "bob1",
      };
    },
    pets(parent, { input }, { models }) {
      console.log(input, models);
      const pets = models.Pet.findMany(input || {});
      return pets;
    },
  },
}

Finally we can query for this data using our gql playground:

# playground query

{
  pets(input: { name: "Fido", type: "Wolf" }){
      id
      createdAt
      name
      type
  }
}

❗️Must use double quotes ("") in input field arguments

Challenge

  • define query type for the user and pet model in api/src/db/schema.js
  {
    "user": {
      "id": "string",
      "username": "string"
    },
    "pet":{
      "id": "string",
      "createdAt": "number",
      "name": "string",
      "type": "string"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • create query type for retrieving an array of pets

  • instantiate apollo server with required typeDefs and resolvers

// api/src/db/schema.js

const { gql } = require("apollo-server");

const typeDefs = gql`
  type User {
    id: String!
    username: String!
  }

  type Pet {
    id: String!
    createdAt: Int!
    name: String
    type: String!
  }
  type Query {
    me: User!
    pet: Pet!
  }
`;

module.exports = typeDefs;

Enter fullscreen mode Exit fullscreen mode
// api/src/resolvers.js

module.exports = {
  Query: {
    me() {
      return {
        id: "oajsdfkla",
        username: "bob1",
      };
    },
    pet() {
      return [
        {
          id: "lkajsdflkjasdflkj",
          createdAt: Date.now(),
          name: "fido",
          type: "mini wolf",
        },
      ];
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

returns hard-coded data matching each resolvers expected output.

// api/src/server.js

const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const { models, db } = require("./db");


// add models to apollo-context so that they are available via mutations && resolvers:
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context() {
    return { models };
  },
});



server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
Enter fullscreen mode Exit fullscreen mode

image-20201120153411382

image-20201120153451198

🛠 FIX: pets query not resolving can't read the model from context??

{
  "errors": [
    {
      "message": "Cannot read property 'Pet' of undefined",
    }
  ]
}

Query arguments:

  type Query {
    me: User!
    pets(input: PetInput): [Pet]! # takes optional resolvers defint by PetInput
    pet(input: PetInput): Pet # returns a single pet
  }
Enter fullscreen mode Exit fullscreen mode
    pets(_, { input }, { models }) {
      const pets = models.Pet.findMany(input); // uses input to populate db query
      return pets;
    },
    pet(_, { input }, { models }) {
      const pets = models.Pet.findOne(input); // uses input to populate db query
      return pets;
    },
Enter fullscreen mode Exit fullscreen mode

uses the input arguments to allow us to search for matching data from database

image-20201121000002792

image-20201121000042207

Mutations

A Type on a Schema that defines operations a client can perform to mutate data (CRUD).

Creating Mutations

  • Define Mutation Type on Schema
  • Add fields for Mutation Type
  • Add arguments for Mutation fields (mutations will likely always require arguments)
  • Create Resolvers for Mutation fields
const typeDefs = gql`
    #########

    input NewShoeInput {
    brand: String!
    size: Int!
  }

  type Mutation {
    newShoe(input: NewShoeInput!): Shoe!
  }

    ##########
`    

  const resolvers = {
    Query: {
      /*...*/
    },
    Mutation: {
      newShoe(_, { input }) {
        shoes.push(input);
        console.log(shoes);
        return input;
      },
    },
  }
Enter fullscreen mode Exit fullscreen mode

Challenge

Create a mutation to create a new pet:

// api/src/schema.js

    type Mutation {
    createPet(input: PetInput): Pet!
  }
Enter fullscreen mode Exit fullscreen mode
// api/src/resolvers.js  
  Mutation: {
    createPet(_, { input }, { models }) {
      const newPet = models.Pet.create(input);
      return newPet;
    },
  },
Enter fullscreen mode Exit fullscreen mode

image-20201121005156502

image-20201121005905866

Advanced SDL (Schema Definition Language)

Enums

A set of discrete values that can be used in place of Scalars. An enum field must resolve to one of the values in the Enum. Great for limiting a field to only a few different options.

☝️ By default Enums resolve to a string.

const typeDefs = gql`
  enum ShoeType {
    JORDAN
    NIKE
    ADIDAS
  }
  type Shoe {
    brand: ShoeType!
    size: Int!
  }
`
Enter fullscreen mode Exit fullscreen mode

Now we have limited the types to only the ones we've listed in our enum as options.

image-20201121011000974

Interfaces

Abstract Types that can't be used as field values, but instead are used as foundations for explicit Types. Great for when you have Types that share common fields, but differ slightly.

  interface Shoe {
    brand: ShoeType!
    size: Int!
  }
  type Sneaker implements Shoe {
    brand: ShoeType!
    size: Int!
    sport: String!
  }
  type Boot implements Shoe {
    brand: ShoeType!
    size: Int!
    hasGrip: Boolean!
  } 
Enter fullscreen mode Exit fullscreen mode

❗️ The real power of this isn't felt in our definitions, but rather when we make queries. A type that implements an interface doesn't require new query definitions. We can define all of our queries for Shoes, and they will also apply to Sneakers.

🤔 Think of Javascript Class constructors and how they can extend functionality from a parent class. This seems to work in a similar fashion.

❗️NOTE: You can not query an interface directly, a Shoe in our current example will always resolver to either a Sneaker or a Boot.

Interface Resolver Types

⚠️ We'll need to teach our resolvers how to determine whether they are resolving to a boot or a sneaker in the above use case. We even get a warning in the console that instructs us accordingly:

Type "Shoe" is missing a "__resolveType" resolver. Pass false into "resolverValidationOptions.requireResolversForResolveType" to disable this warning.
server running on port 4000
Enter fullscreen mode Exit fullscreen mode
  • note that the server is still running and didn't fail to start.

In order to resolve our shoe type, we'll need to create a custom resolver with the __resolveType method:

const resolvers = {
  /*...*/
  Shoe: {
    __resolveType(shoe) {
      if(shoe.sport) return 'Sneaker'
      return 'Boot'
    }  
  }
}
Enter fullscreen mode Exit fullscreen mode

image-20201121025946762

⚠️ If there is an error in this setup, you'll likely see the following error:

Abstract type X must resolve to an Object type at runtime 

Either the X type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function."

image-20201121030030479

Querying for Type specific fields of an Interface

image-20201121030507815

  • We also get access to the typeName in our queries:

image-20201121031448360

*☝️NOTE: * it seems that the __typename seems to only run once, even though we've defined it for the shoe itself and then for each type of shoe. This is because graphql de-dupes our fields automatically.

image-20201121031553601

Unions

Like interfaces, but without any defined common fields amongst the Types. Useful when you need to access more than one disjoint Type from Query, like a search.

The Union type indicates that a field can return more than one object type, but doesn't define specific fields itself

  type Sneaker {
    brand: ShoeType!
    size: Int!
    sport: String!
  }
  type Boot{
    brand: ShoeType!
    size: Int!
    hasGrip: Boolean!
  }

  union FootWear = Sneaker | Boot

  FootWear: {
    // required to resolve the type of the interface:
    __resolveType(shoe) {
      // returns the type name as a string
      if (shoe.sport) return "Sneaker";
      return "Boot";
    },
  },
Enter fullscreen mode Exit fullscreen mode

image-20201121033621725

Combining Interfaces and Unions:

  interface Shoe {
    brand: ShoeType!
    size: Int!
  }
  type Sneaker implements Shoe {
    brand: ShoeType!
    size: Int!
    sport: String!
  }
  type Boot implements Shoe {
    brand: ShoeType!
    size: Int!
    hasGrip: Boolean!
  }
  union FootWear = Sneaker | Boot

    FootWear: {
    // required to resolve the type of the interface:
    __resolveType(shoe) {
      // returns the type name as a string
      if (shoe.sport) return "Sneaker";
      return "Boot";
    },
  },
  Shoe: {
    // required to resolve the type of the interface:
    __resolveType(shoe) {
      // returns the type name as a string
      if (shoe.sport) return "Sneaker";
      return "Boot";
    },
  },
Enter fullscreen mode Exit fullscreen mode

image-20201121033437501

Relationships

Thinking in Graphs

Your API is no longer a predefined list of operations that always return the same shapes. Instead, your API is a set of Nodes that know how to resolve themselves and contain links to toher related Nodes. This allows a client to ask for Nodes and then follow those links to get related Nodes.

Adding Relationships

  • Add a Type as a field value on another Type
  • Create resolvers for those fields on the Type

First we'll update our data, so that we can grab a reference to users who own our shoes:

const shoes = [
  // 🚧
  { brand: "NIKE", size: "11", sport: "basketball", user: 1 },
  { brand: "ADIDAS", size: "19", sport: "soccer", user: 2 },
  { brand: "TIMBERLAND", size: "10", hasGrip: true, user: 3 },
];
Enter fullscreen mode Exit fullscreen mode

Next let's update our current schema:

  type User { # describes the shape of the User type
    # non-nullable fields ('!')
    id: Int! # 🚧
    email: String!
    avatar: String!
    shoes: [Shoe]! # 🚧
  }
  interface Shoe {
    brand: ShoeType!
    size: Int!
    user: User!
  }
  type Sneaker implements Shoe {
    brand: ShoeType!
    size: Int!
    user: User!
    sport: String!
  }
  type Boot implements Shoe {
    brand: ShoeType!
    size: Int!
    user: User!
    hasGrip: Boolean!
  }
  union FootWear = Sneaker | Boot
Enter fullscreen mode Exit fullscreen mode
const resolvers = {
    /*...*/
  Shoe: {
    // required to resolve the type of the interface:
    __resolveType(shoe) {
      // returns the type name as a string
      if (shoe.sport) return "Sneaker";
      return "Boot";
    },
    user(shoe) {
      return user;
    },
  },
  Sneaker: {
    user(shoe) {
      return user;
    },
  },
  Boot: {
    user(shoe) {
      return user;
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Note that we were not able to create the resolver for user on the Abstract Object (interface) Shoe. Instead we must individually resolve each of the types that implement our interface, in this case: "Boot" and "Sneaker"

image-20201121141940504

We can also query the interface alone:

image-20201121142600074

We can also use our new definitions to do other relational queries by adding a User resolver to help resolve shoes that belong to this specific user:

const resolvers = {
  /*...*/
  User: {
    shoes(user) {
      return shoes.filter((shoe) => shoe.user !== user.id);
    },
  },
  /*...*/
}
Enter fullscreen mode Exit fullscreen mode

image-20201121143109408

⚠️ This type of logic causes endless recursion:

image-20201121143328423

Challenge

  • A pet should have an associated user
  • A user should also contain an array of pets.

Create resolvers to handle the related data

First, we'll want to add the user field to our pet instances in the db:

    {
      "id": 1,
      "createdAt": "10-24-2020",
      "name": "fido",
      "type": "DOG",
      "user": "jBWMVGjm50l6LGwepDoty"
    },
Enter fullscreen mode Exit fullscreen mode

Nextwe'll need to ensure we expose our current user to our context:

// api/src/server.js

  context() {
    const user = models.User.findOne({ username: "frontendmaster" });
    return { models, db, user };
  },
Enter fullscreen mode Exit fullscreen mode

Next we can update our schema:

  type User {
    id: String!
    username: String!
    pets: [Pet]! # 🚧
  }

  type Pet {
    id: String!
    createdAt: String!
    name: String!
    type: String!
    user: User! # 🚧
  }
Enter fullscreen mode Exit fullscreen mode

And lastly we'll need to add resolvers for each type to resolver to the correct data for our new fields:

  Pet: {
    owner(pet, __, { models }) {
      // normally we'd have to find the matching user, but we only have one user in the db
      return models.User.findOne();
    },
  },
  User: {
    pets(_, __, { models, user }) {
      // return models.Pet.findMany({ user: user.id });
      return models.Pet.findMany();
    },
  },
Enter fullscreen mode Exit fullscreen mode

image-20201121172940796

image-20201121172958983

Client Side Config

As we saw in the server-side section queries and mutations get defined on the server-side of any graphql application, but from the front end we then have to execute those queries and mutations to interact wtih our data.

image-20201121184541107

Operation Names

Unique names for your client side Query and Mutation operations. Used for client-side caching, indexing inside of tools like GraphQl Playground.. Like naming your functions in JS vs keeping them anonymous.

Variables with operations

Operations can define arugments, very much like a function in most programming languages. Those variables can then be passed to query/mutation calls inside the operation as arguments. Variables are expected to be given at run time during operation execution from your client.

query AllCharacters {
  characters(page: 2) {
      results {
          name
      }
  }
}

I this query we're using a query variable called page, and we've currently hard coded it with the value '2', to help us target the second page of characters from this api.

We can also use query variables in graphql playgorund to help us test our queries that accept certain variable arguments:

query AllCharacters($page: Int) {
  characters(page: $page) {
      results {
          name
      }
  }
}

In this example we've created a variable called $page instead of head coding it.

☝️ NOTE: it is initially defined after the operation name and used in our characters query

image-20201121200731201

query varibles are scoped to the actual operation itself and can be used anywhere inside of them (when applicable):

query AllCharacters($page: Int, $id: String) {
  characters(page: $page) {
      results {
          name (id: $id)
      }
  }
}

Aliasing Queries and Mutations

Used to rename fields so that they match the shape expected by our clientside application.

image-20201121201812426

  • Also an example of a filter operation using query variables

We can also run multiple queries together:

image-20201121220551935

Apollo Client

Encapsulates HTTP logic used to interact with a GraphQL API. Doubles as a client side state management alternative as well (such as: Redux, MobX, etc...).

If your GraphQL API is also an Apollo Server, provies some extra features. Offers a plug-in approach for extending it's capabilities. It's also framework-independent.

☝️ Note that we can use graphql to handle local application state, as an alternative to local state management.

Storing Data from your API
  • All nodes (fields returned from a query) are stored flat by an unique ID
  • Unique ID is default to .id or ._id, from nodes. You can alter this behavior
  • Every node should send an .id or ._id, or none at all.
    • Or you can add custom logic to handle the id reference.

Queries in React

Setup Apollo Client
// client/src/client.js

import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import gql from "graphql-tag";

// network interface to access gql server -- link to gql connection
const link = new HttpLink({ uri: "http://localhost:4000" });
// sets up apollos in-memory-caching
const cache = new InMemoryCache();

// client takes the httplink and the cache as an argument
const client = new ApolloClient({ link, cache });

export default client;

Enter fullscreen mode Exit fullscreen mode
Testing External Queries:
// client/src/client.js

const link = new HttpLink({ uri: "https://rickandmortyapi.com/graphql" });


const query = gql`
  {
    characters {
      results {
        id
        name
      }
    }
  }
`;

client.query({ query }).then((results) => console.log(results));
Enter fullscreen mode Exit fullscreen mode

image-20201121225917476

  • Let's update our client uri once again:
// client/client.js

const link = new HttpLink({ uri: "http://localhost:4000" });
Enter fullscreen mode Exit fullscreen mode
  • Then we can use this to provide our client to our application:
// client/index.js

import { ApolloProvider } from "@apollo/react-hooks";
import client from "./client";


const Root = () => (
  <ApolloProvider client={client}>

    <BrowserRouter>
      <App />
    </BrowserRouter>

  </ApolloProvider>
);
Enter fullscreen mode Exit fullscreen mode

With this in place we've now gained access to the Apollo-dev-tools in our browser as well.

image-20201121231936713

This allows us to access our queries, mutations, and cache as well as the GraphiQL interface.

Now we can use the useQuery hook form apollo to help us qeury for our data:

// client/src/pages/Pets.js


export default function Pets() {
  const [modal, setModal] = useState(false);

  const MY_PETS = gql`
    query MyPets {
      pets {
        id
        name
      }
    }
  `;

  const { data, loading, error } = useQuery(MY_PETS);
  if (loading) return <p>Loading...</p>;
  if (error) return `XError! ${error.message}`;

  return (
    <div className='page pets-page'>
        {/*...*/}
      <section>
        <div className='row'>
          <PetsList pets={data.pets} />
        </div>
      </section>

    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Mutations

mutation CreateAPet ($newPet: NewPetInput!) {
  createPet(input: $newPet) {
    id
    name
    type
  }
}
Enter fullscreen mode Exit fullscreen mode

image-20201122003951671

Client Side Variables for mutations

// client/src/pages/Pets.js


   const NEW_PET = gql`
    mutation CreateAPet($newPet: NewPetInput!) {
      createPet(input: $newPet) {
        id
        name
        type
      }
    }
  `;


  const [createPet, newPet] = useMutation(NEW_PET);
  if (loading || newPet.loading) return <Loader />;
  if (error || newPet.error)
    return `XError! ${error && error.message} ${
      newPet.error && newPet.error.message
    }`;

  const onSubmit = (input) => {
    // input contains the values from our form: {name, type}
    setModal(false);
    createPet({ variables: { newPet: input } });
  };
Enter fullscreen mode Exit fullscreen mode

image-20201122010149481

Currently we're able to create a new pet, although our page does not respond to the new data and so we actually have to do a refresh to get the new data.

Why is Cache out of sync?

If you perform a mutation that updates or creates a single node, then apollo will update your cache automatically given the mutation and query have the same fiields and id.

⚠️ Always ty to match the fields on mutations and queries 1to1, this let's us opt into apollo's default caching behavior

image-20201122011032386

This is also why when we create a new pet apollo doesn't refresh the page, because it has never cached that item yet since it was just created. Which is why refreshing does pull down the item.

⚠️ This is also the case when we're deleting

If you perofrm a mutation that updates a node in a list or removes a node, you are responsible for updating any queries referencing that list or node. There are many ways to do this with apollo.

Keeping Cache in Sync

  • Refetch matching queries after a mutation
  • use update method on mutation (recommended)
  • watch queries
Refetch

Refetching enables you to refresh query results in response to a particular user action, as opposed to using a fixed interval.

Update method
  const [createPet, newPet] = useMutation(NEW_PET, {
    update(cache, { data: { createPet } }) {
      const data = cache.readQuery({ query: ALL_PETS });
      cache.writeQuery({
        query: ALL_PETS,
        data: { pets: [createPet, ...data.pets] },
      });
    },
  });
Enter fullscreen mode Exit fullscreen mode

In this approach we're manually handling the caching so that we can run an update using our query, which will allow us to re-load our data after a mutation occurs.

Watch Query

Provide this object to set application-wide default values for options you can provide to the watchQuery, query, and mutate functions.

Note: The useQuery hook uses Apollo Client's watchQuery function. To set defaultOptions when using the useQuery hook, make sure to set them under the defaultOptions.watchQuery property.

const defaultOptions = {
  watchQuery: {
    fetchPolicy: 'cache-and-network',
    errorPolicy: 'ignore',
  },
  query: {
    fetchPolicy: 'network-only',
    errorPolicy: 'all',
  },
  mutate: {
    errorPolicy: 'all',
  },
};
Enter fullscreen mode Exit fullscreen mode
watchQuery(options)

This watches the cache store of the query according to the options specified and returns an ObservableQuery. We can subscribe to this ObservableQuery and receive updated results through a GraphQL observer when the cache store changes.

NAME / TYPE DESCRIPTION
contextany Context to be passed to link execution chain
errorPolicy`"none" "ignore"
{% raw %}fetchPolicy`"cache-first" "network-only"
{% raw %}nextFetchPolicyany Specifies the FetchPolicy to be used after this query has completed.
notifyOnNetworkStatusChangeany Whether or not updates to the network status should trigger next on the observer of this query
partialRefetchany If true, perform a query refetch if the query result is marked as being partial, and the returned data is reset to an empty Object by the Apollo Client QueryManager (due to a cache miss).
pollIntervalany The time interval (in milliseconds) on which this query should be refetched from the server.
query`DocumentNode TypedDocumentNode`
returnPartialDataany Allow returning incomplete data from the cache when a larger query cannot be fully satisfied by the cache, instead of returning nothing.
variablesTVariables A map going from variable name to variable value, where the variables are used within the GraphQL query.

Optimistic UI

Your UI does not have to wait until after a mutation operation to update itself. Instead, it anticipates the response from the API and process as if the API call was in sync. The API response replace the generated response. This gives the illusion of your app being really fast.

Optimistic UI is a pattern that you can use to simulate the results of a mutation and update the UI even before receiving a response from the server. Once the response is received from the server, the optimistic result is thrown away and replaced with the actual result.

☝️ Loading Skeletons > Loading Spinners

// client/src/client.js


import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import { ApolloLink } from "apollo-link";
import { setContext } from "apollo-link-context";

import gql from "graphql-tag";

// network interface to access gql server -- link to gql connection
const http = new HttpLink({ uri: "http://localhost:4000" });

// create test for slower networks using apollo:
const delay = setContext(
  (request) =>
    new Promise((success, fail) => {
      setTimeout(() => {
        success();
      }, 800);
    })
);

// combines the usage of httplink && apollo link to a single link reference:
const link = ApolloLink.from([delay, http]);

// sets up apollos in-memory-caching
const cache = new InMemoryCache();

// client takes the httplink (from our custom link) and the cache as an argument
const client = new ApolloClient({ link, cache });

export default client;

Enter fullscreen mode Exit fullscreen mode

here we've simply taken the HttpLink that the client needs to intantiate ApolloClient and added a reference to a delay paramter using setContext from apollo. This allows us to simulate slow network requests, with our given delay parameter.

Now we can use this in our application and we can test our optimistic response setup using the delay we've configured so that the loading states will last longer than normal while in development mode.

// client/src/Pets.js

 const onSubmit = (input) => {
    setModal(false);
    createPet({
      variables: { newPet: input },
      optimisticResponse: {
        // used to render updated ui while graphql syncs db
        __typename: "Mutation", // operation type
        createPet: {
          // matches the resolver name
          id: "this-id-does-not-matter-here", // if id is available include -- gets replaced with new data from mutation
          __typename: "Pet", // include all expected fields,
          // these will be used to populate the data until the actuam mutation occurs/updates.
          type: input.type,
          name: input.name,
        },
      },
    });
  };
Enter fullscreen mode Exit fullscreen mode

optimisticResponse is a property we can set from our mutation's update functions.

Since we're taking advantage of the optimistic UI rendering, it's only good practice to remove any loading state handlers.

  // ❌ if (loading || newPet.loading) return <Loader />;
  if (loading) return <Loader />;

Remove newPet Loading state to opt out of loading state in favor of optimistic rendering.

☝️ NOTE we removed the loading handler for only our newPet mutation loading state, this is because we're looking to render it optimistically and if were to keep the loading state in place, it would flicker in very quickly as the state of our hook went from loading: true => loading: false.

Client Side Schemas

In addition to managing data from your API, apollo client can also be used to manage local state originating from our front end client side application.

Think of it as an alternative to reach for redux or other state management/machine libraries.

You can create a schema to define that state which allows you to query for that state the same way you query you api for data.

☝️ The approach here is very similar to creating schema for the backend of your application, you'll need the same two things to get started:

  1. typeDefs
  2. resolvers
// client/src/client.js

// extend backend types with front-end schema. 
const typeDefs = gql`
    extend type User {
        age: int
    }
`

const resolvers = {
  User: {
    age() {
      return 3 // hard-coded repsonse
        }
  }
}

// ❌ const client = new ApolloClient({ link, cache }); 
const client = new ApolloClient({ link, cache, resolvers, typeDefs });
Enter fullscreen mode Exit fullscreen mode

Now we can update our query to incorporate our changes:

// client/pages/Pets.js

  const ALL_PETS = gql`
    query allPets {
      pets {
        id
        type
        name
                owner {
                    id
                    age @client
                }
      }
    }
  `;
Enter fullscreen mode Exit fullscreen mode

The @directive @client tells apollo that this field gets resolved on the client-side only. It will simply ignore this field for any backend only operations and still allow us to use it on the front end as a piece of local state related to our user.

Fragments

A GraphQL fragment is a reusable part of the query. A GraphQL fragment lets you build multiple fields, and include them in multiple queries.

# fragment ownerInfo for RepositoryOwner fields

fragment ownerInfo on RepositoryOwner {
  id
  avatarUrl
  resourcePath
  url
}
Enter fullscreen mode Exit fullscreen mode
// GraphQL Query with fragments used in multiple queries

{
  googleRepo: repository(owner: "google", name: "WebFundamentals") {
    name
    owner {
      ...ownerInfo //fragment
    }
  }
  facebookRepo: repository(owner: "facebook", name: "react") {
    name
    owner {
     ...ownerInfo //fragment
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A fragment consists of three unique components:

  • Name: This is the unique name of the fragment (each fragment can have its own name)
  • TypeName: The type of object the fragment is going to be used on. This indicates which nested object, from the GraphQL schema, this fragment is created on
  • Body: The last part is the body of the fragment. The body of the fragment defines the fields that will be queried for

So why are fragments cool within a GrapQL query?

  • Reusability – With fragments, you can organize your queries into reusable units
  • CachingGraphQL clients make use of fragments, to provide caching options
// client/src/pages/Pets.js

  const PETS_FIELDS = gql`
    fragment PetsFields on Pet {
      id
      name
      type
      vaccinated
    }
  `;

// usage:

  const ALL_PETS = gql`
    query allPets {
      pets {
        ...PetFields
      }
    }
    ${PET_FIELDS}
  `;

  const NEW_PET = gql`
    mutation CreateAPet($newPet: NewPetInput!) {
      createPet(input: $newPet) {
        ...PetFields
      }
    }
    ${PET_FIELDS}
  `;
Enter fullscreen mode Exit fullscreen mode

Challenge:

  • Extend Pet Type with new Field: Vaccinated
  // client/src/client.js


  const typeDefs = gql`
    extend type Pet {
      vacinated: Boolean!
    }
  `;

  const resolvers = {
    Pet: {
      vacinated: () => true
    }
  };
Enter fullscreen mode Exit fullscreen mode
  • Fragment for pet type

  • Update fragment to exist for both ALL_PETS query and NEW_PET mutation


Hey everyone, I'm Gaurang, a full-stack developer that is constantly learning and building in public. I love that I was able to change my career at such a late stage in life all by learning online with so many different resources. I generate content to reinforce what I'm learning and to help others like me in the future.

If you enjoyed my content, please feel free to connect with me wherever you spend your time online.

Latest comments (0)