React Architecture in Product Hunt

React Architecture in Product Hunt

Talk was given at React Alicante 2019 (https://reactalicante.es)

Video of the talk 👉 https://www.youtube.com/watch?v=bo0u9402OXU

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

September 24, 2019
Tweet

Transcript

  1. September 2019 React Architecture In Product Hunt Radoslav Stankov

  2. Thanks to our sponsors!

  3. Radoslav Stankov @rstankov blog.rstankov.com github.com/rstankov
 twitter.com/rstankov

  4. None
  5. None
  6. Architecture

  7. None
  8. None
  9. None
  10. 
 Engineering Team ! 


  11. 
 " Architecture History 


  12. 
 " Architecture History 
 2014

  13. 
 " Architecture History 
 2014 2015

  14. 
 " Architecture History 
 2014 2015 2016

  15. 
 " Architecture History 
 2014 2015 2016 2017

  16. 
 " Architecture History 
 2014 2019 2015 2016 2017

  17. None
  18. None
  19. None
  20. None
  21. 
 # Frontend Tech Stack 


  22. 
 # Frontend Tech Stack 
 $

  23. 
 # Frontend Tech Stack 
 $

  24. 
 # Frontend Tech Stack 
 $

  25. 
 # Frontend Tech Stack 
 $

  26. 
 # Frontend Tech Stack 
 %

  27. 
 # Frontend Tech Stack 


  28. 
 # Frontend Tech Stack 


  29. None
  30. None
  31. None
  32. None
  33. & Have good defaults ' Have good code organization (

    Make common operations easy ) Isolate dependencies * Extensibility and reusability
  34. & Have good defaults ' Have good code organization (

    Make common operations easy ) Isolate dependencies * Extensibility and reusability
  35. None
  36. None
  37. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  38. 2 1 3 Support Components Pages

  39. Support Components Pages

  40. 1) Support 2) Components 3) Pages

  41. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  42. 1 Support components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/

    styles/ types/ utils/ config.ts paths.ts
  43. 1 Support Components components/ graphql/ hooks/ layouts/ pages/ routes/ server/

    static/ styles/ types/ utils/ config.ts paths.ts 2
  44. 1 Support Components Pages components/ graphql/ hooks/ layouts/ pages/ routes/

    server/ static/ styles/ types/ utils/ config.ts paths.ts 2 3
  45. 1 Support

  46. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  47. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  48. import getNextConfig from 'next/config'; const config = getNextConfig() as any;

    // ... other configuration export const environment = { isTest: config.publicRuntimeConfig.NODE_ENV === 'test', isProduction: config.publicRuntimeConfig.NODE_ENV === 'production', isBrowser: !!process.browser, }; 
 config.ts

  49. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  50. apollo client:codegen \ --localSchemaFile="graphql/schema.json" \ --addTypename \ --tagName=gql \ --target=typescript

    \ --includes="{components,routes,utils,hooks,layouts}/**/*.{tsx,ts}" \ --outputFlat="graphql/types.ts"
  51. None
  52. 
 components/Profile/Avatar/Fragment.ts
 import gql from 'graphql-tag'; export default gql` fragment

    ProfileAvatarFragment on Profile { id name kind imageUrl } `;
  53. // ==================================================== // GraphQL fragment: ProfileAvatarFragment // ==================================================== export interface

    ProfileAvatarFragment { __typename: "Profile"; id: string; name: string; kind: string; imageUrl: string | null; } 
 graphql/types.ts

  54. import { ProfileAvatarFragment } from '~/graphql/types';

  55. None
  56. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  57. None
  58. useKey() useIsMounted() useTimeout()
 
 useHideOnTop()

  59. useGraphQLFragment() useViewier() useIsLoggedIn()

  60. useGraphQLFragment() useViewier() useIsLoggedIn()

  61. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  62. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  63. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  64. None
  65. import formatCount from './formatCount'; describe(formatCount.name, () => { it('does proper

    pluralization', () => { expect(formatCount(0, 'item')).toEqual('0 items'); expect(formatCount(1, 'item')).toEqual('1 item'); expect(formatCount(10, 'items')).toEqual('10 items'); }); it('formats 1000-9999 as Ks', () => { expect(formatCount(1000)).toEqual('1K'); expect(formatCount(1500)).toEqual('1.5K'); }); // .... }); 
 utils/formatCount.test.ts

  66. import { format } from 'date-fns'; export function formatDateTime(date: string)

    { return format(date, 'H:mm A · MMM D, YYYY'); } 
 utils/date.ts

  67. moment date.ts Component Page

  68. date.ts Component Page date-fns

  69. utils/ external/ Intercom/ OneSignal/ Segment/ Sentry/

  70. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  71. 2 Components

  72. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  73. None
  74. 
 + Component as directory 
 components/ Component/ SubComponent/ Fragment.graphql

    Mutation.graphql icon.svg index.js styles.css utils.js
  75. None
  76. import * as React from "react"; import Font from "components/Font";

    <Font.Text>{text}</Font.Text>
  77. import * as React from "react"; import styles from "./styles.css";

    export function Text({ children }) { return ( <span className={styles.text}> {children} </span> ); } 
 components/Font/index.tsx

  78. CSS // style.css .text { font-size: 14px; } CSS //

    component.js
 import styles from './styles.css'; <span className={styles.text}> </span> CSS // style.css .text_3mRwv { font-size: 14px; } CSS // result.js <span class="text_3mRwv"> </span>

  79. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span>
  80. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span> <Font.Text component="p">text</Font.Text> // -> <p class="text">text</p>
  81. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span> <Font.Text component="p">text</Font.Text> // -> <p class="text">text</p> <Font.Text component={Link} to="/page">text</Font.Text> // -> <a class="text" href="/page">text</a>
  82. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span> <Font.Text component="p">text</Font.Text> // -> <p class="text">text</p> <Font.Text component={Link} to="/page">text</Font.Text> // -> <a class="text" href="/page">text</a> 
 + Pass custom component as prop 

  83. import * as React from "react"; import styles from "./styles.css";

    export function Text({ component, children, ...props }) { Component = component || "span";
 return ( <Component className={styles.text} {...props}> {children} </Component> ); } 
 + Pass custom component as prop 

  84. import * as React from "react"; import styles from "./styles.css";

    export function Text({ component, children, ...props }) { Component = component || "span";
 return ( <Component className={styles.text} {...props}> {children} </Component> ); } $ 
 + Pass custom component as prop 

  85. import * as React from "react"; import styles from "./styles.css";

    export function Text({ component, ...props }) { Component = component || "span"; return <Component className={styles.text} {...props} />; } , 
 + Pass custom component as prop 

  86. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">Text</span>
  87. import * as React from "react"; import Font from "components/Font";

    import styles from "./styles.css"; <Font.Text className={styles.custom}>text</Font.Text> // -> <span class="text custom">text</span>
  88. import * as React from "react"; import Font from "components/Font";

    import styles from "./styles.css"; <Font.Text className={styles.custom}>text</Font.Text> // -> <span class="text custom">text</span> 
 + Pass extra class name 

  89. import * as React from "react"; import styles from "./styles.css";

    import classNames from "classnames"; export function Text({ component, className, ...props }) { Component = component || "span";
 return <Component className={className(styles.text, className)} {...props} />; } 
 + Pass extra class name 

  90. yarn install "classnames"

  91. None
  92. None
  93. <Form.Mutation mutation={MUTATION} onSubmit={onComplete}> <Form.Field name="title" /> <Form.Field name="email" control="email" />

    <Form.Field name="tagline" control="textarea" /> <Form.Field name="category" control="select" options={CATEGORIES} /> <Form.Field name="photos" control={PhotosInput} />
 <Form.Field name="topics" control={TopicsInput} /> <Form.Submit /> </Form.Mutation>
  94. Input

  95. Input Loading

  96. Input Loading Success

  97. Input Loading Success Errors

  98. Submit Server Success Errors

  99. remoteCall Server { result: 'ok' } { errors: {…} }

  100. remoteCall Server { result: 'ok' } { 
 errors: {

    field1: 'error1',
 field2: 'error2'
 }
 }
  101. { 
 errors: { field1: 'error1',
 field2: 'error2'
 }
 }

  102. { 
 errors: { field1: 'error1',
 field2: 'error2'
 }
 }

  103. mutation UserSettingsUpdate($input: UserSettingsUpdateInput!) { response: userSettingsUpdate(input: $input) { node {

    id ...MySettingsPageViewer } errors { field message } } }
  104. mutation UserSettingsUpdate($input: UserSettingsUpdateInput!) { response: userSettingsUpdate(input: $input) { node {

    id ...MySettingsPageViewer } errors { field message } } }
  105. <Form.Mutation mutation={MUTATION} onSubmit={onComplete}> <Form.Field name="title" /> <Form.Field name="email" control="email" />

    <Form.Field name="tagline" control="textarea" /> <Form.Field name="category" control="select" options={CATEGORIES} /> <Form.Field name="photos" control={PhotosInput} />
 <Form.Field name="topics" control={TopicsInput} /> <Form.Submit /> </Form.Mutation>
  106. None
  107. import { TopicSearch_Topic as ITopic } from '~/graphql/types'; import ItemList

    from './ItemList' import QUERY from './Query'; import SearchResultItem from './SearchResultItem' import { IFormArrayFields } from '~/components/Form/types'; import { useLoadTopicsIds } from './utils'; interface IProps { fields: IFormArrayFields<string>; } export default function TopicsInput({ fields }: IProps) { const addTopic = (topic: ITopic) => { fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) => { fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields);
  108. import { TopicSearch_Topic as ITopic } from '~/graphql/types'; import ItemList

    from './ItemList' import QUERY from './Query'; import SearchResultItem from './SearchResultItem' import { IFormArrayFields } from '~/components/Form/types'; import { useLoadTopicsIds } from './utils'; interface IProps { fields: IFormArrayFields<string>; } export default function TopicsInput({ fields }: IProps) { const addTopic = (topic: ITopic) => { fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) => { fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields);
  109. import { TopicSearch_Topic as ITopic } from '~/graphql/types'; import ItemList

    from './ItemList' import QUERY from './Query'; import SearchResultItem from './SearchResultItem' import { IFormArrayFields } from '~/components/Form/types'; import { useLoadTopicsIds } from './utils'; interface IProps { fields: IFormArrayFields<string>; } export default function TopicsInput({ fields }: IProps) { const addTopic = (topic: ITopic) => { fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) => { fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields);
  110. import { TopicSearch_Topic as ITopic } from '~/graphql/types'; import ItemList

    from './ItemList' import QUERY from './Query'; import SearchResultItem from './SearchResultItem' import { IFormArrayFields } from '~/components/Form/types'; import { useLoadTopicsIds } from './utils'; interface IProps { fields: IFormArrayFields<string>; } export default function TopicsInput({ fields }: IProps) { const addTopic = (topic: ITopic) => { fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) => { fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields);
  111. import { TopicSearch_Topic as ITopic } from '~/graphql/types'; import ItemList

    from './ItemList' import QUERY from './Query'; import SearchResultItem from './SearchResultItem' import { IFormArrayFields } from '~/components/Form/types'; import { useLoadTopicsIds } from './utils'; interface IProps { fields: IFormArrayFields<string>; } export default function TopicsInput({ fields }: IProps) { const addTopic = (topic: ITopic) => { fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) => { fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields);
  112. import { TopicSearch_Topic as ITopic } from '~/graphql/types'; import ItemList

    from './ItemList' import QUERY from './Query'; import SearchResultItem from './SearchResultItem' import { IFormArrayFields } from '~/components/Form/types'; import { useLoadTopicsIds } from './utils'; interface IProps { fields: IFormArrayFields<string>; } export default function TopicsInput({ fields }: IProps) { const addTopic = (topic: ITopic) => { fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) => { fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields);
  113. fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields); if (topics === null)

    { return null; } return ( <React.Fragment> <SearchInput itemsPath="topics" onSelect={addTopic} query={QUERY} renderItem={SearchResultItem} /> <ItemList topics={topics} removeTopic={removeTopic} /> </React.Fragment> ); } ProductsInput.isArray = true;
  114. fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields); if (topics === null)

    { return null; } return ( <React.Fragment> <SearchInput itemsPath="topics" onSelect={addTopic} query={QUERY} renderItem={SearchResultItem} /> <ItemList topics={topics} removeTopic={removeTopic} /> </React.Fragment> ); } ProductsInput.isArray = true;
  115. fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields); if (topics === null)

    { return null; } return ( <React.Fragment> <SearchInput itemsPath="topics" onSelect={addTopic} query={QUERY} renderItem={SearchResultItem} /> <ItemList topics={topics} removeTopic={removeTopic} /> </React.Fragment> ); } ProductsInput.isArray = true;
  116. fields.remove(fields.value.indexOf(topic.id)); } const topics = useLoadTopicsIds(fields); if (topics === null)

    { return null; } return ( <React.Fragment> <SearchInput itemsPath="topics" onSelect={addTopic} query={QUERY} renderItem={SearchResultItem} /> <ItemList topics={topics} removeTopic={removeTopic} /> </React.Fragment> ); } ProductsInput.isArray = true;
  117. None
  118. Which is the form library we are using?!

  119. None
  120. None
  121. -

  122. -

  123. -

  124. . unified styles / common interface 0 custom inputs
 1

    understand backend GraphQL 
 2 Form 

  125. None
  126. <Button href={paths.profile(profile)} />

  127. <Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} loadingText="Waiting for promise to resolve"

    />
  128. <Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} loadingText="Waiting for promise to resolve"

    /> <Button confirm="Are you sure?" mutation={DELETE_MUTATION} input={{ id }} loadingText="Deleting..." onMutate={redirectSomeWhere} />
  129. <Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} loadingText="Waiting for promise to resolve"

    /> <Button confirm="Are you sure?" mutation={DELETE_MUTATION} input={{ id }} loadingText="Deleting..." onMutate={redirectSomeWhere} />
  130. <Button.Bordered {...props} /> <Button.Solid {...props} /> <Button.White {...props} /> <Button.Text

    {...props} /> <Button.Icon {...props} />
  131. Utility Styling Domain

  132. None
  133. None
  134. LikeButton Button

  135. 
 Domain Component


  136. AnswerCard Like Button

  137. <AnswerCard>
 <Box> <Card thumbnail={<ProfileAvatar profile={answer.profile}} name={answer.profile.name} subtitle="recommendations for" /> <Font.Title>{answer.question.title}</Font.Title>

    <Flex.Grid> {answer.products.map((product) => ( <Card url={paths.profile.show(product)} thumbnail={<ProfileAvatar profile={product}} name={product.name} /> ))} </Flex> <LikeButton subject={answer} /> <DateFormat date={answer.createdAt} /> </Box> </AnswerCard> 
 Domain Component

  138. 
 Domain Component
 <AnswerCard> <Box>
 <Card /> <Font.Title /> <Flex.Grid>

    <Card /> </Flex> <LikeButton /> <DateFormat />
 </Box> </AnswerCard>
  139. Atomic Design

  140. ...Kinda 3

  141. 4 generic components 0 domain components

  142. 3 Pages

  143. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  144. export default { root: () => '/', static: { about()

    => '/about', // ... } profiles: { people: () => '/people', show: ({ slug }: { slug: string }) => `/@${slug}`, // ... }, // ... }; 
 path.ts

  145. import paths from 'ph/paths'; 
 paths.root(); // => / paths.static.about();

    // => /about/ paths.profiles.people(); // => /people paths.profiles.show(profile); // => /@rstankov
  146. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  147. None
  148. None
  149. None
  150. <Layout> <Header profile={profile} /> <ContentWithMobileMenu> {children} </ContentWithMobileMenu> <Footer /> <CookiePolicyBanner

    /> </Layout> 
 layouts/Main/index.tsx

  151. None
  152. <MainLayout> <ProfileHeader profile={profile} /> <div> <ProfileMenu /> {content} </div> </MainLayout>

    
 layouts/Main/index.tsx

  153. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  154. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts $
  155. None
  156. 
 pages/profiles/[slug]/index.ts


  157. import page from '~/routes/profiles/show'; export default page; 
 pages/profiles/[slug]/index.ts


  158. import page from '~/routes/profiles/show'; export default page; 
 pages/profiles/[slug]/index.ts
 5

  159. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  160. 
 Component as directory
 routing/ profiles/ show/ SubComponent1/ SubComponent2/ Query.graphql

    index.js styles.css utils.js
  161. Loading State 
 Page Life Cycle


  162. Loading State 
 Page Life Cycle


  163. Loading State Error State 
 Page Life Cycle


  164. Loading State Not Found Error Server Error Authorization Error Authentication

    Error Error State 
 Page Life Cycle

  165. Loading State Not Found Error Server Error Authorization Error Authentication

    Error Error State Loaded State 
 Page Life Cycle

  166. Loading State Not Found Error Server Error Authorization Error Authentication

    Error SEO Error State Analytics Loaded State 
 Page Life Cycle
 ???
  167. Loading State Not Found Error Server Error Authorization Error Authentication

    Error SEO Error State Analytics Loaded State render 
 Page Life Cycle
 ???
  168. import createPage from '~/utils/createPage'; import ProfileLayout from '~/layouts/Profile'; import {

    ProfileShowPage } from '~/graphql/types'; import QUERY from './Query'; export default createPage<ProfileShowPage>({ query: QUERY, queryVariables: ({ slug }) => ({ slug }), requireLogin: true, requirePermissions: ({ profile }) => profile.canManage, requireFeature: 'feature_flag', tags: ({ profile }) => profile.seoTags, title: ({ profile }) => profile.name, component: ({ data: { profile } }) => ( <ProfileLayout profile={profile}>{...}</ProfileLayout> ), });
  169. import createPage from '~/utils/createPage'; import ProfileLayout from '~/layouts/Profile'; import {

    ProfileShowPage } from '~/graphql/types'; import QUERY from './Query'; export default createPage<ProfileShowPage>({ query: QUERY, queryVariables: ({ slug }) => ({ slug }), requireLogin: true, requirePermissions: ({ profile }) => profile.canManage, requireFeature: 'feature_flag', tags: ({ profile }) => profile.seoTags, title: ({ profile }) => profile.name, component: ({ data: { profile } }) => ( <ProfileLayout profile={profile}>{...}</ProfileLayout> ), });
  170. import createPage from '~/utils/createPage'; import ProfileLayout from '~/layouts/Profile'; import {

    ProfileShowPage } from '~/graphql/types'; import QUERY from './Query'; export default createPage<ProfileShowPage>({ query: QUERY, queryVariables: ({ slug }) => ({ slug }), requireLogin: true, requirePermissions: ({ profile }) => profile.canManage, requireFeature: 'feature_flag', tags: ({ profile }) => profile.seoTags, title: ({ profile }) => profile.name, component: ({ data: { profile } }) => ( <ProfileLayout profile={profile}>{...}</ProfileLayout> ), });
  171. import createPage from '~/utils/createPage'; import ProfileLayout from '~/layouts/Profile'; import {

    ProfileShowPage } from '~/graphql/types'; import QUERY from './Query'; export default createPage<ProfileShowPage>({ query: QUERY, queryVariables: ({ slug }) => ({ slug }), requireLogin: true, requirePermissions: ({ profile }) => profile.canManage, requireFeature: 'feature_flag', tags: ({ profile }) => profile.seoTags, title: ({ profile }) => profile.name, component: ({ data: { profile } }) => ( <ProfileLayout profile={profile}>{...}</ProfileLayout> ), });
  172. import createPage from '~/utils/createPage'; import ProfileLayout from '~/layouts/Profile'; import {

    ProfileShowPage } from '~/graphql/types'; import QUERY from './Query'; export default createPage<ProfileShowPage>({ query: QUERY, queryVariables: ({ slug }) => ({ slug }), requireLogin: true, requirePermissions: ({ profile }) => profile.canManage, requireFeature: 'feature_flag', tags: ({ profile }) => profile.seoTags, title: ({ profile }) => profile.name, component: ({ data: { profile } }) => ( <ProfileLayout profile={profile}>{...}</ProfileLayout> ), });
  173. import createPage from '~/utils/createPage'; import ProfileLayout from '~/layouts/Profile'; import {

    ProfileShowPage } from '~/graphql/types'; import QUERY from './Query'; export default createPage<ProfileShowPage>({ query: QUERY, queryVariables: ({ slug }) => ({ slug }), requireLogin: true, requirePermissions: ({ profile }) => profile.canManage, requireFeature: 'feature_flag', tags: ({ profile }) => profile.seoTags, title: ({ profile }) => profile.name, component: ({ data: { profile } }) => ( <ProfileLayout profile={profile}>{...}</ProfileLayout> ), });
  174. None
  175. 
 http://graphql.org/


  176. None
  177. ViewerFragment ProfileLayoutHeaderFragment ProfileLayout MenuFragment ProfileShowPage

  178. ViewerFragment ProfileLayout MenuFragment FollowButton Fragment Avatar Fragment ProTips Fragment Stack

    Fragment
  179. ViewerFragment ProfileLayout MenuFragment FollowButton Fragment Avatar Fragment Avatar Card LikeButton

  180. Query Fragment Fragment Fragment Fragment Fragment Fragment

  181. import gql from 'graphql-tag'; import AnswerCardWithQuestionFragment from '~/components/Answer/Card/FragmentWithQuestion'; import DoYouUseFragment

    from './DoYouUse/Fragment'; import HeadTagsFragment from '~/utils/createPage/HeadTagsFragment'; import ProfileAvatarLinkFragment from '~/components/Profile/AvatarLink/Fragment'; import ProfileLayoutFragment from '~/layouts/Profile/Fragment'; import ProfilePeopleSectionFragment from '~/components/Profile/PeopleSection/Fragment'; import RecommendedProductsFragment from './RecommendedProducts/Fragment'; import StackItemProfileFragment from '~/components/Stack/Item/Fragment'; import TipCardFragment from '~/components/Tip/Card/Fragment'; export default gql` query ProfilesShowPage($slug: String!) { profile(slug: $slug) { id answersCount canManage likedAnswersCount questionsCount tipsCount whitelisted using(first: 8) { edges { node {
  182. import TipCardFragment from '~/components/Tip/Card/Fragment'; export default gql` query ProfilesShowPage($slug: String!)

    { profile(slug: $slug) { id answersCount canManage likedAnswersCount questionsCount tipsCount whitelisted using(first: 8) { edges { node { id note profileTo { id name ...ProfileAvatarLinkFragment ...StackItemProfileFragment } } } } answers(first: 3) {
  183. peopleWithSimilarInterests(limit: 4) { edges { node { id ...ProfilePeopleSectionFragment }

    } } ...HeadTagsFragment ...ProfileLayoutFragment ...ProfilesShowDoYouUseFragment ...ProfilesShowDoYouUseFragment } } ${AnswerCardWithQuestionFragment} ${DoYouUseFragment} ${HeadTagsFragment} ${ProfileAvatarLinkFragment} ${ProfileLayoutFragment} ${ProfilePeopleSectionFragment} ${RecommendedProductsFragment} ${StackItemProfileFragment} ${TipCardFragment} `;
  184. None
  185. Recap

  186. 0 GraphQL & Components 6 isolating dependancies * directory as

    folder
 1 domain components . Pages
 / paths helper ) layouts
 ( createPage 
 7 Recap 

  187. Thanks 8

  188. 
 https://speakerdeck.com/rstankov