React Architecture at Product Hunt

7a0e72a6f55811246bb5d9a946fd2e49?s=47 Radoslav Stankov
September 12, 2020
240

React Architecture at Product Hunt

What is the architecture at our latest product YourStack.

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

September 12, 2020
Tweet

Transcript

  1. Product Hunt React Architecture Radoslav Stankov 15/09/2020

  2. !

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

  4. None
  5. None
  6. Architecture

  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. " Frontend Tech Stack

  14. None
  15. None
  16. None
  17. None
  18. # Have good defaults $ Have good code organization %

    Make common operations easy & Isolate dependencies ' Extensibility and reusability
  19. # Have good defaults $ Have good code organization %

    Make common operations easy & Isolate dependencies ' Extensibility and reusability
  20. None
  21. None
  22. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

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

  24. Support Components Pages

  25. 1) Support 2) Components 3) Pages

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

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

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

    server/ static/ styles/ types/ utils/ config.ts paths.ts
  29. 1 Support Components Pages 2 3 components/ graphql/ hooks/ layouts/

    pages/ routes/ server/ static/ styles/ types/ utils/ config.ts paths.ts
  30. 1 Support

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

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

    utils/ config.ts paths.ts
  33. 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
  34. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

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

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

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

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

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

    utils/ config.ts paths.ts
  42. None
  43. useKey() useIsMounted() useTimeout()
 
 useHideOnTop()

  44. useGraphQLFragment() useViewier() useIsLoggedIn()

  45. useGraphQLFragment() useViewier() useIsLoggedIn()

  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. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

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

    utils/ config.ts paths.ts
  50. None
  51. 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
  52. import { format } from 'date-fns'; export function formatDateTime(date: string)

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

  54. date.ts Component Page date-fns

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

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

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

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

    utils/ config.ts paths.ts
  59. None
  60. None
  61. ( Component as directory components/ Component/ SubComponent/ Fragment.graphql Mutation.graphql icon.svg

    index.js styles.css utils.js
  62. None
  63. import * as React from "react"; import Font from "components/Font";

    <Font.Text>{text}</Font.Text>
  64. 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
  65. 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>

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

    <Font.Text>text</Font.Text> // -> <span class="text">text</span>
  67. 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>
  68. 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>
  69. 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
  70. 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
  71. 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
  72. 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
  73. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">Text</span>
  74. 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>
  75. 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
  76. 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) } ( Pass extra class name
  77. yarn install "classnames"

  78. None
  79. None
  80. <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>
  81. Input

  82. Input Loading

  83. Input Loading Success

  84. Input Loading Success Errors

  85. Submit Server Success Errors

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

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

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

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

  90. mutation UserSettingsUpdate($input: UserSettingsUpdateInp response: userSettingsUpdate(input: $input) { node { id

    ...MySettingsPageViewer } errors { field message } } }
  91. mutation UserSettingsUpdate($input: UserSettingsUpdateInp response: userSettingsUpdate(input: $input) { node { id

    ...MySettingsPageViewer } errors { field message } } }
  92. <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>
  93. None
  94. 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);
  95. 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);
  96. 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);
  97. 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);
  98. 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);
  99. 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);
  100. fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) =>

    { 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> ); }
  101. fields.value.includes(topic.id) && fields.push(topic.id); } const removeTopic = (topic: ITopic) =>

    { 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> ); }
  102. const removeTopic = (topic: ITopic) => { 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;
  103. const removeTopic = (topic: ITopic) => { 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;
  104. None
  105. Which is the form library we are using?!

  106. None
  107. None
  108. +

  109. +

  110. +

  111. , unified styles - common interface . custom inputs /

    understand backend GraphQL 0 Form
  112. None
  113. <Button href={paths.profile(profile)} />

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

    />
  115. <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} />
  116. <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} />
  117. <Button.Bordered {...props} /> <Button.Solid {...props} /> <Button.White {...props} /> <Button.Text

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

  119. None
  120. None
  121. LikeButton Button

  122. Domain Component

  123. AnswerCard Like Button

  124. <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
  125. Domain Component <AnswerCard> <Box>
 <Card /> <Font.Title /> <Flex.Grid> <Card

    /> </Flex> <LikeButton /> <DateFormat />
 </Box> </AnswerCard>
  126. Atomic Design

  127. ...Kinda 1

  128. 2 generic components . domain components

  129. 3 Pages

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

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

    => '/about', // ... } profiles: { people: () => '/people', show: ({ slug }: { slug: string }) => `/@${slug}`, // ... }, // ... }; path.ts
  132. import paths from 'ph/paths'; 
 paths.root(); // => / paths.static.about();

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

    utils/ config.ts paths.ts
  134. None
  135. None
  136. None
  137. <Layout> <Header profile={profile} /> <ContentWithMobileMenu> {children} </ContentWithMobileMenu> <Footer /> <CookiePolicyBanner

    /> </Layout> layouts/Main/index.tsx
  138. None
  139. <MainLayout> <ProfileHeader profile={profile} /> <div> <ProfileMenu /> {content} </div> </MainLayout>

    layouts/Main/index.tsx
  140. components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  141. ) components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/

    types/ utils/ config.ts paths.ts
  142. None
  143. pages/profiles/[slug]/index.ts

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

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

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

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

    styles.css utils.js
  148. Loading State Page Life Cycle

  149. Loading State Page Life Cycle

  150. Loading State Error State Page Life Cycle

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

    Error Error State Page Life Cycle
  152. Loading State Not Found Error Server Error Authorization Error Authentication

    Error Error State Loaded State Page Life Cycle
  153. Loading State Not Found Error Server Error Authorization Error Authentication

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

    Error SEO Error State Analytics Loaded State render Page Life Cycle ???
  155. 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> ), });
  156. 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> ), });
  157. 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> ), });
  158. 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> ), });
  159. 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> ), });
  160. 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> ), });
  161. None
  162. http://graphql.org/

  163. None
  164. ViewerFragme ProfileLayoutHeaderFragment ProfileLayout MenuFragme nt ProfileShowPage

  165. ViewerFragme ProfileLayout MenuFragme nt FollowButto Avatar Fragment ProTips Fragment Stack

    Fragment
  166. ViewerFragme ProfileLayout MenuFragme nt FollowButto Avatar Fragment Avata r Card

    LikeButton
  167. Query Fragm Fragm Fragm Fragm Fragm Fragm

  168. import gql from 'graphql-tag'; import AnswerCardWithQuestionFragment from '~/components/Answer/Card/FragmentWithQue 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/Fragmen 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 { id note
  169. 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 { id note profileTo { id name ...ProfileAvatarLinkFragment ...StackItemProfileFragment } } } } answers(first: 3) {
  170. } } tips(first: 1) { edges { node { id

    ...TipCardFragment } } } peopleWithSimilarInterests(limit: 4) { edges { node { id ...ProfilePeopleSectionFragment } } } ...HeadTagsFragment ...ProfileLayoutFragment ...ProfilesShowDoYouUseFragment ...ProfilesShowDoYouUseFragment } } ${AnswerCardWithQuestionFragment} ${DoYouUseFragment} ${HeadTagsFragment} ${ProfileAvatarLinkFragment}
  171. None
  172. Recap

  173. . GraphQL # Components 4 isolating dependancies ' directory as

    folder / domain components , Pages - paths helper & layouts % createPage 5 Recap
  174. None
  175. None
  176. Thanks 6