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

Recall Workshop

Recall Workshop

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

Chris Pearce

January 30, 2018
Tweet

More Decks by Chris Pearce

Other Decks in Programming

Transcript

  1. 1
    Recall

    View Slide

  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

    View Slide

  3. 3
    https://github.com/kalohq/frontend/pull/2208
    Check out the new docs!

    View Slide

  4. 4
    Why?

    View Slide

  5. 5
    Imperative Data Fetching :(
    Why?

    View Slide

  6. 6
    Imperative
    data fetching

    View Slide

  7. 7
    Data Management :(
    Why?

    View Slide

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

    View Slide

  9. 9
    What?

    View Slide

  10. 10
    Declarative Data Fetching
    What?

    View Slide

  11. 11
    Imperative
    data fetching
    Declarative
    data fetching

    View Slide

  12. 12
    GraphQL
    What?

    View Slide

  13. 13
    “ GraphQL is a query language for APIs
    and a runtime for fulfilling those queries
    with your existing data. ”
    – The GraphQL Docs

    View Slide

  14. 14

    View Slide

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

    View Slide

  16. 16
    Imperative
    data fetching
    Declarative
    data fetching

    View Slide

  17. 17
    JSON-API
    What?

    View Slide

  18. 18
    “ A specification for building
    APIs in JSON ”
    – The JSON-API Docs

    View Slide

  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 "
    }
    }]

    View Slide

  20. 20
    Fields = Relationships + Attributes
    Explicit Shape Fetching = Sparse-fieldsets + Includes
    Parameters = Query Parameters
    Could be treated as a GraphQL synonym (without runtime)

    View Slide

  21. 21
    Recall
    What?

    View Slide

  22. 22
    “ Recall allows you to fetch data from
    anywhere in your React application in a
    declarative fashion ”
    – The Recall Docs

    View Slide

  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

    View Slide

  24. 24
    BE Services
    GraphQL
    Runtime
    GraphQL
    Recall’s responsibilities overlaying the traditional GraphQL stack
    Recall
    Relay /
    Apollo

    View Slide

  25. 25
    BE Services
    JSON-API
    Recall
    Recall is responsible for reconciling queries into
    numerous optimal requests a JSON-API backend.

    View Slide

  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

    View Slide

  27. 27
    How?
    cd frontend && git checkout recall-workshop

    View Slide

  28. 28
    Schema
    How?

    View Slide

  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

    View Slide

  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

    View Slide

  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}
    )
    },
    }),
    })

    View Slide

  32. 32
    Resources (as Records)
    How?

    View Slide

  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

    View Slide

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

    View Slide

  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'
    );

    View Slide

  36. 36
    Queries
    How?

    View Slide

  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

    View Slide

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

    View Slide

  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,

    View Slide

  40. 40
    Containers
    How?

    View Slide

  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 {props.user.name};
    }
    export default container(MyComponent);
    // example usage

    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

    View Slide

  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

    View Slide

  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 {
    ...
    }
    })
    },
    });

    View Slide

  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 {props.user.name};
    }
    export default container(MyComponent);
    // example usage

    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

    View Slide

  45. 45
    Tips, Tricks & Answers
    How?

    View Slide

  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.

    View Slide

  47. 47
    “How can I optimise containers?”
    You can mark queries as conditional by defining
    an “active” property on the query.

    View Slide

  48. 48
    Where should my containers go?
    As high in the tree as you can put them,
    avoiding adding to many branches.

    View Slide

  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.

    View Slide

  50. 50
    Internals Glance

    View Slide

  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.

    View Slide

  52. 52
    The End
    https://github.com/kalohq/frontend/pull/2208
    Don’t forget to check out and refer to the new docs!

    View Slide