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 Slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View Slide

  3. View Slide

  4. Developer Team

    View Slide

  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

    View Slide

  6. View Slide

  7. View Slide

  8. html
    html
    json

    View Slide

  9. json
    json

    View Slide

  10. View Slide

  11. View Slide

  12. components/
    graphql/

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

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

    View Slide

  13. components/
    graphql/

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

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

    View Slide

  14. components/
    graphql/

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

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

    View Slide

  15. components/
    graphql/

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

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

    View Slide

  16. components/
    graphql/

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

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

    View Slide

  17. components/
    graphql/

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

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

    View Slide

  18. components/
    graphql/

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide



  28. View Slide





  29. View Slide





  30. View Slide





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

    View Slide

  32. components/
    graphql/

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

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

    View Slide

  33. View Slide

  34. components/
    graphql/

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

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

    View Slide

  35. Ducks: Redux Reducer Bundles

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


    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    export default reducer;

    View Slide

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

    View Slide

  41. components/
    graphql/

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

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

    View Slide

  42. View Slide

  43. View Slide

  44. View Slide













  45. View Slide













  46. View Slide













  47. View Slide













  48. View Slide













  49. View Slide













  50. View Slide













  51. View Slide













  52. View Slide









  53. View Slide









  54. View Slide









  55. View Slide









  56. View Slide









  57. View Slide









  58. View Slide









  59. View Slide

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




    {topic.name}
    {topic.description}




    );
    }

    View Slide

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




    {topic.name}
    {topic.description}




    );
    }

    View Slide

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

    import styles from './styles.css';


    View Slide

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

    import styles from './styles.css';


    View Slide

  64. 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 Slide

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




    {topic.name}
    {topic.description}




    );
    }

    View Slide

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




    {topic.name}
    {topic.description}




    );
    }

    View Slide








  67. View Slide

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




    {topic.name}
    {topic.description}




    );
    }

    View Slide

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




    {topic.name}
    {topic.description}




    );
    }

    View Slide

  70. 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 Slide

  71. 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 Slide

  72. 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 Slide

  73. 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 Slide

  74. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide


  80. http://graphql.org/


    View Slide


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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide


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

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

    View Slide


  87. http://www.apollodata.com/


    View Slide

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

    View Slide

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

    View Slide

  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) => );
    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 Slide

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

    View Slide


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


    View Slide

  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

    View Slide

  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
    }

    View Slide

  95. components/
    graphql/

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

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

    View Slide

  96. components/
    graphql/

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

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

    View Slide

  97. v3

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


    View Slide


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


    View Slide

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

    View Slide

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

    View Slide

  101. components/
    graphql/

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

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

    View Slide

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

    // ...
    };

    View Slide

  103. import paths from 'ph/paths';

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

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

    View Slide

  104. components/
    graphql/

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

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

    View Slide

  105. View Slide





  106. {children}



    View Slide





  107. {children}



    View Slide





  108. {children}



    View Slide





  109. {children}



    View Slide





  110. {children}



    View Slide





  111. {children}



    View Slide





  112. {children}



    View Slide

  113. components/
    graphql/

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

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

    View Slide

  114. View Slide

  115. View Slide

  116. View Slide

  117. Header
    Media
    Recommended
    Posts
    Makers
    Discussion

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  123. components/
    graphql/

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

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

    View Slide

  124. View Slide

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

    View Slide

  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

    View Slide

  127. Thanks !

    View Slide

  128. View Slide