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

React at Product Hunt

React at Product Hunt

At ProductHunt we use React for almost 2 years. In the talk, I'm going to share some of the things we learned during that period.

Radoslav Stankov

November 19, 2016
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. React at Product Hunt
    Radoslav Stankov 19/11/2016

    View Slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View Slide

  3. View Slide

  4. View Slide

  5. Developer Team

    View Slide

  6. Tech Stack

    View Slide

  7. Tech Stack

    View Slide

  8. View Slide

  9. Tech Stack
    ES2016
    BABEL
    ESLINT
    FLOW

    View Slide

  10. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux

    View Slide

  11. December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    October 2016 JSTalks talk :)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  18. components/
    layouts/
    lib/
    modules/
    pages/
    styles/
    types/
    utils/
    api.js
    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. https://github.com/facebook/flow

    View Slide

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

    View Slide

  34. https://github.com/producthunt/duxy

    View Slide

  35. const http = createHttpAdapter();
    export default duxy({ http }, ({ resources, resource, get }) => {
    get('context');
    resources('topics', { only: ['findAll', 'findOne'] }, () => {
    resource('topContributors', { path: 'top_contributors', only: ['findAll'] });
    resource('followers', { only: ['create', 'delete'] });
    });
    // ... more definitions
    });

    View Slide

  36. import api from 'ph/api';
    const response = await api.context(); // GET /context
    const response = await api.topics.findOne({ id: 1 }) // GET /topics/1
    const response = await api.topics.findAll({ limit: 10 }) // GET /topics?limit=10

    View Slide

  37. import api from 'ph/api';


    try {
    // POST /topics/1/followers
    const response = await api.topics.followers.create({ topicId: 1 });
    } catch(e) {
    // handle errors
    }

    View Slide

  38. ⚽ resources - findAll / findOne / create / update / delete
    resource - findAll / create / update / delete
    get
    ⚾ post
    patch
    delete
    namespace
    adapters
    Duxy

    View Slide

  39. https://github.com/producthunt/duxy

    View Slide

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

    View Slide

  41. 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. function TopicItemList({ topics }: { topics: Array }) {
    return (

    {topics.map((topic) => (



    ))}

    );
    }

    View Slide

  52. CSS
    // style.css
    .list {
    list-style: none;
    }
    CSS
    // component.js

    import styles from './styles.css';


    CSS
    // style.css
    .list_3mRwv {
    list-style: none;
    }
    CSS
    // result.js


    View Slide

  53. function TopicItemList({ topics }: { topics: Array }) {
    return (

    {topics.map((topic) => (



    ))}

    );
    }

    View Slide

  54. describe(TopicItemList.name, () => {
    it('renders list of topics', () => {
    const topic = new TopicFactory();
    const wrapper = shallow()
    expect(wrapper).to.contain(});
    });

    View Slide

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

    View Slide

  56. View Slide









  57. View Slide









  58. View Slide









  59. View Slide









  60. View Slide









  61. View Slide









  62. View Slide









  63. View Slide

  64. function TopicItem({ topic }: { topic: Topic }) {
    return (




    {topic.name}


    {followersCount(topic.followers_count)}



    );
    }

    View Slide








  65. View Slide

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




    {topic.name}


    {followersCount(topic.followers_count)}



    );
    }

    View Slide

  67. export class TopicFollowButton extends React.Component {
    props: {
    isFollowing: boolean,
    toggle: typeof toggleTopicFollow,
    topicId: number,
    className: string,
    };
    render() {
    return (
    active={this.props.isFollowing}
    className={this.props.className}
    onClick={this.handleClickAction}
    data-test="follow-button"
    {this.props.isFollowing ? 'Following' : 'Follow'}

    );
    }
    handleClickAction: Function = () => {

    View Slide

  68. export createContainer({
    renderComponent: Component,
    actionCreators: actionCreators,
    mapStateToProps: mapStateToProps,
    });
    export connect(mapStateToProps, actionCreators)(Component)

    View Slide

  69. export createContainer({
    renderComponent: Component,
    clientFetch() {
    // Browser data load
    },
    });


    class Component extends React.Component {
    componentDidMount() {
    clientFetch();
    }
    }

    View Slide

  70. export createContainer({
    renderComponent: Component,
    clientFetch(dispatch, getState, api): Promise {
    // Browser data load
    },
    fetch(dispatch, getState, api): Promise {
    // Browser/Server data load
    },
    });

    View Slide

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

    View Slide

  72. export class TopicFollowButton extends React.Component {
    props: {
    isFollowing: boolean,
    toggle: typeof toggleTopicFollow,
    topicId: number,
    className: string,
    };
    render() {
    return (
    active={this.props.isFollowing}
    className={this.props.className}
    onClick={this.handleClickAction}
    data-test="follow-button"
    {this.props.isFollowing ? 'Following' : 'Follow'}

    );
    }
    handleClickAction: Function = () => {

    View Slide

  73. describe(TopicFollowButton.name, () => {
    const call = sut(TopicFollowButton, {
    isFollowing: true,
    topicId: 1,
    toggle() {},
    });
    it('renders "Follow" text when is not following', () => {
    const wrapper = shallow(call({ isFollowing: false }));
    expect(wrapper).to.contain('Follow');
    });
    it('renders "Following" text when is following', () => {
    const wrapper = shallow(call({ isFollowing: true }));
    expect(wrapper).to.contain('Following');
    });
    it('can follow a topic', () => {
    const spy = sinon.spy();
    const wrapper = shallow(call({ toggle: spy, topicId: 1 }));

    View Slide

  74. /components/Box/index.js
    /components/Button/index.js
    /components/Fonts/index.js
    /components/InfiniteScroll/index.js
    /components/TopicFollowButton/index.js
    /components/TopicItemList/Item/index.js
    /components/TopicItemList/index.js

    View Slide

  75. View Slide

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

    View Slide

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

    View Slide

  78. Ducks: Redux Reducer Bundles
    https://github.com/erikras/ducks-modular-redux

    View Slide

  79. /modules/annotations.js
    /modules/collections.js
    /modules/context.js
    /modules/currentUser.js
    /modules/pages.js
    /modules/posts.js
    /modules/topics.js

    View Slide

  80. modules/[reducer].js
    1. Actions
    2. Reducer
    3. Action Creators
    4. Selectors
    5. Other stuff ¯\_(ツ)_/¯

    View Slide

  81. modules/[reducer].js
    1. Actions
    2. Reducer
    3. Action Creators
    4. Selectors
    5. Other stuff ¯\_(ツ)_/¯

    View Slide

  82. // Actions
    export const FOLLOW_TOPIC = 'TOPICS/FOLLOW';
    export const RECEIVE_TOPICS = 'TOPICS/RECEIVE';
    export const UNFOLLOW_TOPIC = 'TOPICS/UNFOLLOW';
    // Reducer
    type TopicsState = Immutable.Map;
    export const initialState: TopicsState = new Immutable.Map();
    export default createReducer(initialState, {
    [RECEIVE_TOPICS]: (state: TopicsState, action: Action): TopicsState => { /* ... */ },
    [FOLLOW_TOPIC]: (state: TopicsState, action: Action): TopicsState => { /* ... */ },
    [UNFOLLOW_TOPIC]: (state: TopicsState, action: Action): TopicsState => { /*...*/ },
    });
    // Action Creators
    export function receiveTopic({ topic }: { topic: Topic }): Dispatchable {
    return { type: RECEIVE_TOPICS, payload: { topics: [topic] } };
    }
    export function receiveTopics({ topics }: { topics: Array }): Dispatchable {
    return { type: RECEIVE_TOPICS, payload: { topics } };
    }
    export function toggleTopicFollow(topicId: number): Dispatchable {
    return function(dispatch: Dispatch, getState: Function, api: any): void {
    const { currentUser } = getState();
    if (isFollowingTopic(currentUser, id)) {

    View Slide

  83. export function toggleTopicFollow(topicId: number): Dispatchable {
    return function(dispatch: Dispatch, getState: Function, api: any): void {
    const { currentUser } = getState();
    if (isFollowingTopic(currentUser, id)) {
    api.topics.followers.delete({ topicId });
    dispatch({ type: UNFOLLOW_TOPIC, payload: { topicId } });
    } else {
    api.topics.followers.create({ topicId });
    dispatch({ type: FOLLOW_TOPIC, payload: { topicId } });
    }
    };
    }

    View Slide

  84. https://github.com/gaearon/redux-thunk

    View Slide

  85. function action() {
    return {
    type: ACTION,

    payload: {},
    };
    }
    redux-thunk

    View Slide

  86. function action() {
    return (dispatch, getState) => {
    // do stuff
    dispatch({ type: ACTION_1 });
    // do stuff
    dispatch({ type: ACTION_2 });
    // ...so on
    };
    }
    redux-thunk

    View Slide

  87. import { applyMiddleware, compose, createStore } from 'redux';
    import api from 'ph/api';
    import reducer from 'ph/reducers';
    export default function createStore(state = {}, options = {}) {
    const enhancers = [
    applyMiddleware(thunkInject(options.api || api)),
    // ... other middlewares
    ];
    return compose(...enhancers)(createStore)(reducer, initialState);
    }

    View Slide

  88. export function toggleTopicFollow(topicId: number): Dispatchable {
    return function(dispatch: Dispatch, getState: Function, api: any): void {
    const { currentUser } = getState();
    if (isFollowingTopic(currentUser, topicId)) {
    api.topics.followers.delete({ topicId });
    dispatch({ type: UNFOLLOW_TOPIC, payload: { topicId } });
    } else {
    api.topics.followers.create({ topicId });
    dispatch({ type: FOLLOW_TOPIC, payload: { topicId } });
    }
    };
    }

    View Slide

  89. describe(toggleTopicFollow.name, () => {
    const api = stubApi({
    'api.frontend.topics.followers.create': {},
    'api.frontend.topics.followers.delete': {},
    });
    const testStore = ({ topic, currentUser }) => {
    const store = createStore({}, { api });
    store.dispatch(receiveCurrentUser(currentUser));
    store.dispatch(receiveTopic(topic));
    return store;
    }
    const getState = (store) => store.getState().topics;
    it('increases followers count on follow', () => {
    const topic = new TopicFactory({ followersCount: 0 });
    const currentUser = new CurrentUserFactory();
    const store = testStore({ topic, currentUser });
    store.dispatch(toggleTopicFollow(topic));
    expect(topicById(getState(store), topic.id).followersCount).to.equal(1);
    });
    it('decreases followers count on unfollow', () => {
    const topic = new TopicFactory({ followersCount: 1 });
    const currentUser = new CurrentUserFactory({ followedTopicIds: [topic.id] });

    View Slide

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

    View Slide

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

    View Slide

  92. // routes.js
    export default createRoutes(










    );

    View Slide

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

    View Slide

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

    View Slide

  95. import paths from 'ph/paths';


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

    View Slide

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

    View Slide

  97. // routes.js
    export default createRoutes(










    );

    View Slide

  98. export default function Main({ children }: { children: any }) {
    return (








    {children}

    );
    }

    View Slide

  99. View Slide

  100. View Slide





  101. {children}



    View Slide





  102. {children}



    View Slide





  103. {children}



    View Slide





  104. {children}



    View Slide





  105. {children}



    View Slide





  106. {children}



    View Slide





  107. {children}



    View Slide

  108. export function Profile({ children, user, components, topics }: Pro
    return (





    {children}




    );
    }
    export default createContainer({
    renderComponent: Profile,
    decorators: [
    renderLoading(),
    renderNotFound((user) => !!user),
    ],
    fetch({ dispatch, getState, params: { username } }) {
    return dispatch(fetchProfile(username));

    View Slide

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

    View Slide

  110. View Slide

  111. {
    isLoading
    isLoaded
    resourceId/resourceIds
    ...otherValues
    }

    View Slide

  112. {
    isLoading
    isLoaded
    postId
    postSlug
    }

    View Slide

  113. LOADED
    RESET
    LOADING
    UPDATE

    View Slide

  114. // Actions
    export const RESET = 'PAGES/RESET';
    export const LOADING = 'PAGES/LOADING';
    export const LOADED = 'PAGES/LOADED';
    export const UPDATE = 'PAGES/UPDATE';
    // Pages
    export const POST = 'PAGES/POST';
    const PAGES = {
    [POST]: {
    isLoading: false,
    isLoaded: false,
    postId: [],
    postSlug: null,
    },
    // ... other pages
    };
    // ... other pages
    const initialState = new Immutable.Map(PAGES);

    View Slide

  115. export default createContainer({
    renderComponent: Post,
    decorators: [
    renderLoading(),
    documentTitle(({ post }) => post.title),
    ],
    fetch({ dispatch, params: { slug } }) {
    return dispatch(fetchPostPage(slug));
    },
    mapStateToProps({ pages, posts }) {
    const page = getPage(page, POST);
    return {
    ...page,
    post: postById(posts, page.postId),
    };
    },
    });

    View Slide

  116. export function fetchPostPage(slug: string): Dispatchable {
    return async (dispatch, getState, api) => {
    const page = getPage(getState().pages, POST);
    if (page.postSlug === slug) {
    return;
    }
    dispatch(reset(POST, {
    isLoading: true,
    postSlug: slug
    }));
    const { body } = await api.frontend.posts.findOne({id: slug});
    dispatch(bootstrap(body));
    dispatch(loaded(POST, {
    postId: body.post.id,
    }));
    };
    }

    View Slide

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

    View Slide

  118. View Slide

  119. https://github.com/ReactTraining/react-router
    https://github.com/airbnb/enzyme/
    https://github.com/chaijs/chai
    https://github.com/css-modules/css-modules
    https://github.com/erikras/ducks-modular-redux
    https://github.com/eslint/eslint
    https://github.com/facebook/flow
    https://github.com/gaearon/redux-thunk
    https://github.com/jnunemaker/flipper
    https://github.com/producthunt/chai-enzyme
    https://github.com/producthunt/duxy

    View Slide

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

    View Slide

  121. https://www.meetup.com/React-Sofia/

    View Slide

  122. Thanks

    View Slide

  123. View Slide