Ian Huet - Development Journal

Global vs. Server State

Within a client-side app, can the creation & maintainence of global state be automated away without loosing capabilities?

TL;DR

Is there a easy-to-use, automated way to handle read-only server data on the client-side without building global state? I created a simple application in both Vue3 & React to investigated this. Using Apollo Client to connect with the same GraphQL API in both apps, the ability to do fine-grain cache querying in React presents a significant advantage. For now Vue@3 still requires a client-side global store to achieve the same functionality.


A client’s project is built with Vue@2, Vuex, Apollo Client & GraphQL. Coming from a React background, I thought this stack would enable the automated handling of server state. During a recent discussion I queried why Apollo Client caching was not being used in this way, instead of using Vuex to store all server data. The response was that it was not possible. To sense check my thoughts around this I created a simple application in both Vue3 & React to compare capabilities.

Technical Context:

The client/server architecture of SPAs1 neccessitates all server data be transmitted to the client. Tools like Vuex or Redux are then used to cache this data as global state, to reduce the latency penalty this pattern incures. This has several benefits: after the client buffer is primed it creates a perceived UI performance improvement, it removes the need for prop drilling, data can be accessed rather than copied, as well as acting as a communciation bus - potentially connecting separate aspects of an app.

Global state is the generic name for this client-side data store. The bucket that both a clients app state and all that server persisted data are dropped into. While the mentioned benefits are handy this approach also adds complexity, and an on-going maintenance overhead. Further more it handles these different data types, with their different characteristics, in the same way. This is extra work for a suboptimal solution.

Can this client data and server data be broken up for a better outcome achieved with less work?

Application Outline

  1. Use the same, existing GraphQL API - star-wars-swapi@current.
  2. Use a router to enable both a List & Detail view of the available data.
  3. Using Apollo Client, how much can be achieved in React & Vue@3 without using global state?

Iterations

Vue v.1

  1. Using Apollo Studio makes exploring the API simple, and just as easy to create GraphQL queries.

  2. These queries were then copied across to the application, and wrapped with graphql-tag to enable GraphQL to parse those queries.

  3. @vue/apollo-composable@4.beta is a Vue wrapper around @apollo/client@3.7.15. It provides a ‘hook’ style abstraction for making requests. Using this makes the whole data request lifecycle super, straight forward.
    const { result, loading, error } = useQuery(queries.listFilms)
    
  4. Setup @graphql-codegen/cli to generate Typescript types from the defined GraphQL queries, and apply them to add type-safety.

React v.1

Firstly, recreate everything implemented in the Vue app. Then try to improve on several aspects:

  1. Refactor queries using fragments, extracting portions of the queried data that are reuseable. This fragmention has the added bonus of enabling the use of cache querying with the Apollo Client readFragment method. As sections of the cached data can now be directly accessed this removes the need for prop drilling.
    const production = client.readFragment({
      id: `Film:${id}`,
      fragment: fragments.filmProduction,
    });
    
  2. Review the GraphQL codegen configuration as manually applying the generated types is tedious. In place of graphql-tag the codegen process generates its’ own template literal tag for parsing GraphQL queries. Switching this over achieves the same functionality while also automatic applying the types generated from the queries to the responses.
    import { gql } from '../generated/gql';
    
  3. Apollo Client enables individual field data decorators, applied via a Type Policy on the cache configuration. Applying this type policy on the cache enables the transformation of individual data points as they are received, upstream of any use within the application.
    const cacheConfig = {
      typePolicies: {
     Film: {
       fields: {
         releaseDate: {
           read(releaseDate: string): string {
             const date = new Date(releaseDate);
             return `${date.getDate()} / ${date.getMonth() + 1} / ${date.getFullYear()}`;
           }
         },
    
  4. If navigating to a Film Detail view from the Film List view there is cached data that can be presented while loading the FilmDetail query. This is enabled by passing the cached FilmProduction fragment across with the navigation, using the react-router-dom data APIs.
    const film = {
      ...cache,
      ...data?.film,
    }
    
  5. Using the cache in this way can be extended with local fields, custom fields added on the client-side and queryable with any of the cached server state. Though a local schema is required to integrate local fields with the automatically generated types.
    extend type Film {
      episodeIdNumeral: String
    }
    

Vue v.2

Time now to see how many of this enhancements can be ported to the Vue app…

  1. Reconfigure @graphql-codegen/cli to use in place of graphql-tag, enabling the automatic application of type-safe on requests.
  2. Add a client-side local field and add it to the generated types using a supplimentary local schema.

Take aways

Reference:

Troubleshooting:

Learn More:


  1. Single Page Application (SPA): Is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of a web browser loading entire new pages. The goal is faster transitions that make the website feel more like a native app. wikipedia