$30 off During Our Annual Pro Sale. View Details »

How to introduce GraphQL to an existing React-Redux application

How to introduce GraphQL to an existing React-Redux application

Fumiaki MATSUSHIMA

December 06, 2019
Tweet

More Decks by Fumiaki MATSUSHIMA

Other Decks in Programming

Transcript

  1. #GraphQLTokyo
    How to introduce GraphQL to an
    existing React-Redux application
    @mtsmfm
    Fumiaki Matsushima
    GraphQL Tokyo Meetup #9

    View Slide

  2. #GraphQLTokyo
    ➔ Fumiaki Matsushima
    ➔ Web Dev at Quipper
    ➔ Ruby, Board game
    ➔ GraphQL Tokyo organizer
    @mtsmfm.inspect

    View Slide

  3. #GraphQLTokyo
    https://studysapuri.jp/

    View Slide

  4. #GraphQLTokyo 4
    https://trends.google.com/trends/explore?date=today%205-y&q=graphql
    GraphQL is still not popular in Japan

    View Slide

  5. #GraphQLTokyo
    Experience in Production
    is important

    View Slide

  6. #GraphQLTokyo
    Introduce GraphQL to solve
    existing problems

    View Slide

  7. #GraphQLTokyo
    What I want to tell you in this talk
    7
    - It’s not difficult to introduce GraphQL partially
    - You may not need GraphQL client library
    - You don’t have to replace your existing client store

    View Slide

  8. #GraphQLTokyo
    Agenda
    8
    1. About our existing application
    2. Policies and codes
    3. Result

    View Slide

  9. #GraphQLTokyo
    Our existing application
    9
    - Frontend: SPA (React + Redux)
    - Backend: Rails

    View Slide

  10. #GraphQLTokyo
    Mainly I wanted to solve
    backend problems by
    GraphQL

    View Slide

  11. #GraphQLTokyo
    Problems
    11
    - Typing
    - There’s no schema stuff for our API
    - We need to write a type definition manually
    - [Backend] Similar classes to represent the
    same resource
    - UserDetail, UserLite, ...
    - [Backend] Messy logics to solve N+1 problem

    View Slide

  12. #GraphQLTokyo 12
    GET /users … List all users
    GET /activities … List all activities
    User
    Activity Profile
    1
    * 1 1

    View Slide

  13. #GraphQLTokyo
    Similar classes to represent the same resource
    13
    class UserDetailEntity < UserLiteEntity
    def as_json
    super.merge(
    email: @user.email
    )
    end
    end
    class ActivityEntity
    class User < UserLite
    def as_json
    super.merge(
    avatar_url: @user.avatar_url
    )
    end
    end
    def as_json
    {
    id: @activity.id,
    user: User.new(@activity.user).as_json
    }
    end
    end
    class UserLiteEntity
    def as_json
    {
    id: @user.id,
    first_name: @user.profile.first_name,
    last_name: @user.profile.last_name,
    }
    end
    end

    View Slide

  14. #GraphQLTokyo
    Similar classes to represent the same resource
    14
    class UserLiteEntity
    def as_json
    {
    id: @user.id,
    first_name: @user.profile.first_name,
    last_name: @user.profile.last_name,
    }
    end
    end
    class UserDetailEntity < UserLiteEntity
    def as_json
    super.merge(
    email: @user.email
    )
    end
    end
    class ActivityEntity
    class User < UserLite
    def as_json
    super.merge(
    avatar_url: @user.avatar_url
    )
    end
    end
    def as_json
    {
    id: @activity.id,
    user: User.new(@activity.user).as_json
    }
    end
    end
    N + 1
    N + 1

    View Slide

  15. #GraphQLTokyo
    Messy logics to solve N+1 problem
    15
    class UserLiteEntity
    def as_json
    {
    id: @user.id,
    first_name: @user.profile.first_name,
    last_name: @user.profile.last_name
    }
    end
    end
    class UserLiteEntity
    def initialize(user, profile)
    @user = user
    @profile = profile
    end
    def as_json
    {
    id: @user.id,
    first_name: @profile.first_name,
    last_name: @profile.last_name
    }
    end
    end

    View Slide

  16. #GraphQLTokyo
    Messy logics to solve N+1 problem
    16
    class ActivityEntity
    def initialize(activity)
    @activity = activity
    end
    def as_json
    {
    id: @activity.id,
    user: User.new(@user).as_json
    }
    end
    end
    class ActivityEntity
    def initialize(activity, user, profile)
    @activity = activity
    @user = user
    @profile = profile
    end
    def as_json
    {
    id: @activity.id,
    user: User.new(@user, @profile).as_json
    }
    end
    end

    View Slide

  17. #GraphQLTokyo
    Messy logics to solve N+1 problem
    17
    activities = Activity.all
    users_cache = User.where(activity_id: activities.ids).index_by(&:id)
    profiles_cache = Profile.where(user_id: users_cache.keys).index_by(&:user_id)
    render json: activities.map {|a|
    ActivityEntity.new(a, users_cache[a.user_id], profiles_cache[a.user_id]).as_json
    }.as_json
    /users
    /activities
    users = User.all
    profiles_cache = Profile.where(user_id: users.ids).index_by(&:user_id)
    render json: users.map {|u|
    UserDetailEntity.new(u, profiles_cache[u.id]).as_json
    }.as_json

    View Slide

  18. #GraphQLTokyo
    It’ll be like this if we can use ActiveRecord gem
    though, we can’t because of Mongo
    18
    class UserLiteEntity
    def as_json
    {
    id: @user.id,
    first_name: @user.profile.first_name,
    last_name: @user.profile.last_name,
    }
    end
    end
    users = User.all.includes(:profile)
    render json: users.map {|u|
    UserDetailEntity.new(u).as_json
    }.as_json
    Microservices also face this problem
    `includes(:profile)` executes
    SELECT * FROM profiles
    and sets to user.profile

    View Slide

  19. #GraphQLTokyo
    Messy logics to solve N+1 problem
    19
    activities = Activity.all
    users_cache = User.where(activity_id: activities.ids).index_by(&:id)
    profiles_cache = Profile.where(user_id: users_cache.keys).index_by(&:user_id)
    render json: activities.map {|a|
    ActivityEntity.new(a, users_cache[a.user_id], profiles_cache[a.user_id]).as_json
    }.as_json
    /users
    /activities
    users = User.all
    profiles_cache = Profile.where(user_id: users.ids).index_by(&:user_id)
    render json: users.map {|u|
    UserDetailEntity.new(u, profiles_cache[u.id]).as_json
    }.as_json
    We need to know all relationships to
    prepare cache
    e.g. Activity - User - Profile

    View Slide

  20. #GraphQLTokyo
    GraphQL can solve them
    20
    - Typing
    - It provides typing out of the box
    - [Backend] Similar classes to represent the
    same resource
    - Declarative entity classes and lazy loading
    - [Backend] Messy logics to solve N+1 problem
    - Loader

    View Slide

  21. #GraphQLTokyo
    Declarative entity classes and lazy loading
    21
    module Types
    class UserType < Types::BaseObject
    field :id, ID, null: false
    field :first_name, String, null: false
    field :last_name, String, null: false
    field :email, String, null: false
    field :avatar_url, String, null: false
    end
    end
    query Users {
    users {
    id, email
    }
    }
    class UserLiteEntity
    def as_json
    {
    id: @user.id,
    first_name: @user.profile.first_name,
    last_name: @user.profile.last_name,
    }
    end
    end
    class UserDetailEntity < UserLiteEntity
    def as_json
    super.merge(
    email: @user.email
    )
    end
    end

    View Slide

  22. #GraphQLTokyo
    Loader
    22
    class RecordLoader < GraphQL::Batch::Loader
    def initialize(model, column)
    @model = model
    @column = column
    end
    def perform(ids)
    @model.where(column => ids).each { |record| fulfill(record[column], record) }
    ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
    end
    end

    View Slide

  23. #GraphQLTokyo
    Loader
    23
    module Types
    class UserType < Types::BaseObject
    ...
    field :first_name, String, null: false
    def first_name
    RecordLoader.for(Profile, 'user_id').load(object.id)
    end
    end
    end module Types
    class ActivityType < Types::BaseObject
    ...
    field :user, Types::User, null: false
    def user
    RecordLoader.for(User, 'id').load(object.user_id)
    end
    end
    end
    Parent knows how to load children

    View Slide

  24. #GraphQLTokyo
    Agenda
    24
    1. About our existing application
    2. Policies and codes
    3. Result

    View Slide

  25. #GraphQLTokyo
    Our policies
    25
    - Try GraphQL for only one page for now
    - We have only one variation of query

    View Slide

  26. #GraphQLTokyo
    Our policies
    26
    - Try GraphQL for only one page for now
    - We have only one variation of query
    - Avoid using client library
    - We already have client data store, Redux

    View Slide

  27. #GraphQLTokyo 27

    View Slide

  28. #GraphQLTokyo
    Why do we avoid GraphQL client?
    28
    - GraphQL client is mainly for cache
    - We already use Redux for such a case
    - GraphQL itself doesn’t require complex client
    library
    - Fetch API is enough!

    View Slide

  29. #GraphQLTokyo
    Our policies
    29
    - Try GraphQL for only one page for now
    - We have only one variation of query
    - Avoid using client library
    - We already have client data store, Redux
    - Use simple tool to generate TypeScript type
    definition
    - https://graphql-code-generator.com

    View Slide

  30. #GraphQLTokyo 30
    https://graphql-code-generator.com/

    View Slide

  31. #GraphQLTokyo
    Backend code structure
    31
    - app/graphql/types/activity_type.rb
    - app/graphql/types/user_type.rb

    View Slide

  32. #GraphQLTokyo
    Frontend code structure
    32
    - actions/activities.ts
    - actions/activities.graphql
    - components/activities.tsx
    - interfaces/graphql.ts
    - reducers/activities.ts

    View Slide

  33. #GraphQLTokyo
    Typing!
    33
    query Activities {
    activities {
    id
    user {
    id, email
    }
    }
    }
    export type ActivitiesQuery = { __typename?: 'Query' } & {
    activities: Array<
    Maybe<
    { __typename?: 'Activity' } & Pick & {
    user: { __typename?: 'User' } & Pick;
    }
    >
    >;
    };
    interfaces/graphql.ts
    generates:
    interfaces/graphql.ts:
    plugins:
    - 'typescript'
    - 'typescript-operations'

    View Slide

  34. #GraphQLTokyo
    Frontend code structure
    34
    query Activities {
    activities {
    id
    user {
    id, email
    }
    }
    }
    actions/activities.graphql
    import query from './activities.graphql';
    const fetchActivitiesAction = createAsync('FETCH_ACTIVITIES', async () => {
    (await (await fetch('/graphql', {
    method: 'POST',
    body: JSON.stringify({ query }),
    })).json()).data as ActivitiesQuery;
    });
    actions/activities.ts

    View Slide

  35. #GraphQLTokyo 35
    reducerWithInitialState({
    data: {},
    isFetching: false,
    hasError: false,
    })
    .case(fetchActivitiesAction.async.started, state => ({
    ...state,
    isFetching: true,
    }))
    .case(fetchActivitiesAction.async.failed, state => ({
    ...state,
    isFetching: false,
    hasError: true,
    }))
    .case(fetchActivitiesAction.async.done, (state, { result }) => ({
    ...state,
    isFetching: false,
    hasError: false,
    data: result.data,
    }));
    reducers/activities.ts

    View Slide

  36. #GraphQLTokyo 36
    const Activities = connect(
    ({ activities }) => activities.data,
    { fetchActivities: fetchActivitiesAction.action },
    )(({ fetchActivities, activities }) => {
    useEffect(fetchActivities, []);
    return (
    <>
    {activities.map(a => (

    ))}
    >
    );
    });
    components/activities.tsx

    View Slide

  37. #GraphQLTokyo
    Agenda
    37
    1. About our existing application
    2. Policies and codes
    3. Result

    View Slide

  38. #GraphQLTokyo
    Result
    38
    - Frontend
    - Generating type definition by CLI is very useful
    - API schema makes easier to work team members in
    parallel
    - Backend
    - Much structured than before
    - There’s some confusions about new things
    especially “Loader”
    - We need more types/queries to get more merits

    View Slide

  39. #GraphQLTokyo
    Conclusion
    39
    - It’s not difficult to introduce GraphQL partially
    - You may not need GraphQL client library
    - You don’t have to replace your existing client store

    View Slide

  40. #GraphQLTokyo
    Conclusion
    40
    - It’s not difficult to introduce GraphQL partially
    - You may not need GraphQL client library
    - You don’t have to replace your existing client store
    - Have a discussion with your team to introduce
    new stuff

    View Slide