Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQL integration with Redux

fr0gM4ch1n3
January 08, 2019

GraphQL integration with Redux

fr0gM4ch1n3

January 08, 2019
Tweet

More Decks by fr0gM4ch1n3

Other Decks in Education

Transcript

  1. Table of content I. Redux (ngrx) II. GraphQL Subscriptions (data

    resources) III. Apollo Cache IV. Apollo ngrx-cache
  2. Consider using Redux You should when: • multiple components needs

    to access the same state but do not have any parent/child relationship • you start to feel awkward passing down the state to multiple components with props Dan Abramov says “Flux libraries are like glasses: you’ll know when you need them.”
  3. Consider using Redux And in fact it worked like that

    for me. Be aware that Redux is not useful for smaller apps. It really shines in bigger ones.
  4. Apollos local state management ??? Slides about Apollo: https://goo.gl/w1mjrZ Minimize

    query Redux Apollo Client Client Server UI Components Compose results Normalize
  5. Three Principles • Single source of truth • State is

    read-only • Changes are made with pure functions
  6. @ngrx - actions The principle of Redux says the only

    way to change the state is by sending a signal to the store Actions must have a type field that indicates the type of action being performed
  7. @ngrx - actions import { Action } from '@ngrx/store'; export

    const START = '[Entry] start timer'; export const STARTED = '[Entry] timer started'; export class Start implements Action { readonly type = START; constructor(public payload: { project: number }) { } } export class Started implements Action { readonly type = STARTED; constructor(public payload: Entry) { } } export type All = Start | Started;
  8. @ngrx - selectors import { createSelector } from '@ngrx/store'; import

    { IDB } from '../IDB.interface'; function getEntryState(db: IDB): Entry[] { return db.entryBranch; } export const getAll = createSelector( getEntryState, (state: Entry[]) => state ); export const getActive = createSelector( getEntryState, (state: Entry[]) => state.filter(entry => !entry.end) );
  9. @ngrx - reducers Reducers produce the state of the application

    A reducer is just a pure function. A reducer takes two parameters: the current state and an action const rootReducer = (state = initialState, action) => state;
  10. @ngrx - reducers export function EntryReducer(state = initialEntryBranch, action: EntryActions.All):

    EntryState { switch (action.type) { case EntryActions.START: { return { ...state, ...action.payload }; } case EntryActions.STARTED: { return { ...state, ...action.payload }; } default: { return state; } } }
  11. @ngrx - effects import * as entry from './entry.actions'; import

    { EntryResource } from './entry.resource'; @Injectable() export class EntryEffects { @Effect() start$: Observable<Action> = this.actions$.pipe( ofType<entry.Start>(entry.START), switchMap(({ payload }) => this.er.start(payload).pipe( map(payload => new entry.Started(payload)), catchError(err => of(new entry.Error(err))) ) ) ); constructor(private actions$: Actions, private er: EntryResource) { } }
  12. GraphQL subscribe queryRef.subscribeToMore({ document: gql` subscription entryChanged { entryChanged {

    id data } } `, onError: console.error, updateQuery: (prev, { subscriptionData }) => subscriptionData.data ? { ...prev, entries: [ ...prev.entries.filter((entry: Entry) => entry.id !== subscriptionData.data.entryChanged.id), subscriptionData.data.entryChanged ] } : prev });
  13. Caching data apollo-cache-inmemory is the default cache implementation for Apollo

    Client 2.0. InMemoryCache is a normalized data store that supports all of Apollo Client 1.0’s features without the dependency on Redux. import { InMemoryCache } from 'apollo-cache-inmemory'; import { HttpLink } from 'apollo-link-http'; import ApolloClient from 'apollo-client'; const cache = new InMemoryCache(); const client = new ApolloClient({ link: new HttpLink(), cache }); In some instances, you may need to manipulate the cache directly, such as updating the store after a mutation.
  14. Configuration The InMemoryCache constructor takes an optional config object with

    properties to customize your cache: • addTypename: A boolean to determine whether to add __typename to the document (default: true) • dataIdFromObject: A function that takes a data object and returns a unique identifier to be used when normalizing the data in the store. • fragmentMatcher: By default, the InMemoryCache uses a heuristic fragment matcher. If you are using fragments on unions and interfaces, you will need to use an IntrospectionFragmentMatcher. • cacheRedirects (previously known as cacheResolvers or customResolvers): A map of functions to redirect a query to another entry in the cache before a request takes place. This is useful if you have a list of items and want to use the data from the list query on a detail page where you’re querying an individual item.
  15. Normalization InMemoryCache will attempt to use the commonly found primary

    keys of id and _id for the unique identifier if they exist along with __typename on an object If id and _id are not specified, or if __typename is not specified, InMemoryCache will fall back to the path to the object in the query, such as ROOT_QUERY.allPeople.0 const cache = new InMemoryCache({ dataIdFromObject: object => object.key || null });
  16. Store structure { "Entry:14":{"id":14,"project":1,"start":1537971622,"end":null,"__typename":"Entry"}, "Entry:15":{"id":15,"project":2,"start":1537971624,"end":1537974315,"__typename":"Entry"}, "ROOT_QUERY":{ "entries":[ {"type":"id","generated":false,"id":"Entry:14","typename":"Entry"}, {"type":"id","generated":false,"id":"Entry:15","typename":"Entry"} ],

    "projects":[ {"type":"id","generated":false,"id":"Project:2","typename":"Project"}, {"type":"id","generated":false,"id":"Project:1","typename":"Project"} ] }, "Project:2":{"id":2,"name":"p2","__typename":"Project"}, "Project:1":{"id":1,"name":"p1","__typename":"Project"}, "ROOT_SUBSCRIPTION":{"entryChanged":{"type":"id","generated":false,"id":"Entry:22","typename":"Entry"}}, "ROOT_MUTATION":{"stopTimer({\"end\":1537974315,\"id\":15})":{"type":"id","generated":false,"id":"Entry: 15","typename":"Entry"}} }
  17. Direct Cache Access const { todo } = apollo.readQuery({ query:

    gql` query ReadTodo { todo(id: 5) { id text completed } } `, }); To interact directly with your cache, you can use the Apollo Client class methods readQuery, readFragment, writeQuery, and writeFragment.
  18. Setup ngrx cache @NgModule({ imports: [ StoreModule.forRoot({ apollo: apolloReducer, }),

    NgrxCacheModule, ], }) class AppModule { constructor(ngrxCache: NgrxCache) { const cache = ngrxCache.create({}); } } Put apolloReducer under apollo in your State and import NgrxCacheModule
  19. How to use Just run your GraphQL queries in effects

    and receive data asynchronously from store automatically
  20. How to use function getEntryState(db: IDB): Entry[] { return ((db.apollo.ROOT_QUERY

    || {}) .entries || []) .map((ref: RootQuery) => ref.id) .map((id: string) => db.apollo[id]); } export const getAll = createSelector( getEntryState, state => state ); Minor changes in selectors
  21. Helper for the Apollo store The biggest advantage now is

    to be able to connect the data in selectors. But how do we read from the store export const isApolloRef = (obj: any): Boolean => { return obj && obj.type === 'id' && typeof obj.id !== 'undefined' && typeof obj.typename !== 'undefined'; }; export const resolve = (apollo: any, key: string, obj?: any): any => (typeof (obj || apollo[key]) === 'object') ? Object.keys(obj || apollo[key]) .reduce((res: any, prop) => { !obj && isApolloRef((obj || apollo[key])[prop]) ? res[prop] = resolve(apollo, (obj || apollo[key])[prop].id) : res[prop] = obj && obj[prop] || resolve(apollo, null, apollo[key][prop]); return res; }, Array.isArray(obj || apollo[key]) ? [] : {}) : (obj || apollo[key]);
  22. How to use import { Store } from '@ngrx/store'; import

    { IDB } from '../state-management/IDB.interface'; import * as entry from '../state-management/entry/entry.actions'; import * as fromEntry from '../state-management/entry/entry.reducer'; export class EntriesComponent { public entries$: Observable<Entry[]>; constructor( private store: Store<IDB>) { this.entries$ = this.store.select(fromEntry.getAll); this.store.dispatch(new entry.Load()); } } <ul> <li *ngFor="let entry of entries$ | async">{{ entry | json }}</li> </ul> import { Store } from '@ngrx/store'; import { IDB } from '../state-management/IDB.interface'; import * as entry from '../state-management/entry/entry.actions'; import * as fromEntry from '../state-management/entry/entry.reducer'; export class EntriesComponent { public entries$: Observable<Entry[]>; constructor( private store: Store<IDB>) { this.entries$ = this.store.select(fromEntry.getAll); this.store.dispatch(new entry.Load()); } } <ul> <li *ngFor="let entry of entries$ | async">{{ entry | json }}</li> </ul>