Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React @ Product Hunt (WAD)

React @ Product Hunt (WAD)

Radoslav Stankov

May 11, 2017
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

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

    View full-size slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View full-size slide

  3. Developer Team

    View full-size slide

  4. 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

    View full-size slide

  5. html
    html
    json

    View full-size slide

  6. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  7. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  8. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  9. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  10. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  11. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  12. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  13. https://github.com/facebook/flow

    View full-size slide

  14. export type Topic = {
    id: number,
    image_uuid?: ?string,
    name: string,
    description?: string,
    followers_count: number,
    posts_count: number,
    slug: string,
    };

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. 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);

    View full-size slide

  19. 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);

    View full-size slide

  20. 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); ! "

    View full-size slide

  21. function isFollowing(
    user: { followedTopicsIds: Array },
    topic: { id: number }
    ): boolean {
    return user.followedTopicsIds.indexOf(topic.id) !== -1;
    }

    View full-size slide





  22. class UserImage extends React.Component {
    props:
    {| user: User, width: number, height: number |} |
    {| user: User, variant: 'big' | 'medium' | 'small' |};
    render() { /* ... */ }
    }

    View full-size slide

  23. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  24. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  25. Ducks: Redux Reducer Bundles

    https://github.com/erikras/ducks-modular-redux


    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. 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 = function createReducer(null, {
    [RECEIVE_NOTICE]: (state, { payload: { notice } }) => notice,
    });

    export default reducer;

    View full-size slide

  30. 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);
    });
    });
    });

    View full-size slide

  31. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

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




    {topic.name}
    {topic.description}




    );
    }

    View full-size slide

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




    {topic.name}
    {topic.description}




    );
    }

    View full-size slide

  34. CSS
    // style.css
    .item {
    border-top: 1px solid $grey;
    }
    CSS
    // component.js

    import styles from './styles.css';


    View full-size slide

  35. CSS
    // style.css
    .item {
    border-top: 1px solid $grey;
    }
    CSS
    // component.js

    import styles from './styles.css';


    View full-size slide

  36. CSS
    // style.css
    .item {
    border-top: 1px solid $grey;
    }
    CSS
    // component.js

    import styles from './styles.css';


    CSS
    // style.css
    .item_3mRwv {
    border-top: 1px solid $grey;
    }
    CSS
    // result.js


    View full-size slide

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




    {topic.name}
    {topic.description}




    );
    }

    View full-size slide

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




    {topic.name}
    {topic.description}




    );
    }

    View full-size slide








  39. View full-size slide

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




    {topic.name}
    {topic.description}




    );
    }

    View full-size slide

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




    {topic.name}
    {topic.description}




    );
    }

    View full-size slide

  42. export class TopicFollowButton extends React.Component {
    props: {
    topic: Topic,
    isLogged: boolean,
    openLoginFullscreen: Function,
    followTopic: Function,
    unfollowTopic: Function,
    };
    render() {
    return (
    active={this.props.topic.isFollowed}
    onClick={this.handleClick}
    data-test="topic-follow">
    {this.props.topic.isFollowed ? 'Following' : 'Follow'}

    );
    }
    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();
    }
    };
    }

    View full-size slide

  43. export class TopicFollowButton extends React.Component {
    props: {
    topic: Topic,
    isLogged: boolean,
    openLoginFullscreen: Function,
    followTopic: Function,
    unfollowTopic: Function,
    };
    render() {
    return (
    active={this.props.topic.isFollowed}
    onClick={this.handleClick}
    data-test="topic-follow">
    {this.props.topic.isFollowed ? 'Following' : 'Follow'}

    );
    }
    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();
    }
    };
    }

    View full-size slide

  44. export class TopicFollowButton extends React.Component {
    props: {
    topic: Topic,
    isLogged: boolean,
    openLoginFullscreen: Function,
    followTopic: Function,
    unfollowTopic: Function,
    };
    render() {
    return (
    active={this.props.topic.isFollowed}
    onClick={this.handleClick}
    data-test="topic-follow">
    {this.props.topic.isFollowed ? 'Following' : 'Follow'}

    );
    }
    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();
    }
    };
    }

    View full-size slide

  45. export class TopicFollowButton extends React.Component {
    props: {
    topic: Topic,
    isLogged: boolean,
    openLoginFullscreen: Function,
    followTopic: Function,
    unfollowTopic: Function,
    };
    render() {
    return (
    active={this.props.topic.isFollowed}
    onClick={this.handleClick}
    data-test="topic-follow">
    {this.props.topic.isFollowed ? 'Following' : 'Follow'}

    );
    }
    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();
    }
    };
    }

    View full-size slide

  46. export class TopicFollowButton extends React.Component {
    props: {
    topic: Topic,
    isLogged: boolean,
    openLoginFullscreen: Function,
    followTopic: Function,
    unfollowTopic: Function,
    };
    render() {
    return (
    active={this.props.topic.isFollowed}
    onClick={this.handleClick}
    data-test="topic-follow">
    {this.props.topic.isFollowed ? 'Following' : 'Follow'}

    );
    }
    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();
    }
    };
    }

    View full-size slide

  47. 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, { /* ... */ }),
    ],
    }

    View full-size slide

  48. export default createContainer({
    renderComponent: Component,
    actionCreators: { action1, action2, action2 },
    mapStateToProps: ({ currentUser }) => ({ currentUser }),
    });
    export connect(mapStateToProps, actionCreators)(Component)

    View full-size slide

  49. export default createContainer({
    renderComponent: Component,
    decorators: [
    renderNotFound(someCondition),
    renderLoading(),
    addBodyClassName('white-background')
    documentTitle('About page'),
    requireEnabledFeature('new_topics')
    // ...
    ]
    });

    View full-size slide

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

    View full-size slide

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

    View full-size slide


  52. http://graphql.org/


    View full-size slide


  53. 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
    }
    }
    }

    View full-size slide

  54. 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
    }
    }
    }

    View full-size slide

  55. 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
    }
    }
    }

    View full-size slide

  56. 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
    }
    }
    }

    View full-size slide

  57. 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
    }
    }
    }

    View full-size slide


  58. mutation FollowTopic($input) {
    followTopic(input: $input) {
    node {
    id
    isFollowed
    }
    }
    }
    POST /graphql
    {
    "data": {

    "followTopic": {
    "node": {
    "id": 1,
    "isFollowed": true
    }
    }
    }
    }

    View full-size slide


  59. http://www.apollodata.com/


    View full-size slide

  60. 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, { /* ... */ }),
    ],
    }

    View full-size slide

  61. // components/FollowButton/FollowTopic.graphql
    mutation FollowTopic($input: FollowTopicInput!) {
    followTopic(input: $input) {
    node {
    id
    isFollowed
    }
    }
    }

    View full-size slide

  62. 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) => );
    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 }));

    View full-size slide

  63. 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;
    });
    });

    View full-size slide


  64. https://github.com/producthunt/chai-enzyme


    View full-size slide

  65. /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

    View full-size slide

  66. // 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
    }

    View full-size slide

  67. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  68. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  69. v3

    https://github.com/ReactTraining/react-router


    View full-size slide


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


    View full-size slide

  71. 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));
    },
    },
    // ...
    ],
    };

    View full-size slide

  72. 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);
    }

    View full-size slide

  73. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  74. export default {
    root(): string {
    return '/';
    },
    topics: {
    index(): string {
    return '/topics';
    },
    show({ slug }: Topic): string {
    return `/topics/${ slug }`;
    },
    },
    pages: {
    about(): string {
    return '/about';
    },
    // ...
    }

    // ...
    };

    View full-size slide

  75. import paths from 'ph/paths';

    paths.topics.index(); // => /topics/
    paths.topics.show(topic); // => /topics/developer-tools

    paths.pages.about(); // => /about/

    View full-size slide

  76. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide





  77. {children}



    View full-size slide





  78. {children}



    View full-size slide





  79. {children}



    View full-size slide





  80. {children}



    View full-size slide





  81. {children}



    View full-size slide





  82. {children}



    View full-size slide





  83. {children}



    View full-size slide

  84. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  85. Header
    Media
    Recommended
    Posts
    Makers
    Discussion

    View full-size slide

  86. /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

    View full-size slide

  87. // 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
    }
    }
    }

    View full-size slide

  88. // pages/Post/RecommendedPosts/Fragment.graphql
    #import "ph/pages/Post/RecommendedPosts/Item/Fragment.graphql"
    fragment PostRecommendedPosts on Post {
    recommended_posts(first: 10) {
    edges {
    node {
    ...PostRecommendedPostsItem
    }
    }
    }
    }

    View full-size slide

  89. // 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
    }
    }

    View full-size slide

  90. 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(),
    graphql(QUERY, {
    options: ({ params: { id } }) => ({
    variables: {
    id,
    },
    }),
    }),
    ],
    });

    View full-size slide

  91. components/
    graphql/

    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/

    config.js
    entry.js
    paths.js
    reducers.js
    routes.js

    View full-size slide

  92. https://speakerdeck.com/rstankov/react-at-product-hunt-wad

    View full-size slide

  93. # 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

    View full-size slide