3 Years GraphQL at Product Hunt

3 Years GraphQL at Product Hunt

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

February 21, 2020
Tweet

Transcript

  1. Three+ Years GraphQL in Product Hunt Radoslav Stankov 21/02/2020

  2. None
  3. Radoslav Stankov @rstankov blog.rstankov.com
 twitter.com/rstankov
 github.com/rstankov
 speakerdeck.com/rstankov

  4. None
  5. None
  6. 9 MAY | SOFIA | 2020 EDITON

  7. None
  8. https://speakerdeck.com/rstankov

  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  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
  16. April 2019 Your Stack

  17. None
  18. None
  19. GraphQL First

  20. Have good defaults Have good code organization Isolate dependencies Extensibility

    and reusability Make common operations easy
  21. Have good defaults Have good code organization Isolate dependencies Extensibility

    and reusability Make common operations easy
  22. Frontend Stack

  23. Backend Stack

  24. None
  25. GraphQL GraphQL (ruby) Apollo React Ruby on Rails Business Logic

    Business Logic frontend backend
  26. None
  27. None
  28. GraphQL GraphQL (ruby) Apollo React Ruby on Rails Business Logic

    Business Logic frontend backend Custom Utilities
  29. Library Your Utilities Application

  30. https://www.apollographql.com/

  31. https://relay.dev

  32. Library Specification for how to design you GraphQL schema https://relay.dev

  33. Library Specification for how to design you GraphQL schema https://relay.dev/docs/en/graphql-server-specification.html

  34. Node interface Connections Mutations GraphQL Relay Specification https://relay.dev/docs/en/graphql-server-specification.html

  35. Node interface Connections Mutations GraphQL Relay Specification https://relay.dev/docs/en/graphql-server-specification.html

  36. Connections Mutations GraphQL Relay Specification https://relay.dev/docs/en/graphql-server-specification.html

  37. Connections Mutations GraphQL Relay Specification https://relay.dev/docs/en/graphql-server-specification.html

  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
  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
  40. Connections + Total Count query Name($cursor: String) { someField(first: 20,

    after: $cursor) {
 totalCount
 edges { cursor
 node {
 ...Node
 }
 }
 pageInfo {
 hasNextPage hasPreviousPage startCursor endCursor
 } } }
  41. None
  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} `;
  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} `;
  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} `;
  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> ); }, });
  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: );
  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> ); }, });
  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>
  49. How many SQL queries are performed here?

  50. How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 1 1 1
  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
  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
  53. None
  54. [{ id: 1, isLiked: true }, { id: 2, isLiked:

    false }, { id: 3, isLiked: false }]
  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}" }]
  56. gem "graphql-batch"

  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)] }]
  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
  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
  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
  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
  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
  63. None
  64. How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 + N * 3
  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
  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
  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}
  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}
  69. https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb https://gist.github.com/RStankov/48070003a31d71a66f57a237e27d5865

  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
  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
  72. How many SQL queries are performed here?

  73. How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 + N * 3
  74. How many SQL queries are performed here? 1 1 ‼

  75. http://blog.rstankov.com/dealing-with-n-1-with-graphql-part-1 http://blog.rstankov.com/dealing-with-n-1-in-graphql-part-2

  76. Connections Mutations GraphQL Relay Specification https://relay.dev/docs/en/graphql-server-specification.html

  77. None
  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
  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
  80. mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations
  81. mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations
  82. [namespace][object][action] Mutation Naming Convention

  83. [namespace][object][action]
 
 BookmarkCreate
 BookmarkDestroy
 ProductLikeCreate
 ProductLikeDestroy TeamInviteAccept TeamInviteCreate TeamInviteDecline
 UserSettingsUpdate

    Mutation Naming Convention
  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
  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
  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
  87. None
  88. import gql from 'graphql-tag'; export default gql` fragment ProductLikeButtonFragment on

    Product { id isLiked likesCount } `; ~/components/ProductLikeButton/Fragment.ts
  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
  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
  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={...} />
  92. None
  93. None
  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>
  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
  96. Conclusion

  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 [...]
  98. None
  99. None
  100. https://speakerdeck.com/rstankov Thanks