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

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
  2. #GraphQLTokyo ➔ Fumiaki Matsushima ➔ Web Dev at Quipper ➔

    Ruby, Board game ➔ GraphQL Tokyo organizer @mtsmfm.inspect
  3. #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
  4. #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
  5. #GraphQLTokyo 12 GET /users … List all users GET /activities

    … List all activities User Activity Profile 1 * 1 1
  6. #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
  7. #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
  8. #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
  9. #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
  10. #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
  11. #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
  12. #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
  13. #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
  14. #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
  15. #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
  16. #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
  17. #GraphQLTokyo Our policies 25 - Try GraphQL for only one

    page for now - We have only one variation of query
  18. #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
  19. #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!
  20. #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
  21. #GraphQLTokyo Frontend code structure 32 - actions/activities.ts - actions/activities.graphql -

    components/activities.tsx - interfaces/graphql.ts - reducers/activities.ts
  22. #GraphQLTokyo Typing! 33 query Activities { activities { id user

    { id, email } } } export type ActivitiesQuery = { __typename?: 'Query' } & { activities: Array< Maybe< { __typename?: 'Activity' } & Pick<Activity, 'id'> & { user: { __typename?: 'User' } & Pick<User, 'id' | 'email'>; } > >; }; interfaces/graphql.ts generates: interfaces/graphql.ts: plugins: - 'typescript' - 'typescript-operations'
  23. #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
  24. #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
  25. #GraphQLTokyo 36 const Activities = connect( ({ activities }) =>

    activities.data, { fetchActivities: fetchActivitiesAction.action }, )(({ fetchActivities, activities }) => { useEffect(fetchActivities, []); return ( <> {activities.map(a => ( <Activity activity={a} key={a.id} /> ))} </> ); }); components/activities.tsx
  26. #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
  27. #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
  28. #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