Save 37% off PRO during our Black Friday Sale! »

React Architecture at Product Hunt

React Architecture at Product Hunt

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

November 03, 2021
Tweet

Transcript

  1. Product Hunt React Architecture Radoslav Stankov 11/11/2021

  2. !

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

  4. None
  5. Product Hunt React Architecture Radoslav Stankov 11/11/2021

  6. None
  7. https://speakerdeck.com/rstankov Thanks "

  8. Architecture

  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. None
  17. # Frontend Tech Stack

  18. None
  19. None
  20. None
  21. None
  22. $ Have good defaults % Have good code organization &

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

    Make common operations easy ' Isolate dependencies ( Extensibility and reusability
  24. None
  25. None
  26. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

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

  28. Support Components Pages

  29. 1) Support 2) Components 3) Pages

  30. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  31. 1 Support components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/

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

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

    pages/ screens/ server/ static/ styles/ types/ utils/ config.ts paths.ts
  34. 1 Support

  35. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  36. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  37. 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
  38. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

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

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

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

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

  44. None
  45. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  46. None
  47. useKey() useIsMounted() useTimeout()
 
 useHideOnTop()

  48. useGraphQLFragment() useViewier() useIsLoggedIn()

  49. useGraphQLFragment() useViewier() useIsLoggedIn()

  50. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  51. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  52. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  53. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  54. None
  55. import { format } from 'date-fns'; export function formatDateTime(date: string)

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

  57. date.ts Component Page date-fns

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

  59. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

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

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

    utils/ config.ts paths.ts
  62. None
  63. ) Component as directory components/ Component/ SubComponent/ Fragment.graphql Mutation.graphql icon.svg

    index.js styles.css utils.js
  64. None
  65. import * as React from "react"; import Text from "components/Text";

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

    export function Text({ children }) { return ( <span className={styles.text}> {children} </span> ); } components/Text/index.tsx
  67. 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>

  68. import * as React from "react"; import Text from "components/Text";

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

    <Text>text</Text> // -> <span class="text">text</span> <Text as="p">text</Text> // -> <p class="text">text</p>
  70. import * as React from "react"; import styles from "./styles.css";

    export function Text({ as, ...props }) { Component = as || "span"; return <Component className={styles.text} {...props} />; } ) Pass custom component as prop
  71. import * as React from "react"; import Text from "components/Text";

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

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

    import classNames from "classnames"; export function Text({ as, className, ...props }) { Component = as || "span";
 return <Component className={className(styles.text, className) } ) Pass extra class name
  74. yarn install "classnames"

  75. import * as React from "react"; import Text from "components/Text";

    <Text.Button onClick={fn}>text</Text.Button> // -> <Button className="text">text</Button> <Text.Link to="/page">text</Text.Link> // -> <Link className="text" href="/page">text</Link>
  76. None
  77. None
  78. <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>
  79. Input

  80. Input Loading

  81. Input Loading Success

  82. Input Loading Success Errors

  83. Submit Server Success Errors

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

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

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

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

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

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

    ...MySettingsPageViewer } errors { field message } } }
  90. <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>
  91. <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>
  92. None
  93. Which is the form library we are using?!

  94. None
  95. None
  96. *

  97. *

  98. *

  99. + unified styles , common interface - custom inputs .

    understand backend GraphQL / Form
  100. None
  101. <Button href={paths.profile(profile)} />

  102. <Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} />

  103. <Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} /> <Button confirm="Are you sure?"

    mutation={DELETE_MUTATION} input={{ id }} onMutate={redirectSomeWhere} />
  104. <Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} /> <Button confirm="Are you sure?"

    mutation={DELETE_MUTATION} input={{ id }} onMutate={redirectSomeWhere} />
  105. <Button.Primary {...props} /> <Button.Secondary {...props} /> <Button.Line {...props} /> <Button.Small

    {...props} />
  106. const clickHandler = useOnClick({ disabled, confirm, requireLogin, mutation, input, onMutate,

    onMutateError, optimisticResponse, update, updateQueries, refetchQueries, onClick, trackingComponent, trackingData, });
  107. Utility Styling Domain

  108. None
  109. VoteButton Button

  110. Domain Component

  111. PostItem Like Button

  112. Domain Component <PostList> <PostItem>
 <Flex.Row>
 <PostThumbnail />
 <Flex.Column> <Text.Title />

    <Text.Subtitle /> <Flex.Row>
 <PostCommentButton> <Button />
 </PostCommentButton>
 <Text.Small />
 </Flex.Row>
 </Flex.Column> <PostVoteButton>
 <Button />
 </PostVoteButton> </Flex.Row>
 </PostItem> </PostList>
  113. Atomic Design

  114. ...Kinda 0

  115. 1 generic components - domain components

  116. 3 Pages

  117. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

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

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

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

    utils/ config.ts paths.ts
  121. None
  122. None
  123. None
  124. None
  125. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

    utils/ config.ts paths.ts
  126. 2 components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/

    types/ utils/ config.ts paths.ts
  127. None
  128. pages/profiles/[slug]/index.ts

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

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

  131. components/ graphql/ hooks/ layouts/ pages/ screens/ server/ static/ styles/ types/

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

    styles.css utils.js
  133. None
  134. Loading State Page Life Cycle

  135. Loading State Page Life Cycle

  136. Loading State Error State Page Life Cycle

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

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

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

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

    Error SEO Error State Analytics Loaded State render Page Life Cycle ???
  141. 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> ), });
  142. 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> ), });
  143. 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> ), });
  144. 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> ), });
  145. 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> ), });
  146. 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> ), });
  147. http://graphql.org/

  148. None
  149. ProfileLayout Header Fragment ProfileLayout Sidebar Fragment ProfileShowPage

  150. Avatar Fragment PostItemFragment CommentFragment ProfileLayout
 SidebarInfo
 Fragment

  151. Query Fragm Fragm Fragm Fragm Fragm Fragm

  152. #import "ph/utils/seo/MetaTagsFragment.graphql" #import "ph/components/PostItemList/Framgent.graphql" #import "ph/layouts/Profile/Fragment.graphql" query ProfileShowPage( $cursor: String

    $username: String! $query: String ) { user(username: $username) { id votesCount votedPosts(first: 20, after: $cursor, query: $query) { edges { node { id ...PostItemListFragment } } pageInfo { endCursor hasNextPage } } ...MetaTags ...ProfileLayoutFragment } }
  153. None
  154. Recap

  155. - GraphQL $ Components 4 isolating dependancies ( directory as

    folder . domain components + Pages , paths helper ' layouts & createPage 5 Recap
  156. None
  157. None
  158. Thanks "

  159. https://speakerdeck.com/rstankov Thanks "

  160. None