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

3 Years GraphQL at Product Hunt

3 Years GraphQL at Product Hunt

Radoslav Stankov

February 21, 2020
Tweet

More Decks by Radoslav Stankov

Other Decks in Programming

Transcript

  1. 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
  2. GraphQL GraphQL (ruby) Apollo React Ruby on Rails Business Logic

    Business Logic frontend backend Custom Utilities
  3. 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
  4. 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
  5. Connections + Total Count query Name($cursor: String) { someField(first: 20,

    after: $cursor) {
 totalCount
 edges { cursor
 node {
 ...Node
 }
 }
 pageInfo {
 hasNextPage hasPreviousPage startCursor endCursor
 } } }
  6. 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} `;
  7. 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} `;
  8. 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} `;
  9. 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> ); }, });
  10. 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: );
  11. 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> ); }, });
  12. knows how to map connection knows how to read "pageInfo"

    fetches more pages type safe InfiniteScroll <InfiniteScroll connection={...} connectionPath={...} cursorName={...} fetchMore={...} variables={...}> {(item) => ...} </Form>
  13. 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
  14. 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
  15. [{ id: 1, isLiked: true }, { id: 2, isLiked:

    false }, { id: 3, isLiked: false }]
  16. [{ 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}" }]
  17. 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)] }]
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 + N * 3
  24. 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
  25. 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
  26. 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}
  27. 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}
  28. 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
  29. 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
  30. How many SQL queries are performed here? 1 1 1

    1 1 1 1 1 1 1 1 1 1 1 + N * 3
  31. 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
  32. 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
  33. mutation ProductLikeCreate($input: ProductLikeCreateInput!) { productLikeCreate(input: $input) { node { id

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

    ...LikeButtonFragment } errors { name messages } } } Mutations Mutations
  35. 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
  36. 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
  37. 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
  38. import gql from 'graphql-tag'; export default gql` fragment ProductLikeButtonFragment on

    Product { id isLiked likesCount } `; ~/components/ProductLikeButton/Fragment.ts
  39. 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
  40. 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
  41. 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={...} />
  42. <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>
  43. 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
  44. 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 [...]