React @ Product Hunt (WAD)

React @ Product Hunt (WAD)

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

May 11, 2017
Tweet

Transcript

  1. React @ Product Hunt Radoslav Stankov 12/05/2017

  2. Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

  3. None
  4. Developer Team

  5. October 2014 Backbone early 2014 jQuery spaghetti February 2014 React

    on Rails May 2014 custom Flux December 2015 Redux January 2016 React-Router April 2016 Redux Ducks January 2017 Frontend app February 2017 GraphQL March 2017 Code splitting
  6. None
  7. None
  8. html html json

  9. json json

  10. None
  11. None
  12. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  13. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  14. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  15. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  16. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  17. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  18. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  19. https://github.com/facebook/flow

  20. export type Topic = { id: number, image_uuid?: ?string, name:

    string, description?: string, followers_count: number, posts_count: number, slug: string, };
  21. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; }
  22. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse);
  23. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse);
  24. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic);
  25. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic);
  26. function isFollowing(user: CurrentUser, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); const variable: number = isFollowing(user, topic); const variable: boolean = isFollowing(user, topic); ! "
  27. function isFollowing( user: { followedTopicsIds: Array<number> }, topic: { id:

    number } ): boolean { return user.followedTopicsIds.indexOf(topic.id) !== -1; }
  28. <UserImage user={user} width={50} height={30} /> <UserImage user={user} variant="small" />

  29. <UserImage user={user} width={50} height={30} /> <UserImage user={user} variant="small" /> <UserImage

    user={user} width={50} variant="small" /> <UserImage user={user} width={50} height={30} variant="small" />
  30. <UserImage user={user} width={50} height={30} /> <UserImage user={user} variant="small" /> <UserImage

    user={user} width={50} variant="small" /> <UserImage user={user} width={50} height={30} variant="small" />
  31. <UserImage user={user} width={50} height={30} /> <UserImage user={user} variant="small" /> <UserImage

    user={user} width={50} variant="small" /> <UserImage user={user} width={50} height={30} variant="small" /> class UserImage extends React.Component { props: {| user: User, width: number, height: number |} | {| user: User, variant: 'big' | 'medium' | 'small' |}; render() { /* ... */ } }
  32. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  33. None
  34. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  35. Ducks: Redux Reducer Bundles 
 https://github.com/erikras/ducks-modular-redux


  36. /modules/anchor.js /modules/context.js /modules/cookies.js /modules/currentUser.js /modules/notice.js /modules/seed.js /modules/visitedTopics.js

  37. modules/[reducer].js 1. Action Constants 2. Action Creators 3. Reducer 4.

    Selectors 5. Other stuff ¯\_(ϑ)_/¯
  38. modules/[reducer].js 1. Action Constants 2. Action Creators 3. Reducer 4.

    Selectors 5. Other stuff ¯\_(ϑ)_/¯
  39. import { createReducer } from 'ph/redux'; import type { Dispatchable,

    Reducer } from 'ph/types/redux'; import type { Notice } from 'ph/types/Notice'; // Constants const RECEIVE_NOTICE = 'NOTICE/RECEIVE'; // Actions export function receiveNotice(notice: Notice): Dispatchable { return { type: RECEIVE_NOTICE, payload: { notice } }; } export function clearNotice(): Dispatchable { return { type: RECEIVE_NOTICE, payload: { notice: null } }; } // Reducer
 const reducer: Reducer<?Notice> = function createReducer(null, { [RECEIVE_NOTICE]: (state, { payload: { notice } }) => notice, });
 export default reducer;
  40. import createTestStore from 'ph/createTestStore'; import { clearNotice, receiveNotice } from

    'ph/modules/notice'; describe('notice', () => { const getState = (store) => store.getState().notice; describe(receiveNotice.name, () => { it('sets a notice object', () => { const store = createTestStore(); const notice = { type: 'notice', message: 'submitted' } store.dispatch(receiveNotice(notice)); expect(getState(store)).to.deep.equal(notice); }); }); describe(clearNotice.name, () => { it('sets a notice object', () => { const store = createTestStore(); store.dispatch(clearNotice()); expect(getState(store)).to.equal(null); }); }); });
  41. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  42. None
  43. None
  44. None
  45. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  46. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  47. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  48. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  49. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  50. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  51. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  52. <TopicsPage> <InfiniteScroll> <Box>
 <BoxHeader>
 <SearchField />
 </BoxHeader> <TopicItemList> <TopicItem />

    </TopicItemList> </Box> </InfiniteScroll> </TopicsPage>
  53. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  54. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  55. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  56. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  57. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  58. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  59. <TopicItem> <Link> <TopicImage /> <Featured /> <Text /> </Link> <TopicFollowButton

    /> </TopicItem>
  60. export default function TopicItem({ topic }: { topic: Topic })

    { return ( <div className={styles.item}> <Link to={paths.topics.show(topic)} className={styles.link}> <TopicImage topic={topic} size={50} /> <div className={styles.info}> <Featured>{topic.name}</Featured> <Text>{topic.description}</Text> </div> </TrackLink> <TopicFollowButton topic={topic} /> </div> ); }
  61. export default function TopicItem({ topic }: { topic: Topic })

    { return ( <div className={styles.item}> <Link to={paths.topics.show(topic)} className={styles.link}> <TopicImage topic={topic} size={50} /> <div className={styles.info}> <Featured>{topic.name}</Featured> <Text>{topic.description}</Text> </div> </TrackLink> <TopicFollowButton topic={topic} /> </div> ); }
  62. CSS // style.css .item { border-top: 1px solid $grey; }

    CSS // component.js
 import styles from './styles.css'; <div className={styles.item}> </div>
  63. CSS // style.css .item { border-top: 1px solid $grey; }

    CSS // component.js
 import styles from './styles.css'; <div className={styles.item}> </div>
  64. CSS // style.css .item { border-top: 1px solid $grey; }

    CSS // component.js
 import styles from './styles.css'; <div className={styles.item}> </div> CSS // style.css .item_3mRwv { border-top: 1px solid $grey; } CSS // result.js <div className="item_3mRwv"> </div>

  65. export default function TopicItem({ topic }: { topic: Topic })

    { return ( <div className={styles.item}> <Link to={paths.topics.show(topic)} className={styles.link}> <TopicImage topic={topic} size={50} /> <div className={styles.info}> <Featured>{topic.name}</Featured> <Text>{topic.description}</Text> </div> </TrackLink> <TopicFollowButton topic={topic} /> </div> ); }
  66. export default function TopicItem({ topic }: { topic: Topic })

    { return ( <div className={styles.item}> <Link to={paths.topics.show(topic)} className={styles.link}> <TopicImage topic={topic} size={50} /> <div className={styles.info}> <Featured>{topic.name}</Featured> <Text>{topic.description}</Text> </div> </TrackLink> <TopicFollowButton topic={topic} /> </div> ); }
  67. <Headline>…</Headline> <Featured>…</Featured> <Title>…</Title> <Text>…</Text> <BoldText>…</BoldText> <SecondaryText>…</SecondaryText> <SecondaryBoldText>…</SecondaryBoldText>

  68. export default function TopicItem({ topic }: { topic: Topic })

    { return ( <div className={styles.item}> <Link to={paths.topics.show(topic)} className={styles.link}> <TopicImage topic={topic} size={50} /> <div className={styles.info}> <Featured>{topic.name}</Featured> <Text>{topic.description}</Text> </div> </TrackLink> <TopicFollowButton topic={topic} /> </div> ); }
  69. export default function TopicItem({ topic }: { topic: Topic })

    { return ( <div className={styles.item}> <Link to={paths.topics.show(topic)} className={styles.link}> <TopicImage topic={topic} size={50} /> <div className={styles.info}> <Featured>{topic.name}</Featured> <Text>{topic.description}</Text> </div> </TrackLink> <TopicFollowButton topic={topic} /> </div> ); }
  70. export class TopicFollowButton extends React.Component { props: { topic: Topic,

    isLogged: boolean, openLoginFullscreen: Function, followTopic: Function, unfollowTopic: Function, }; render() { return ( <Button active={this.props.topic.isFollowed} onClick={this.handleClick} data-test="topic-follow"> {this.props.topic.isFollowed ? 'Following' : 'Follow'} </Button> ); } handleClick: Function = async (event: BrowserEvent) => { event.preventDefault(); if (!this.props.isLogged) { this.props.openLoginFullscreen({ reason: `follow ${ this.props.topic.name }` }); } else if (this.props.topic.isFollowed) { await this.props.unfollowTopic(); } else { await this.props.followTopic(); } }; }
  71. export class TopicFollowButton extends React.Component { props: { topic: Topic,

    isLogged: boolean, openLoginFullscreen: Function, followTopic: Function, unfollowTopic: Function, }; render() { return ( <Button active={this.props.topic.isFollowed} onClick={this.handleClick} data-test="topic-follow"> {this.props.topic.isFollowed ? 'Following' : 'Follow'} </Button> ); } handleClick: Function = async (event: BrowserEvent) => { event.preventDefault(); if (!this.props.isLogged) { this.props.openLoginFullscreen({ reason: `follow ${ this.props.topic.name }` }); } else if (this.props.topic.isFollowed) { await this.props.unfollowTopic(); } else { await this.props.followTopic(); } }; }
  72. export class TopicFollowButton extends React.Component { props: { topic: Topic,

    isLogged: boolean, openLoginFullscreen: Function, followTopic: Function, unfollowTopic: Function, }; render() { return ( <Button active={this.props.topic.isFollowed} onClick={this.handleClick} data-test="topic-follow"> {this.props.topic.isFollowed ? 'Following' : 'Follow'} </Button> ); } handleClick: Function = async (event: BrowserEvent) => { event.preventDefault(); if (!this.props.isLogged) { this.props.openLoginFullscreen({ reason: `follow ${ this.props.topic.name }` }); } else if (this.props.topic.isFollowed) { await this.props.unfollowTopic(); } else { await this.props.followTopic(); } }; }
  73. export class TopicFollowButton extends React.Component { props: { topic: Topic,

    isLogged: boolean, openLoginFullscreen: Function, followTopic: Function, unfollowTopic: Function, }; render() { return ( <Button active={this.props.topic.isFollowed} onClick={this.handleClick} data-test="topic-follow"> {this.props.topic.isFollowed ? 'Following' : 'Follow'} </Button> ); } handleClick: Function = async (event: BrowserEvent) => { event.preventDefault(); if (!this.props.isLogged) { this.props.openLoginFullscreen({ reason: `follow ${ this.props.topic.name }` }); } else if (this.props.topic.isFollowed) { await this.props.unfollowTopic(); } else { await this.props.followTopic(); } }; }
  74. export class TopicFollowButton extends React.Component { props: { topic: Topic,

    isLogged: boolean, openLoginFullscreen: Function, followTopic: Function, unfollowTopic: Function, }; render() { return ( <Button active={this.props.topic.isFollowed} onClick={this.handleClick} data-test="topic-follow"> {this.props.topic.isFollowed ? 'Following' : 'Follow'} </Button> ); } handleClick: Function = async (event: BrowserEvent) => { event.preventDefault(); if (!this.props.isLogged) { this.props.openLoginFullscreen({ reason: `follow ${ this.props.topic.name }` }); } else if (this.props.topic.isFollowed) { await this.props.unfollowTopic(); } else { await this.props.followTopic(); } }; }
  75. reason: `follow ${ this.props.topic.name }` }); } else if (this.props.topic.isFollowed)

    { await this.props.unfollowTopic(); } else { await this.props.followTopic(); } }; } export default createContainer({ renderComponent: TopicFollowButton, actionCreators: { openLoginFullscreen, }, mapStateToProps({ currentUser }) { return { isLogged: currentUser.isLogged }; }, decorators: [ graphql(CREATE_MUTATION, { /* ... */ }), graphql(REMOVE_MUTATION, { /* ... */ }), ], }
  76. export default createContainer({ renderComponent: Component, actionCreators: { action1, action2, action2

    }, mapStateToProps: ({ currentUser }) => ({ currentUser }), }); export connect(mapStateToProps, actionCreators)(Component)
  77. export default createContainer({ renderComponent: Component, decorators: [ renderNotFound(someCondition), renderLoading(), addBodyClassName('white-background')

    documentTitle('About page'), requireEnabledFeature('new_topics') // ... ] });
  78. export default createContainer({ renderComponent: TopicFollowButton, actionCreators: { openLoginFullscreen, }, mapStateToProps({

    currentUser }) { return { isLogged: currentUser.isLogged }; }, decorators: [ graphql(CREATE_MUTATION, { /* ... */ }), graphql(REMOVE_MUTATION, { /* ... */ }), ], }
  79. export default createContainer({ renderComponent: TopicFollowButton, actionCreators: { openLoginFullscreen, }, mapStateToProps({

    currentUser }) { return { isLogged: currentUser.isLogged }; }, decorators: [ graphql(CREATE_MUTATION, { /* ... */ }), graphql(REMOVE_MUTATION, { /* ... */ }), ], }
  80. 
 http://graphql.org/


  81. 
 query { topic(id: 1) { id name description isFollowed

    image } } POST /graphql { "data": { "topic": { "id": 1, "name": "Tech", "description": "Hardware or "isFollowed": true, "image": "assets.producthun } } }
  82. query { topic(id: 1) { id ...Item } } fragment

    Item on Topic { id name description ...Button ...Image } fragment Button on Topic { id name isFollowed } fragment Image on Topic { image } POST /graphql { "data": { "topic": { "id": 1, "name": "Tech", "description": "Hardware or "isFollowed": true, "image": "assets.producthun } } }
  83. query { topic(id: 1) { id ...Item } } fragment

    Item on Topic { id name description ...Button ...Image } fragment Button on Topic { id name isFollowed } fragment Image on Topic { image } POST /graphql { "data": { "topic": { "id": 1, "name": "Tech", "description": "Hardware or "isFollowed": true, "image": "assets.producthun } } }
  84. query { topic(id: 1) { id ...Item } } fragment

    Item on Topic { id name description ...Button ...Image } fragment Button on Topic { id name isFollowed } fragment Image on Topic { image } POST /graphql { "data": { "topic": { "id": 1, "name": "Tech", "description": "Hardware or "isFollowed": true, "image": "assets.producthun } } }
  85. query { topic(id: 1) { id ...Item } } fragment

    Item on Topic { id name description ...Button ...Image } fragment Button on Topic { id name isFollowed } fragment Image on Topic { image } POST /graphql { "data": { "topic": { "id": 1, "name": "Tech", "description": "Hardware or "isFollowed": true, "image": "assets.producthun } } }
  86. 
 mutation FollowTopic($input) { followTopic(input: $input) { node { id

    isFollowed } } } POST /graphql { "data": {
 "followTopic": { "node": { "id": 1, "isFollowed": true } } } }
  87. 
 http://www.apollodata.com/


  88. import CREATE_MUTATION from './FollowTopic.graphql';
 import REMOVE_MUTATION from './UnfollowTopic.graphql';
 
 //

    ... 
 export default createContainer({ // ... decorators: [ graphql(CREATE_MUTATION, { props: ({ ownProps: { topic: { id } }, mutate }) => ({ followTopic() { return mutate({ variables: { input: { id }, }, optimisticResponse: { response: { node: { __typename: 'Topic', id, isFollowed: true }, }, } }); }, }), }), graphql(REMOVE_MUTATION, { /* ... */ }), ], }
  89. // components/FollowButton/FollowTopic.graphql mutation FollowTopic($input: FollowTopicInput!) { followTopic(input: $input) { node

    { id isFollowed } } }
  90. import React from 'react'; import sinon from 'sinon'; import TopicFactory

    from 'factories/TopicFactory';
 import { Component } from 'ph/components/TopicFollowButton'; describe(Component.name, () => { const followed = new TopicFactory({ isFollowed: true }); const unfollowed = new TopicFactory({ isFollowed: false }); const defaults = { topic: unfollowed, isLogged: true, openLoginFullscreen() {}, followTopic() {}, unfollowTopic() {}, }; const call = sut(defaults, (opts) => <Component {...opts} />); it('renders "Follow" text when is not following', () => { const wrapper = shallow(call({ topic: followed })); expect(wrapper).to.contain('Follow'); }); it('renders "Following" text when is following', () => { const wrapper = shallow(call({ topic: unfollowed })); expect(wrapper).to.contain('Following'); }); it('can follow a topic', () => { const spy = sinon.spy(); const wrapper = shallow(call({ followTopic: spy, topic: followed }));
  91. expect(wrapper).to.contain('Following'); }); it('can follow a topic', () => { const

    spy = sinon.spy(); const wrapper = shallow(call({ followTopic: spy, topic: followed })); wrapper.simulate('click', { preventDefault: () => {} }); expect(spy).to.have.been.calledOnce; }); it('can unfollow a topic', () => { const spy = sinon.spy(); const wrapper = shallow(call({ unfollowTopic: spy, topic: followed })); wrapper.simulate('click', { preventDefault: () => {} }); expect(spy).to.have.been.calledOnce; }); it('calls openLoginFullscreen when user is not logged', () => { const spy = sinon.spy(); const wrapper = shallow(call({ handleNotLogged: spy, isLogged: false, })); wrapper.simulate('click', { preventDefault: () => {} }); expect(spy).to.have.been.calledOnce; }); });
  92. 
 https://github.com/producthunt/chai-enzyme


  93. /components/TopicFollowButton/FollowTopic.graphql /components/TopicFollowButton/Fragment.graphql /components/TopicFollowButton/UnollowTopic.graphql /components/TopicFollowButton/index.js /components/TopicItem/Fragment.graphql /components/TopicItem/index.js /components/TopicItem/styles.js

  94. // components/TopicItem/Fragment.graphql #import "ph/components/TopicFollowButton/TopicFollowButton.graphql" #import "ph/components/TopicImage/TopicImage.graphql" fragment TopicItem on Topic

    { id name slug description ...TopicFollowButton ...TopicImage }
  95. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  96. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  97. v3 
 https://github.com/ReactTraining/react-router


  98. 
 https://webpack.js.org/guides/code-splitting-async


  99. export default { component: Main, childRoutes: [ { path: '/',

    track: 'home', getComponent(_, cb) { require.ensure([], (require) => cb(null, require('ph/pages/Homepage').default)); }, }, { path: '/topics', track: 'topic_index', getComponent(_, cb) { require.ensure([], (require) => cb(null, require('ph/pages/Topics').default)); }, }, { path: '/topics/:slug', track: 'topic_detail', getComponent(_, cb) { require.ensure([], (require) => cb(null, require('ph/pages/Topic').default)); }, }, // ... ], };
  100. export default function createReduxStore(initialState = {}, { apollo } =

    {}) { const middlewares = [ reduxReactRouter({ createHistory: canUseDOM ? createBrowserHistory : createMemoryHistory, routes, }), applyMiddleware(analytics), ]; if (apollo) { middlewares.push(applyMiddleware(apollo.middleware())); } if (canUseDOM && window.localStorage) { middlewares.push(applyMiddleware(persistState())); } if (canUseDOM && window.devToolsExtension) { middlewares.push(window.devToolsExtension()); } const finalReducers = apollo ? { ...reducers, apollo: apollo.reducer() } : reducers; const finalStore = compose(...middlewares)(createStore); return finalStore(combineReducers(finalReducers), initialState); }
  101. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  102. export default { root(): string { return '/'; }, topics:

    { index(): string { return '/topics'; }, show({ slug }: Topic): string { return `/topics/${ slug }`; }, }, pages: { about(): string { return '/about'; }, // ... }
 // ... };
  103. import paths from 'ph/paths';
 paths.topics.index(); // => /topics/ paths.topics.show(topic); //

    => /topics/developer-tools
 paths.pages.about(); // => /about/
  104. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  105. None
  106. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  107. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  108. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  109. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  110. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  111. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  112. <Main>
 <Header /> <Feed> <Menu /> {children} <Sidebar /> </Feed>

    </Main>
  113. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  114. None
  115. None
  116. None
  117. Header Media Recommended Posts Makers Discussion

  118. /pages/Post/Discussion/Fragment.graphql /pages/Post/Discussion/index.js /pages/Post/Discussion/styles.css /pages/Post/RecommendedPosts/Fragment.graphql /pages/Post/RecommendedPosts/index.js /pages/Post/RecommendedPosts/styles.css /pages/Post/RecommendedPosts/Item/... /pages/Post/Header/... /pages/Post/Makers/... /pages/Post/Media/...

    /pages/Post/Query.graphql /pages/Post/index.js /pages/Post/styles.js
  119. // pages/Post/Discussion/Fragment.graphql
 fragment PostDiscussion on Post { comments { id

    created_at parent_comment_id state votes_count body user { id username name headline } } }
  120. // pages/Post/RecommendedPosts/Fragment.graphql #import "ph/pages/Post/RecommendedPosts/Item/Fragment.graphql" fragment PostRecommendedPosts on Post { recommended_posts(first:

    10) { edges { node { ...PostRecommendedPostsItem } } } }
  121. // pages/Post/PostPage.graphql #import "ph/lib/meta/MetaTags.graphql" #import "ph/pages/Post/Discussion/Fragment.graphql" #import "ph/pages/Post/Header/Fragment.graphql" #import "ph/pages/Post/Makers/Fragment.graphql"

    #import "ph/pages/Post/Media/Fragment.graphql" #import "ph/pages/Post/RecommendedPosts/Fragment.graphql" query PostPage($id: String!) { post(id: $id) { id
 ...MetaTags ...PostDiscussion ...PostHeader ...PostMedia ...PostRecommendedPosts } }
  122. import QUERY from './Query.graphql'; import { graphql } from 'react-apollo';

    // ... export default createContainer({ renderComponent: Content, decorators: [ setTags(({ data: { post: { meta } } }) => meta), visibleIf(({ data }) => !!data.post),
 deferredRender(<Loading subject="post" />), graphql(QUERY, { options: ({ params: { id } }) => ({ variables: { id, }, }), }), ], });
  123. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  124. None
  125. https://speakerdeck.com/rstankov/react-at-product-hunt-wad

  126. # https://github.com/acdlite/redux-router $ https://github.com/airbnb/enzyme/ % https://github.com/apollographql & https://github.com/chaijs/chai ' https://github.com/css-modules/css-modules

    ( https://github.com/DylanPiercey/require-ensure ) https://github.com/erikras/ducks-modular-redux * https://github.com/facebook/flow + https://github.com/graphql , https://github.com/producthunt/chai-enzyme - https://github.com/ReactTraining/react-router . https://webpack.js.org/guides/code-splitting-async
  127. Thanks !

  128. None