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

React Patterns in ProductHunt

React Patterns in ProductHunt

Radoslav Stankov

July 08, 2018
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. React Patterns in
    Product Hunt
    Radoslav Stankov 08/07/2018

    View Slide

  2. Radoslav Stankov
    @rstankov

    blog.rstankov.com

    github.com/rstankov

    twitter.com/rstankov

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

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

    View Slide

  8. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux

    View Slide

  9. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router

    View Slide

  10. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks

    View Slide

  11. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  12. October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  13. February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  14. December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  15. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    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. https://speakerdeck.com/rstankov/react-at-product-hunt-wad


    View Slide

  19. components/
    graphql/

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

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

    View Slide


  20. https://speakerdeck.com/rstankov/graphql-at-product-hunt


    View Slide

  21. components/
    graphql/

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

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

    View Slide

  22. ! Generic
    " Utility
    # Domain
    $ Pages

    Component types


    View Slide

  23. ! Generic
    " Utility
    # Domain

    Component types


    View Slide

  24. ! Generic
    " Utility
    # Domain

    Component types


    View Slide

  25. Generic components

    View Slide

  26. View Slide

  27. components/
    Font/

    index.js
    styles.css


    Component as folder


    View Slide

  28. components/
    Component/

    SubComponent/
    Fragment.graphql
    Mutation.graphql
    icon.svg
    index.js
    styles.css
    utils.js

    Component as folder


    View Slide

  29. components/
    Font/

    index.js
    styles.css


    Component as folder


    View Slide

  30. import * as React from "react";
    import Font from "components/Font";
    {text}

    View Slide

  31. components/Font/index.js
    import * as React from "react";
    import styles from "./styles.css";
    export function Text({ children }) {
    return (

    {children}

    );
    }

    Functional components


    View Slide

  32. components/
    Font/

    index.js
    styles.css


    CSS Modules


    View Slide

  33. CSS
    // style.css
    .text {
    font-size: 14px;
    }
    CSS
    // component.js

    import styles from './styles.css';


    View Slide

  34. CSS
    // style.css
    .text {
    font-size: 14px;
    }
    CSS
    // component.js

    import styles from './styles.css';


    View Slide

  35. CSS
    // style.css
    .text {
    font-size: 14px;
    }
    CSS
    // component.js

    import styles from './styles.css';


    CSS
    // style.css
    .text_3mRwv {
    font-size: 14px;
    }
    CSS
    // result.js


    View Slide

  36. import * as React from "react";
    import Font from "components/Font";
    text
    // -> text

    View Slide

  37. import * as React from "react";
    import Font from "components/Font";
    text
    // -> text
    text
    // -> text

    View Slide

  38. import * as React from "react";
    import Font from "components/Font";
    text
    // -> text
    text
    // -> text
    text
    // -> text

    View Slide

  39. import * as React from "react";
    import Font from "components/Font";
    text
    // -> text
    text
    // -> text
    text
    // -> text

    Pass custom component as prop


    View Slide


  40. Pass custom component as prop

    import * as React from "react";
    import styles from "./styles.css";
    export function Text({ component, children, ...props }) {
    Component = component || "span";

    return (

    {children}

    );
    }
    components/Font/index.js

    View Slide


  41. Pass custom component as prop

    import * as React from "react";
    import styles from "./styles.css";
    export function Text({ component, children, ...props }) {
    Component = component || "span";

    return (

    {children}

    );
    }
    components/Font/index.js
    %

    View Slide

  42. import * as React from "react";
    import styles from "./styles.css";
    export function Text({ component, ...props }) {
    Component = component || "span";
    return ;
    }

    Pass custom component as prop

    &
    components/Font/index.js

    View Slide

  43. import * as React from "react";
    import Font from "components/Font";
    text
    // -> Text

    View Slide

  44. import * as React from "react";
    import Font from "components/Font";
    import styles from "./styles.css";
    text
    // -> text

    Pass extra class name


    View Slide

  45. import * as React from "react";
    import styles from "./styles.css";
    import classNames from "classnames";
    export function Text({ component, className, ...props }) {
    Component = component || "span";

    return }

    Pass extra class name

    components/Font/index.js

    View Slide

  46. yarn install "classnames"

    View Slide

  47. import * as React from "react";
    import styles from "./styles.css";
    import classNames from "classnames";
    export function Text({ component, className, ...props }) {
    Component = component || "span";
    return ;
    }
    components/Font/index.js

    View Slide

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

    View Slide

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

    View Slide

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


    View Slide

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


    View Slide

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


    View Slide

  53. function isFollowing(user: User, 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

  54. function isFollowing(user: User, 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

  55. function isFollowing(user: User, 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

  56. function isFollowing(user: User, 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



  57. View Slide





  58. View Slide





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

    View Slide





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

    View Slide

  61. View Slide

  62. /* @flow */

    import * as React from "react";
    import styles from "./styles.css";
    import classNames from "classnames";
    type Props = {
    component: any,
    className?: string
    };
    export function Text({ component, className, ...props }: Props) {
    Component = component || "span";
    return (
    );
    }

    Type safety

    components/Font/index.js

    View Slide

  63. View Slide

  64. /* @flow */
    import * as React from "react";
    import classNames from "classnames";
    import styles from "./styles.css";
    type Props = {
    className?: ?string,
    color?: "black" | "grey" | "orange" | "silver" | "white" | "error",
    component?: any,
    size?: "xSmall" | "small" | "medium" | "large" | "xLarge",
    uppercase?: boolean,
    weight?: "light" | "normal" | "semiBold" | "bold"
    };
    export default function Font({
    className,
    color,
    component,
    size,
    uppercase,

    Component packages

    components/Font/index.js

    View Slide

  65. weight?: "light" | "normal" | "semiBold" | "bold"
    };
    export default function Font({
    className,
    color,
    component,
    size,
    uppercase,
    weight,
    ...props
    }: Props) {
    const Component = component || "span";
    const classes = classNames(
    styles.font,
    styles[color],
    styles[size],
    styles[weight],
    uppercase && styles.uppercase,
    className
    );
    return ;
    }
    Font.Text = (props: Props) => (

    Component packages

    components/Font/index.js

    View Slide

  66. return ;
    }
    Font.Text = (props: Props) => (

    );
    Font.SecondaryText = (props: Props) => (

    );
    Font.Title = (props: Props) => (

    );
    Font.Featured = (props: Props) => (

    );
    Font.Headline = (props: Props) => (

    );

    Component packages

    components/Font/index.js

    View Slide

  67. import Font from "components/Font";

    text
    text
    text
    text
    text

    Component packages


    View Slide

  68. import Form from "components/Form";









    Component packages


    View Slide

  69. ! Generic
    " Utility
    # Domain

    Component types


    View Slide

  70. ✅ Generic
    " Utility
    # Domain

    Component types


    View Slide

  71. ✅ Generic
    " Utility
    # Domain

    Component types


    View Slide

  72. Utility components

    View Slide


  73. View Slide

  74. /* @flow */
    import * as React from 'react';
    import { throttle } from 'lodash';
    type Props = {
    onResize: ({| height: number, width: number |}) => void,
    };
    export default class WindowResize extends React.Component {
    componentDidMount() {
    this.handleResize();
    window.addEventListener('resize', this.handleResize);
    }
    componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
    }
    shouldComponentUpdate() {

    Renderless components

    components/WindowResize/index.js

    View Slide

  75. window.addEventListener('resize', this.handleResize);
    }
    componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
    }
    shouldComponentUpdate() {
    return false;
    }
    render() {
    return null;
    }
    handleResize = throttle(() => {
    this.props.onResize({
    height: window.innerHeight,
    width: window.innerWidth,
    });
    }, 500);
    }

    Renderless components

    components/WindowResize/index.js

    View Slide





  76. View Slide


  77. Function as props


    {error => {error}}

    View Slide

  78. /* @flow */
    import classNames from "classnames";
    import * as React from "react";
    import styles from "./styles.css";
    import { formContextTypes, getErrorMessage } from "./utils";
    import type { FormContextType } from "./types";
    type Props = {
    name: string,
    children?: Function
    };
    export default function ErrorMessage(
    { name, children }: Props,
    { form }: FormContextType
    ) {
    const message = getErrorMessage(form, name);
    if (!message) {
    return null;

    Function as props

    components/Form/ErrorMessage/index.js

    View Slide

  79. children?: Function
    };
    export default function ErrorMessage(
    { name, children }: Props,
    { form }: FormContextType
    ) {
    const message = getErrorMessage(form, name);
    if (!message) {
    return null;
    }
    if (typeof children === "function") {
    return children(message) || null;
    }
    return {message};
    }
    ErrorMessage.defaultProps = { name: "base"};
    ErrorMessage.contextTypes = formContextTypes;

    Function as props

    components/Form/ErrorMessage/index.js

    View Slide


  80. {currentTime => (

    )}


    Function as props


    View Slide

  81. /* @flow */
    import * as React from "react";
    import moment from "moment";
    import type { Moment } from "types/Moment";
    type Props = {
    children: (time: Moment) => any
    };
    type State = {
    time: Moment
    };
    export default class Clock extends React.Component {
    state = { time: moment() };
    intervalId = null;
    componentDidMount() {
    this.updateTime();

    Function as props

    components/Clock/index.js

    View Slide

  82. time: Moment
    };
    export default class Clock extends React.Component {
    state = { time: moment() };
    intervalId = null;
    componentDidMount() {
    this.updateTime();
    if (!this.intervalId) {
    this.intervalId = setInterval(this.updateTime, 1000);
    }
    }
    componentWillUnmount() {
    if (this.intervalId) {
    clearInterval(this.intervalId);
    this.intervalId = null;
    }
    }
    updateTime = () => {

    Function as props

    components/Clock/index.js

    View Slide

  83. this.updateTime();
    if (!this.intervalId) {
    this.intervalId = setInterval(this.updateTime, 1000);
    }
    }
    componentWillUnmount() {
    if (this.intervalId) {
    clearInterval(this.intervalId);
    this.intervalId = null;
    }
    }
    updateTime = () => {
    this.setTime({ time: moment() });
    };
    render() {
    return this.children(this.state.time);
    }
    }

    Function as props

    components/Clock/index.js

    View Slide




  84. View Slide







  85. View Slide


  86. {isLoggedIn => (isLoggedIn ? : )}

    View Slide




  87. View Slide


  88. {isMobile => (isMobile ? : )}

    View Slide




  89. View Slide

  90. ✅ Generic
    " Utility
    # Domain

    Component Types


    View Slide

  91. ✅ Generic
    ✅ Utility
    # Domain

    Component Types


    View Slide

  92. ✅ Generic
    ✅ Utility
    # Domain

    Component Types


    View Slide

  93. Domain components

    View Slide

  94. View Slide

  95. View Slide

  96. View Slide

  97. View Slide







  98. View Slide







  99. View Slide







  100. View Slide







  101. View Slide







  102. View Slide







  103. View Slide

  104. /* @flow */
    import * as React from "react";
    import Font from "components/Font";
    import Link from "components/Link";
    import TopicFollowButton from "components/TopicFollowButton";
    import TopicImage from "components/TopicImage";
    import classNames from "classNames";
    import paths from "paths";
    import styles from "./styles.css";
    import type { TopicItemFragament as Topic } from "graphql/schema.js";
    type Props = {
    topic: Topic,
    className?: string
    };
    export default function TopicItem({ topic, className }: Props) {
    return (


    components/TopicItem/index.js

    Domain component


    View Slide

  105. type Props = {
    topic: Topic,
    className?: string
    };
    export default function TopicItem({ topic, className }: Props) {
    return (





    {topic.name}
    {topic.description}



    );
    }
    components/TopicItem/index.js

    Domain component


    View Slide

  106. components/
    TopicItem/

    Fragment.graphql
    index.js
    styles.css

    Component as folder


    View Slide

  107. #import "components/TopicFollowButton/Fragment.graphql"
    #import "components/TopicImage/Fragment.graphql"
    fragment TopicItem on Topic {
    id
    name
    slug
    description
    ...TopicFollowButton
    ...TopicImage
    }
    components/TopicItem/Fragment.graphql

    GraphQL fragment


    View Slide

  108. TopicItem
    TopicImage
    TopicFollowButton

    View Slide

  109. View Slide

  110. View Slide

  111. components/
    TopicFollowButton/

    Fragment.graphql
    Mutation.graphql
    index.js
    styles.css

    Component as folder


    View Slide

  112. fragment TopicFollowButton on Topic {
    id
    name
    isFollowed
    }

    GraphQL fragment

    components/TopicFollowButton/Fragment.graphql

    View Slide

  113. mutation TopicFollowToggle($input: TopicFollowToggleInput!) {
    topicFollowToggle(input: $input) {
    node {

    id
    isFollowed

    }
    }
    }
    components/TopicItem/Mutation.graphql

    GraphQL mutation


    View Slide

  114. /* @flow */
    import * as React from "react";
    import Button from "ph/components/Button";
    import MUTATION from "./Mutation.graphql";
    import openLogin from "ph/utils/openLogin";
    import type { TopicButtonFragament as Topic } from "ph/graphql/
    schema.js";
    import { createContainer } from "ph/lib/container";
    import { graphql } from "react-apollo";
    type Props = {
    className?: string,
    isLogged: boolean,
    topic: Topic,
    toggleFollow: Function
    };
    export class TopicFollowButton extends React.Component {
    render() {
    const isFollowed = this.props.topic.isFollowed;

    Action button

    components/TopicItem/index.graphql

    View Slide

  115. topic: Topic,
    toggleFollow: Function
    };
    export class TopicFollowButton extends React.Component {
    render() {
    const isFollowed = this.props.topic.isFollowed;
    return (
    active={isFollowed}
    className={this.props.className}
    onClick={this.handleClick}
    data-test="topic-toggle-follow-button"
    >
    {isFollowed ? "Following" : "Follow"}

    );
    }
    handleClick: Function = async () => {
    if (this.props.isLogged) {
    this.props.toggleFollow(!this.props.topic.isFollowed);
    } else {
    openLogin({ reason: `Follow ${this.props.topic.name}` });

    Action button

    components/TopicItem/index.graphql

    View Slide

  116. );
    }
    handleClick: Function = async () => {
    if (this.props.isLogged) {
    this.props.toggleFollow(!this.props.topic.isFollowed);
    } else {
    openLogin({ reason: `Follow ${this.props.topic.name}` });
    }
    };
    }
    export default createContainer({
    renderComponent: TopicFollowButton,
    decorators: [
    graphql(MUTATION, {
    props: ({ ownProps, mutate }) => ({
    toggleFollow(follow) {
    return mutate({
    variables: {
    input: { id: ownProps.topic.id, follow: follow }
    }
    });
    }

    Action button

    components/TopicItem/index.graphql

    View Slide

  117. export default createContainer({
    renderComponent: TopicFollowButton,
    decorators: [
    graphql(MUTATION, {
    props: ({ ownProps, mutate }) => ({
    toggleFollow(follow) {
    return mutate({
    variables: {
    input: { id: ownProps.topic.id, follow: follow }
    }
    });
    }
    })
    })
    ],
    mapStateToProps({ currentUser }) {
    return {
    isLogged: currentUser.id
    };
    }
    });

    Action button

    components/TopicItem/index.graphql

    View Slide

  118. View Slide

  119. View Slide

  120. }>
    }>










    View Slide

  121. TopicPage TopicItem
    TopicImage
    TopicFollowButton

    View Slide

  122. ✅ Generic
    ✅ Utility
    # Domain

    Component Types


    View Slide

  123. ✅ Generic
    ✅ Utility
    ✅ Domain

    Component Types


    View Slide

  124. ✅ Generic
    ✅ Utility
    ✅ Domain

    Component Types


    View Slide

  125. View Slide

  126. Thanks '

    View Slide


  127. https://github.com/rstankov/talks-code


    View Slide