Recall Workshop

Recall Workshop

Slides introing an in-house built alternative to GraphQL/Relay/Apollo.

2c4d5ca71a92e0860a78e4f9e50e4406?s=128

Chris Pearce

January 30, 2018
Tweet

Transcript

  1. 1 Recall

  2. 2 Table of Contents 01 02 03 04 Imperative Data

    Fetching Data management Why? Declarative querying GraphQL JSON-API Recall What? How? Exercise! Internals Glance Schema Resources Queries Containers Tips and Tricks Reconciler Record Store Query Expander
  3. 3 https://github.com/kalohq/frontend/pull/2208 Check out the new docs!

  4. 4 Why?

  5. 5 Imperative Data Fetching :( Why?

  6. 6 Imperative data fetching

  7. 7 Data Management :( Why?

  8. 8 Caching Minimising Network Calls Persistance Overfetching Optimal Denormalisation UX

    Concerns
  9. 9 What?

  10. 10 Declarative Data Fetching What?

  11. 11 Imperative data fetching Declarative data fetching

  12. 12 GraphQL What?

  13. 13 “ GraphQL is a query language for APIs and

    a runtime for fulfilling those queries with your existing data. ” – The GraphQL Docs
  14. 14

  15. 15 BE Services GraphQL Runtime GraphQL Typical GraphQL Stack Relay

    / Apollo
  16. 16 Imperative data fetching Declarative data fetching

  17. 17 JSON-API What?

  18. 18 “ A specification for building APIs in JSON ”

    – The JSON-API Docs
  19. 19 Describe your resources Request what you want Get data

    back class Project(): #: The project title title = fields. String( required =True ) projects?fields=title [{ "type": "project", "id": "1", "attributes ": { "title": "My Project " } }]
  20. 20 Fields = Relationships + Attributes Explicit Shape Fetching =

    Sparse-fieldsets + Includes Parameters = Query Parameters Could be treated as a GraphQL synonym (without runtime)
  21. 21 Recall What?

  22. 22 “ Recall allows you to fetch data from anywhere

    in your React application in a declarative fashion ” – The Recall Docs
  23. 23 BE Services GraphQL Runtime GraphQL Typical GraphQL Stack Relay

    / Apollo • Relay reconciles queries into optimal requests to a single GraphQL runtime • The GraphQL Runtime resolves queries by requesting to backend services
  24. 24 BE Services GraphQL Runtime GraphQL Recall’s responsibilities overlaying the

    traditional GraphQL stack Recall Relay / Apollo
  25. 25 BE Services JSON-API Recall Recall is responsible for reconciling

    queries into numerous optimal requests a JSON-API backend.
  26. 26 Describe your resources Request what you want Get data

    back type Project = { // the project title title: string } q.many(Project , { fields: { title : true, }, }) List [ Project { title: "My Project " }] Recall
  27. 27 How? cd frontend && git checkout recall-workshop

  28. 28 Schema How?

  29. 29 import { GraphQLString, GraphQLBoolean, GraphQLObjectType, } from 'graphql/type'; import

    {connectionDefinition} from 'utils/schema/connections'; export const UserType = new GraphQLObjectType({ name: 'user', fields: () => ({ name: {type: GraphQLString}, picture: {type: connectionDefinition(FileType)}, is_verified: {type: GraphQLBoolean}, }), }); export const FileType = new GraphQLObjectType({ name: 'file', fields: () => ({ url: {type: GraphQLString}, }), }); We use the GraphQL type system powered by graphql-js library to write our schema. Every resource in our domain is described as an Object Type. The name is the resource type name. Fields can be any scalar type as well as connections (relationships) to other fields. You can have {many: true} connections. Schema
  30. 30 Expand on the schema found in src/core/api/schema.js and add

    definitions for Projects. This should match our lws schemas. It should at least define the “title”, “status”, “end date”, “number tasks” and “number of pending task” fields as well as relationships to freelancers and team members. Hint: Match up with the backend schemas https://github.com/kalohq/lws/blob/develop/lws/s ervices/projects/schemas.py Schema Exercise
  31. 31 Expand on the schema found in src/core/api/schema.js and add

    definitions for Projects. This should match our lws schemas. It should at least define the “title”, “status”, “end date”, “number tasks” and “number of pending task” fields as well as relationships to freelancers and team members. Hint: Match up with the backend schemas https://github.com/kalohq/lws/blob/develop/lws/s ervices/projects/schemas.py Schema Exercise export const ProjectType = new GraphQLObjectType({ name: 'project', fields: () => ({ title: {type: GraphQLString}, end_date: {type: GraphQLString}, status: {type: ProjectStatusEnum}, num_contributions: {type: GraphQLInt}, num_active_contributions: {type: GraphQLInt}, suppliers: { type: connectionDefinition( SupplierType, {many: true} ) }, users: { type: connectionDefinition( UserType, {many: true} ) }, }), })
  32. 32 Resources (as Records) How?

  33. 33 import {createApiResource} from 'api/utils'; import { UserType, FileType }

    from './schema'; export const User = createApiResource( UserType, 'Users' ); export const File = createApiResource( FileType, 'Files' ); // example usage const myUser = new User({ name: 'Pete', picture: new File({ url: 'http://kalohq.com/picture.gif', }), }) We convert our types into usable Immutable Records which we can use to represent data. These records also hold important metadata about our resources which is vital for recall to function. Most notably they store the root uri on the backend API for accessing this resource. These records will be used to construct the resultant values of your queries later as well as allowing you to create and store data in memory. Resources
  34. 34 Create resources for your newly defined types. These can

    be found in src/core/api/resources.js Resources Exercise
  35. 35 Create resources for your newly defined types. These can

    be found in src/core/api/resources.js Resources Exercise import {ProjectType} from './schema' export const Project = createApiResource( ProjectType, 'projects' );
  36. 36 Queries How?

  37. 37 import {query} from 'api/recall'; query.single(User, { params: vars =>

    ({ id: vars.id, }), fields: { name: true, picture: { url: true, }, }, }) query.many(User, { params: () => ({ page: { offset: 0, limit: 5, }, }), fields: {}, }) To start fetching data we first need to write Query Declarations. These describe what resource to query, parameters refining how to fetch the resource as well as the exact fields we expect to be provided. Since our schema is a cyclic graph of our entire domain our fields can be a tree that dives as deeply as we need to for our current UI. Our parameters can describe pretty much anything as they get mapped directly to query parameters on our network requests. Queries
  38. 38 We want to render the below UI. Write a

    query in any new file which describes the data requirements. Queries Exercise
  39. 39 We want to render the below UI. Write a

    query which describes the data requirements. Queries Exercise q.many(Project , { params: vars => ({ filter : { archived : vars.query.archived , status : vars.query.status, supplier_id : vars.params.supplierId || vars.query.supplier , q_title : vars.debouncedQuery .q, combined_members : vars.query.members, }, sort: vars.query.sort, page: { limit : vars.query.page * PAGE_SIZE , offset : 0, }, }), fields: { id: true, status : true, title : true, end_date : true, users : { limit : 3, edges : { name : true, picture : { url : true, }, }, }, suppliers : { limit : 3, edges : { name : true, email : true, picture : { url : true, }, }, }, num_contributions : true,
  40. 40 Containers How?

  41. 41 import {query, createContainer} from 'api/recall'; const container = createContainer({

    queries: { user: query.single(User, { params: vars => ({ id: vars.id, }), fields: { name: true, picture: { url: true, }, }, }), }, }); function MyComponent(props) { return <div>{props.user.name}</div>; } export default container(MyComponent); // example usage <ContainedComponent id="5" /> Recall is a React-first library. It provides a higher-order-component (createContainer) to allow you to define data requirements for components in-place. Note the query declaration we’ve used here is the same as before but we’ve put inside of a queries object and passed that to createContainer. The keys are important here as that is the name of the prop that your data will be passed to your component as. The vars of a query declaration in a container become the props being passed into the component. Containers
  42. 42 1. Using your previous query declaration, provide a container

    around the X component. Now when you visit your application you should see the your UI rendering with live data! 2. We have already hooked up various filters which store state in the client url (query parameters) and then pass them through via props to your container. Try hooking these filters up to your query parameters. 3. You have now used Recall to fulfil the requirements of this UI! Containers Exercise
  43. 43 1. Using your previous query declaration, provide a container

    around the X component. Now when you visit your application you should see the your UI rendering with live data! This lives in src/core/components/projects-index-page/p rojects-index-page.js 2. We have already hooked up various filters which store state in the client url (query parameters) and then pass them through via props to your container. Try hooking these filters up to your query parameters. 3. You have now used Recall to fulfil the requirements of this UI! Containers Exercise const contain = createContainer({ queries: { projects: q.many(Project, { params: props => ({ ... }), fields { ... } }) }, });
  44. 44 import {query, createContainer} from 'api/recall'; const container = createContainer({

    queries: { user: query.single(User, { params: vars => ({ id: vars.id, }), fields: { name: true, picture: { url: true, }, }, }), }, }); function MyComponent(props) { return <div>{props.user.name}</div>; } export default container(MyComponent); // example usage <ContainedComponent id="5" /> Queries are re-evaluated as props input changes. If the query changes then recall does the work to fetch new data and pass that into your component. Ie. You only DESCRIBE the data requirements. Recall provides. A container will provide another prop called “queries” which describes the state of each of your queries. You have strong guarantees about the data being passed into your components. A few final notes
  45. 45 Tips, Tricks & Answers How?

  46. 46 “How can I optimise queries?” You can mark part

    of a query as “deferred” which tells Recall you don’t mind if it comes back later.
  47. 47 “How can I optimise containers?” You can mark queries

    as conditional by defining an “active” property on the query.
  48. 48 Where should my containers go? As high in the

    tree as you can put them, avoiding adding to many branches.
  49. 49 “How does Recall cache things?” You should not care.

    Recall will always give you the latest value possible. Know the states. Invalidate as you update. It’s either ready or it’s not. Can also be working.
  50. 50 Internals Glance

  51. 51 Internals Glance BE Services Network Container Reconciler Store Query

    Expander JSON-API Query expander acts as a facade between containers and the store. Pulls query state. Reconciler, running on a tick, yields optimised network requests from all live queries. Store responds to network updates, new query states are resolved and the reconciler may move to the next step of a query next tick.
  52. 52 The End https://github.com/kalohq/frontend/pull/2208 Don’t forget to check out and

    refer to the new docs!