3 Years GraphQL at Product Hunt

3 Years GraphQL at Product Hunt

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

February 21, 2020
Tweet

Transcript

  1. 2.
  2. 4.
  3. 5.
  4. 7.
  5. 9.
  6. 10.
  7. 11.
  8. 12.
  9. 13.
  10. 14.
  11. 15.

    early 2014 jQuery spaghetti October 2014 Backbone February 2015 React

    & Rails May 2015 custom Flux December 2015 Redux January 2016 React-Router April 2016 Redux Ducks Febuary 2017 GraphQL
  12. 17.
  13. 18.
  14. 24.
  15. 26.
  16. 27.
  17. 28.

    GraphQL GraphQL (ruby) Apollo React Ruby on Rails Business Logic

    Business Logic frontend backend Custom Utilities
  18. 38.

    Connections query Name($cursor: String) { someField(first: 20, after: $cursor) {


    edges { cursor
 node {
 ...Node
 }
 }
 pageInfo {
 hasNextPage hasPreviousPage startCursor endCursor
 } } } https://relay.dev/docs/en/graphql-server-specification#connections
  19. 39.

    Connections query Name($cursor: String) { someField(last: 20, before: $cursor) {


    edges { cursor
 node {
 ...Node
 }
 }
 pageInfo {
 hasNextPage hasPreviousPage startCursor endCursor
 } } } https://relay.dev/docs/en/graphql-server-specification#connections
  20. 40.

    Connections + Total Count query Name($cursor: String) { someField(first: 20,

    after: $cursor) {
 totalCount
 edges { cursor
 node {
 ...Node
 }
 }
 pageInfo {
 hasNextPage hasPreviousPage startCursor endCursor
 } } }
  21. 41.
  22. 42.

    import HomepageSidebarFragment from './Sidebar/Fragment'; import StackCardFragment from '~/components/StackCard/Fragment'; import gql

    from 'graphql-tag'; export default gql` query HomePage($cursor: String, $loadMore: Boolean!) { homepageFeed(first: 20, after: $cursor) { edges { node { id ...StackCardFragment } } pageInfo { endCursor hasNextPage } } ...@skip(if: $loadMore) { ...HomepageSidebarFragment }
 } ${StackCardFragment} ${HomepageSidebarFragment} `;
  23. 43.

    import HomepageSidebarFragment from './Sidebar/Fragment'; import StackCardFragment from '~/components/StackCard/Fragment'; import gql

    from 'graphql-tag'; export default gql` query HomePage($cursor: String, $loadMore: Boolean!) { homepageFeed(first: 20, after: $cursor) { edges { node { id ...StackCardFragment } } pageInfo { endCursor hasNextPage } } ...@skip(if: $loadMore) { ...HomepageSidebarFragment }
 } ${StackCardFragment} ${HomepageSidebarFragment} `;
  24. 44.

    import HomeSidebarFragment from './Sidebar/Fragment'; import StackCardFragment from '~/components/StackCard/Fragment'; import gql

    from 'graphql-tag'; export default gql` query HomePage($cursor: String, $loadMore: Boolean!) { homepageFeed(first: 20, after: $cursor) { edges { node { id ...StackCardFragment } } pageInfo { endCursor hasNextPage } } ...@skip(if: $loadMore) { ...HomepageSidebarFragment }
 } ${StackCardFragment} ${HomeSidebarFragment} `;
  25. 45.

    export default createPage<HomePage>({ query: QUERY, queryVariables: { cursor: null, loadMore:

    false }, title: 'YourStack', titleNoPrefix: true, component({ data, fetchMore }) { return ( <Layout sidebar={<Sidebar data={data} />}> <InfiniteScroll connection={data.homepageFeed} connectionPath="homepageFeed" fetchMore={fetchMore}> {(use, i) => <StackCard index={i} use={use} />} </InfiniteScroll> </Layout> ); }, });
  26. 46.

    wraps Apollo query handles page state (loading, loaded, error) handles

    errors handles authentication handles authorization handles SEO tags handles feature flags type safe createPage createPage<QueryData>({ component: query: queryVariables: requireLogin: requirePermissions: requireFeature: tags: title: titleNoPrefix: );
  27. 47.

    export default createPage<HomePage>({ query: QUERY, queryVariables: { cursor: null, loadMore:

    false }, title: 'YourStack', titleNoPrefix: true, component({ data, fetchMore }) { return ( <Layout sidebar={<Sidebar data={data} />}> <InfiniteScroll connection={data.homepageFeed} connectionPath="homepageFeed" fetchMore={fetchMore}> {(use, i) => <StackCard index={i} use={use} />} </InfiniteScroll> </Layout> ); }, });
  28. 48.

    knows how to map connection knows how to read "pageInfo"

    fetches more pages type safe InfiniteScroll <InfiniteScroll connection={...} connectionPath={...} cursorName={...} fetchMore={...} variables={...}> {(item) => ...} </Form>
  29. 50.
  30. 51.

    How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + N * 4
  31. 52.

    How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + N * 4
  32. 53.
  33. 54.

    [{ id: 1, isLiked: true }, { id: 2, isLiked:

    false }, { id: 3, isLiked: false }]
  34. 55.

    [{ id: 1, isLiked: "SELECT * FROM likes WHERE stack_item_id=1

    and user_id={user_id}" }, { id: 2, isLiked: "SELECT * FROM likes WHERE stack_item_id=2 and user_id={user_id}" }, { id: 3, isLiked: "SELECT * FROM likes WHERE stack_item_id=3 and user_id={user_id}" }]
  35. 57.

    SELECT * FROM likes WHERE stack_item_id IN {ids} and user_id={user_id}

    [{ id: 1, isLiked: [Promise to check(1)] }, { id: 2, isLiked: [Promise to check(2)] }, { id: 3, isLiked: [Promise to check(3)] }]
  36. 58.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, null: false
 field :product, ProductType, null: false end end
  37. 59.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, null: false
 field :product, ProductType, null: false end end
  38. 60.

    class Graph::Resolvers::IsLiked < Resolvers::Base type Boolean, null: false def resolve

    user = context.current_user return false if user.nil? Loader.for(user).load(object) end class Loader < GraphQL::Batch::Loader def initialize(user) @user = user end def perform(items) ids = @user.likes.where(stack_item_id: items.map(&:id)).pluck(:stack_item_id) items.each do |items| fulfill item, ids.include?(item.id) end end end end
  39. 61.

    class Graph::Resolvers::IsLiked < Resolvers::Base type Boolean, null: false def resolve

    user = context.current_user return false if user.nil? Loader.for(user).load(object) end class Loader < GraphQL::Batch::Loader def initialize(user) @user = user end def perform(items) ids = @user.likes.where(stack_item_id: items.map(&:id)).pluck(:stack_item_id) items.each do |items| fulfill item, ids.include?(item.id) end end end end
  40. 62.

    How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + N * 4
  41. 63.
  42. 64.

    How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 + N * 3
  43. 65.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, null: false
 field :product, ProductType, null: false end end
  44. 66.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, null: false
 field :product, ProductType, null: false end end
  45. 67.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, null: false
 field :product, ProductType, null: false end end SELECT * FROM product WHERE id={id}
  46. 68.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, null: false
 field :product, ProductType, null: false end end SELECT * FROM product WHERE id={id}
  47. 70.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked field :profile, ProfileType, resolve: Graph::Resolvers::Association.new(:profil field :product, ProductType, resolve: Graph::Resolvers::Association.new(:produc end end
  48. 71.

    module Types class StackItemType < BaseRecord field :note, String, null:

    false field :is_liked, resolve: Graph::Resolvers::IsLiked 
 association :profile, ProfileType
 association :product, ProductType end end
  49. 73.

    How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 + N * 3
  50. 77.
  51. 78.

    mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations https://relay.dev/docs/en/graphql-server-specification.html#mutations
  52. 79.

    mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations https://relay.dev/docs/en/graphql-server-specification.html#mutations
  53. 80.

    mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations
  54. 81.

    mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations
  55. 84.

    module Mutations class ProductLikeCreate < BaseMutation argument_record :product, Product authorize

    returns Types::ProductType def perform(product:) Like.find_or_create_by!( product: product, current_user: current_user ) end end end Mutations::BaseMutation
  56. 85.

    Mutations::BaseMutation module Mutations class ProductUpdate < BaseMutation argument_record :product, Product,

    authorize: :manage argument :name, String, required: true
 argument :tagline, String, required: true argument :topic_ids, String, required: true
 returns Types::ProductType def perform(product:, **attributes) Products::Form.submit(
 current_user: current_user, product: product, attributes: attributes ) end end end
  57. 86.

    Mutations::BaseMutation ensures proper mutation format - node and errors handles

    loading of records handles authenication handles authorization handles model validations handles error case handles server validation
  58. 87.
  59. 88.

    import gql from 'graphql-tag'; export default gql` fragment ProductLikeButtonFragment on

    Product { id isLiked likesCount } `; ~/components/ProductLikeButton/Fragment.ts
  60. 89.

    import FRAGMENT from './Fragment'; export const CREATE_MUTATION = gql` mutation

    ProdutLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { ...ProductLikeButtonFragment } } } ${FRAGMENT} `; export const DESTROY_MUTATION = gql` mutation ProductLikeDestroy($input: ProductLikeDestroyInput!) { productLikeDestroy(input: $input) { node { ...ProductLikeButtonFragment } } } ${FRAGMENT} `; ~/components/ProductLikeButton/Mutation.ts
  61. 90.

    interface IProps { product: ProductLikeButtonFragment; } export default function ProductLikeButton({

    product }: IProps) { return ( <Button.Mutation className={styles.likeButton} requireLogin="like" mutation={product.isLiked ? DESTROY_MUTATION : CREATE_MUTATION} input={{ productId: product.id }} optimisticResponse={optimisticResponseFor(product)}> <LikeIcon /> {product.likesCount} </Button.Mutation> ); } ~/components/ProductLikeButton/index.tsx
  62. 91.

    Button.Mutation wraps our internal Button component wraps Apollo mutation handles

    authentication handles confirms handles mutation responses <Button.Mutation className={...} mutation={...} mutationInput={...}
 onMutate={...} updateCache={...} refetchQueries={...} optimisicResponse={...}
 requireLogin={...}
 confirm={...} />
  63. 92.
  64. 93.
  65. 94.

    <Form.Mutation mutation={MUTATION} onMutate={onComplete}> <Form.Field name="title" /> <Form.Field name="email" control={Form.Email} />

    <Form.Field name="tagline" control={Form.Textarea} /> <Form.Field name="category" control={Form.Select} options={CATEGORIES} /> <Form.Field name="photos" control={PhotosInput} />
 <Form.Field name="topics" control={TopicsInput} /> <Form.Submit /> </Form.Mutation>
  66. 95.

    wraps a form library (FinalForm) wraps Apollo mutation ensures consistent

    form look support for custom inputs executes mutation handles success case handles error case handles cache updates <Form.Mutation className={...}
 initialValues={...}
 mutation={...} mutationInput={...}
 onMutate={...} updateCache={...} refetchQueries={...} optimisicResponse={...}>
 <!-- inputs -->
 </Form> Form.Mutation
  67. 97.

    GraphQL can help you write both cleaner frontend and backend

    code integrate your backend and frontend don't be afraid to build custom tools solve core concerns on top level naming conventions page lifecycle performance validations auth [...]
  68. 98.
  69. 99.