Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
React Architecture at Product Hunt
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Radoslav Stankov
September 12, 2020
860
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
React Architecture at Product Hunt
What is the architecture at our latest product YourStack.
Radoslav Stankov
September 12, 2020
More Decks by Radoslav Stankov
See All by Radoslav Stankov
Tips for Building Useful Agent Skills
rstankov
0
120
Building LLM Powered Features
rstankov
0
180
Tips for Tailwind CSS
rstankov
0
78
Building LLM Powered Features (lightning talk)
rstankov
0
94
All you need is CSS
rstankov
0
170
Ruby on Rails The Single Engineer Framework
rstankov
0
68
Rails: The Missing Parts
rstankov
1
280
The dream that turned into nightmare
rstankov
0
350
The dream that turned into nightmare (lightning)
rstankov
0
160
Featured
See All Featured
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
2
2.9k
Six Lessons from altMBA
skipperchong
29
4.5k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
460
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
6.1k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.8k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
10
1.3k
How Software Deployment tools have changed in the past 20 years
geshan
1
34k
Technical Leadership for Architectural Decision Making
baasie
3
570
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.5k
Art, The Web, and Tiny UX
lynnandtonic
304
22k
Product Roadmaps are Hard
iamctodd
55
13k
Transcript
Product Hunt React Architecture Radoslav Stankov 15/09/2020
!
Radoslav Stankov @rstankov blog.rstankov.com twitter.com/rstankov github.com/rstankov speakerdeck.com/rstankov
None
None
Architecture
None
None
None
None
None
None
" Frontend Tech Stack
None
None
None
None
# Have good defaults $ Have good code organization %
Make common operations easy & Isolate dependencies ' Extensibility and reusability
# Have good defaults $ Have good code organization %
Make common operations easy & Isolate dependencies ' Extensibility and reusability
None
None
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
2 1 3 Support Components Pages
Support Components Pages
1) Support 2) Components 3) Pages
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
1 Support components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/
styles/ types/ utils/ config.ts paths.ts
1 Support Components 2 components/ graphql/ hooks/ layouts/ pages/ routes/
server/ static/ styles/ types/ utils/ config.ts paths.ts
1 Support Components Pages 2 3 components/ graphql/ hooks/ layouts/
pages/ routes/ server/ static/ styles/ types/ utils/ config.ts paths.ts
1 Support
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
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
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
apollo client:codegen \ --localSchemaFile="graphql/schema.json" \ --addTypename \ --tagName=gql \ --target=typescript
\ --includes="{components,routes,utils,hooks,layouts}/**/*.{tsx,ts}" \ --outputFlat="graphql/types.ts"
None
components/Profile/Avatar/Fragment.ts import gql from 'graphql-tag'; export default gql` fragment ProfileAvatarFragment
on Profile { id name kind imageUrl } `;
// ==================================================== // GraphQL fragment: ProfileAvatarFragment // ==================================================== export interface
ProfileAvatarFragment { __typename: "Profile"; id: string; name: string; kind: string; imageUrl: string | null; } graphql/types.ts
import { ProfileAvatarFragment } from '~/graphql/types';
None
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
None
useKey() useIsMounted() useTimeout() useHideOnTop()
useGraphQLFragment() useViewier() useIsLoggedIn()
useGraphQLFragment() useViewier() useIsLoggedIn()
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
None
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
import { format } from 'date-fns'; export function formatDateTime(date: string)
{ return format(date, 'H:mm A · MMM D, YYYY'); } utils/date.ts
moment date.ts Component Page
date.ts Component Page date-fns
utils/ external/ Intercom/ OneSignal/ Segment/ Sentry/
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
2 Components
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
None
None
( Component as directory components/ Component/ SubComponent/ Fragment.graphql Mutation.graphql icon.svg
index.js styles.css utils.js
None
import * as React from "react"; import Font from "components/Font";
<Font.Text>{text}</Font.Text>
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
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>
import * as React from "react"; import Font from "components/Font";
<Font.Text>text</Font.Text> // -> <span class="text">text</span>
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>
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>
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
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
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
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
import * as React from "react"; import Font from "components/Font";
<Font.Text>text</Font.Text> // -> <span class="text">Text</span>
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>
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
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
yarn install "classnames"
None
None
<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>
Input
Input Loading
Input Loading Success
Input Loading Success Errors
Submit Server Success Errors
remoteCall Server { result: 'ok' } { errors: {…} }
remoteCall Server { result: 'ok' } { errors: {
field1: 'error1', field2: 'error2' } }
{ errors: { field1: 'error1', field2: 'error2' } }
{ errors: { field1: 'error1', field2: 'error2' } }
mutation UserSettingsUpdate($input: UserSettingsUpdateInp response: userSettingsUpdate(input: $input) { node { id
...MySettingsPageViewer } errors { field message } } }
mutation UserSettingsUpdate($input: UserSettingsUpdateInp response: userSettingsUpdate(input: $input) { node { id
...MySettingsPageViewer } errors { field message } } }
<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>
None
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);
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);
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);
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);
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);
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);
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> ); }
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> ); }
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;
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;
None
Which is the form library we are using?!
None
None
+
+
+
, unified styles - common interface . custom inputs /
understand backend GraphQL 0 Form
None
<Button href={paths.profile(profile)} />
<Button href={paths.profile(profile)} /> <Button onClick={onClickReturnsPromise} loadingText="Waiting for promise to resolve"
/>
<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} />
<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} />
<Button.Bordered {...props} /> <Button.Solid {...props} /> <Button.White {...props} /> <Button.Text
{...props} /> <Button.Icon {...props} />
Utility Styling Domain
None
None
LikeButton Button
Domain Component
AnswerCard Like Button
<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
Domain Component <AnswerCard> <Box> <Card /> <Font.Title /> <Flex.Grid> <Card
/> </Flex> <LikeButton /> <DateFormat /> </Box> </AnswerCard>
Atomic Design
...Kinda 1
2 generic components . domain components
3 Pages
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
export default { root: () => '/', static: { about()
=> '/about', // ... } profiles: { people: () => '/people', show: ({ slug }: { slug: string }) => `/@${slug}`, // ... }, // ... }; path.ts
import paths from 'ph/paths'; paths.root(); // => / paths.static.about();
// => /about/ paths.profiles.people(); // => /people paths.profiles.show(profile); // => /@rstankov
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
None
None
None
<Layout> <Header profile={profile} /> <ContentWithMobileMenu> {children} </ContentWithMobileMenu> <Footer /> <CookiePolicyBanner
/> </Layout> layouts/Main/index.tsx
None
<MainLayout> <ProfileHeader profile={profile} /> <div> <ProfileMenu /> {content} </div> </MainLayout>
layouts/Main/index.tsx
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
) components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/
types/ utils/ config.ts paths.ts
None
pages/profiles/[slug]/index.ts
import page from '~/routes/profiles/show'; export default page; pages/profiles/[slug]/index.ts
import page from '~/routes/profiles/show'; export default page; pages/profiles/[slug]/index.ts 3
components/ graphql/ hooks/ layouts/ pages/ routes/ server/ static/ styles/ types/
utils/ config.ts paths.ts
Component as directory routing/ profiles/ show/ SubComponent1/ SubComponent2/ Query.graphql index.js
styles.css utils.js
Loading State Page Life Cycle
Loading State Page Life Cycle
Loading State Error State Page Life Cycle
Loading State Not Found Error Server Error Authorization Error Authentication
Error Error State Page Life Cycle
Loading State Not Found Error Server Error Authorization Error Authentication
Error Error State Loaded State Page Life Cycle
Loading State Not Found Error Server Error Authorization Error Authentication
Error SEO Error State Analytics Loaded State Page Life Cycle ???
Loading State Not Found Error Server Error Authorization Error Authentication
Error SEO Error State Analytics Loaded State render Page Life Cycle ???
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> ), });
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> ), });
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> ), });
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> ), });
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> ), });
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> ), });
None
http://graphql.org/
None
ViewerFragme ProfileLayoutHeaderFragment ProfileLayout MenuFragme nt ProfileShowPage
ViewerFragme ProfileLayout MenuFragme nt FollowButto Avatar Fragment ProTips Fragment Stack
Fragment
ViewerFragme ProfileLayout MenuFragme nt FollowButto Avatar Fragment Avata r Card
LikeButton
Query Fragm Fragm Fragm Fragm Fragm Fragm
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
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) {
} } tips(first: 1) { edges { node { id
...TipCardFragment } } } peopleWithSimilarInterests(limit: 4) { edges { node { id ...ProfilePeopleSectionFragment } } } ...HeadTagsFragment ...ProfileLayoutFragment ...ProfilesShowDoYouUseFragment ...ProfilesShowDoYouUseFragment } } ${AnswerCardWithQuestionFragment} ${DoYouUseFragment} ${HeadTagsFragment} ${ProfileAvatarLinkFragment}
None
Recap
. GraphQL # Components 4 isolating dependancies ' directory as
folder / domain components , Pages - paths helper & layouts % createPage 5 Recap
None
None
Thanks 6