Not a day goes by without a new idea for a state management library popping up. I want to talk about yet another one. But be warned, . It is an idea I had and wanted to share with the community. this is not a real library This is supposed to be a mashup of Git and GraphQL logos I will call this hypothetical library . GitState GitState helps you can manage your state locally and keep parts of it in sync with your remote datastore. All these transactions are handled with a git-flavoured api. It should work with any GraphQL backend with possibly some configuration. It is responsible for the following: — It acts like a predictable, immutable state container for both local and tracked remote data. Keeping / managing local state — You can slices of your remote datastore, and expand / alter these slices (for load more, filtering etc.). Loading and error states are handled by GitState. Handling data flow track — Requests are completely abstracted away. You can add or modify tracks, nothing hits the backend until you execute . Once you do, GitState generates an optimal query based on the tracks and the cache, so it hits the backend as few times as possible, requesting minimal amount of payload. Handling the request flow G.pull — GitState keeps track of fetched resources by their and ’s separately from the state, normalised and independent from the hierarchy they were pulled in. It allows caching strategies to be defined globally, for each type or for each field of each type individually. These are considered while optimising the queries. Caching __typename id Here is a brief walkthrough to the API. Note that all functions are auto-curried, in a config first data last order. G Handle of the library, imported like so: import G from 'gitstate' G.init For configuring GitState client. G.init({ origin: "<server ip>" }) G.track Similar to , this method tracks a slice of the remote datastore. Takes in a and a . It can optionally take and options. It either creates a new track, or overrides if it already exists. git track … track key GQL query query parameters G.track(key)(query)(params)(options) The scope of an existing track could easily be modified to support things like , and . This can easily be achieved by overriding the query parameters. To do this, simply omit the query and provide the new values. pagination querying filtering G.track(key)(params)(options) Example: const q = `query Posts($limit: Int, $offset: Int){posts(limit: $limit, offset: $offset) {idtitlecontents}}`; // Create a new track & start pullingG.track('posts')(q)({ limit: 10, offset: 0 })()G.pull('posts')() // ...Later // Modify the scope of the tracked postsG.track('posts')({ offset: 10 })({ append: true })G.pull('posts')() G.pull Takes in two optional parameters, and . Omitting either one works. When track key is provided, it only attempts to fetch the given track, otherwise it targets all existing the tracks. track key options G.pull(key)(options) When called, GitState fetches only the data that needs to be fetched. If a track has changes in their query parameters, it gets fetched. If not, the decision is based on the cache resolution strategy or the option. force Example: G.pull()()G.pull('posts')()G.pull()({ force: true })G.pull('posts')({ force: true }) G.status Status is an object where GitState keeps and . It is organised by the track keys. This object could be used to handle loading / error states, and access track parameters. request info track metadata Example: // G.status// {// userDetails: {// fetched: <boolean>, // has it been fetched// fetching: <boolean>, // is it fetching now// loading: <boolean>, // see G.exec// hasChange: <boolean>, // did the request params change// error: <err object>, // error object for the last request// params: {...}, // current query parameters// options: {...}, // current query options// }// } G.forms is an internally used object that holds a normalised and flattened index of all fetched resources by their and . GitState keeps this object in sync with the backend after each pull. It holds resource data and the last fetch time (for each individual field). Forms object is the single source of truth for the most recently fetched versions of resources, and the application state is a derivation of this object. Forms __typename id G.state Local state of the application. Organised by track keys. The getter function derives the current state from the forms object, thus it always contains the latest fetched version of each resource. Additionally, it holds the non-tracked local variables on the root level. To get the state, simply call the getter function. Example: G.track('userDetails')(`...`)G.track('favoritePosts')(`...`)G.pull() // G.state()// {// userDetails: {...},// favoritePosts: {...},// nonTrackedLocalVariable: 'blah'// } G.add Creates or updates a node in the local state tree. It takes an , and a for the node. It is auto-vivified, meaning that it creates a new node and all its path if it doesn’t exist. object path new value G.add(path)(val) If the updated node is tracked, that means it is actually just a derivation of a resource on the forms object. In that case, GitState updates the associated resource form, so all state variables derived from that resource also gets updated. Example: G.add('nonTrackedLocalVariable')('blah')G.add('counter')(G.state().counter + 1)G.commit() // Below, it actually updates the associated `User` resource on G.forms, so this change gets reflected on all references on the local state object that points to that resource G.add('userDetails.email')('new@email.com')G.commit() Note that this change is only local. The GraphQL way of modifying data is by calling mutations. Updates to the local state on tracked data gets overridden on the next pull if the change didn’t also occur in the backend. See . G.exec G.commit The add operations are just queued and they aren’t effective until they are committed. This assures that the developer can decide exactly when to update the state, and until then, everything that uses the state receives the same version. G.commit(options) G.exec This command is used for executing mutations. It takes in an and a . It can additionally take and some options. execution key GQL mutation parameters G.exec(key)(mutation)(params)(options) The loading / error state of a mutation call is handled on just like the tracks. It is organised under the execution key: G.status G.exec('modifyUserDetails')(`...`)({...})({...}) // G.status// {// modifyUserDetails: {// loading: <boolean>,// error: <err object>,// }// } Mutations are the only way to perform write operations in GQL, and the updates must be kept in sync with the local state. There are a few options to do this. Here is an example mutation to show how these options work: const m = `mutation ModifyUserEmail($id: String!){modifyUserEmail(id: $id){idemail}}`; // Note that since functions are curried, `exec` below is assigned to a function that takes in the options to trigger the G.exec call. const exec = G.exec('modifyUserEmail')(m)({ id: '123' }); — If you know that the mutation responds with the modified resource, or just the modified bits, you can use the option. This goes through the response, matches the resources to the forms counterparts by their given and ’s, and updates the forms object. Thus, you’ll have the latest version of the modified resources on your local state. Merge merge __typename id exec({ merge: true }) — If you know what field changed without consulting the server response, or if you wish to update the state manually, you can use your preferred method for asynchronousy, and update the local state like so: Manual // Manually update the local stateconst updateEmail = res => {G.add('userDetails.email')(res.email);G.commit()} // Callbackexec({ callback: updateEmail }) // Promiseexec({ promise: true }).then(updateEmail) // Async / awaitconst res = await exec({ async: true })updateEmail(res); — There might be times where you don’t know how the resource changed, and the backend might not be returning the modified version. Or for some reason you might need to refetch a track. In those cases, you can use the trigger option. It takes in a track key or multiple track keys, and associates the mutation with the given tracks. It sets the flag of all associated tracks on to true, and once resolved, it triggers a forced on them. Trigger loading G.status G.pull exec({ trigger: 'userDetails' }) // G.status// {// userDetails: {..., loading: true },// modifyUserEmail: {..., loading: true},// } — You can combine all of the above: Hybrid exec({merge: true,trigger: 'userDetails',callback: res => /* do something */}) Final words As I mentioned, this is not a real library. It is not complete, and probably there are many more things to consider to make this a reality. If you liked this idea, you might want to check out . It doesn’t have a git-styled api, but the way things are handled are quite similar. Apollo Client I hope GitState inspires some, and I would love to hear what you think!