React Patterns in ProductHunt

React Patterns in ProductHunt

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

July 08, 2018
Tweet

Transcript

  1. 3.
  2. 4.
  3. 5.
  4. 6.
  5. 8.

    early 2014 jQuery spaghetti October 2014 Backbone February 2015 React

    & Rails May 2015 custom Flux December 2015 Redux
  6. 9.

    early 2014 jQuery spaghetti October 2014 Backbone February 2015 React

    & Rails May 2015 custom Flux December 2015 Redux January 2016 React-Router
  7. 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
  8. 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
  9. 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
  10. 13.

    February 2015 React & Rails May 2015 custom Flux December

    2015 Redux January 2016 React-Router April 2016 Redux Ducks Febuary 2017 GraphQL
  11. 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
  12. 26.
  13. 31.

    components/Font/index.js import * as React from "react"; import styles from

    "./styles.css"; export function Text({ children }) { return ( <span className={styles.text}> {children} </span> ); } 
 Functional components

  14. 33.

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

    component.js
 import styles from './styles.css'; <span className={styles.text}> </span>
  15. 34.

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

    component.js
 import styles from './styles.css'; <span className={styles.text}> </span>
  16. 35.

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

    component.js
 import styles from './styles.css'; <span className={styles.text}> </span> CSS // style.css .text_3mRwv { font-size: 14px; } CSS // result.js <span class="text_3mRwv"> </span>

  17. 36.

    import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span>
  18. 37.

    import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span> <Font.Text component="p">text</Font.Text> // -> <p class="text">text</p>
  19. 38.

    import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span> <Font.Text component="p">text</Font.Text> // -> <p class="text">text</p> <Font.Text component={Link} to="/page">text</Font.Text> // -> <a class="text" href="/page">text</a>
  20. 39.

    import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">text</span> <Font.Text component="p">text</Font.Text> // -> <p class="text">text</p> <Font.Text component={Link} to="/page">text</Font.Text> // -> <a class="text" href="/page">text</a> 
 Pass custom component as prop

  21. 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 ( <Component className={styles.text} {...props}> {children} </Component> ); } components/Font/index.js
  22. 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 ( <Component className={styles.text} {...props}> {children} </Component> ); } components/Font/index.js %
  23. 42.

    import * as React from "react"; import styles from "./styles.css";

    export function Text({ component, ...props }) { Component = component || "span"; return <Component className={styles.text} {...props} />; } 
 Pass custom component as prop
 & components/Font/index.js
  24. 43.

    import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">Text</span>
  25. 44.

    import * as React from "react"; import Font from "components/Font";

    import styles from "./styles.css"; <Font.Text className={styles.custom}>text</Font.Text> // -> <span class="text custom">text</span> 
 Pass extra class name

  26. 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 <Component className={className(styles.text, className)} {...pro } 
 Pass extra class name
 components/Font/index.js
  27. 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 <Component className={className(styles.text, className)} />; } components/Font/index.js
  28. 49.

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

    string, description?: string, followers_count: number, posts_count: number, slug: string, };
  29. 51.

    function isFollowing(user: User, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); 
 

  30. 52.

    function isFollowing(user: User, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } isFollowing(null, topic); isFollowing(user, null); isFollowing(user, somethingElse); 
 

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

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

  33. 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);
  34. 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); ' (
  35. 58.

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

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

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

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

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

    user={user} width={50} variant="small" /> <UserImage user={user} width={50} height={30} variant="small" /> class UserImage extends React.Component { props: | {| user: User, width: number, height: number |} | {| user: User, variant: "big" | "medium" | "small" |}; render() { /* ... */ } }
  38. 61.
  39. 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 ( <Component className={className(styles.text, className)} {...props} / ); } 
 Type safety
 components/Font/index.js
  40. 63.
  41. 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
  42. 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 <Component className={classes} {...props} />; } Font.Text = (props: Props) => ( 
 Component packages
 components/Font/index.js
  43. 66.

    return <Component className={classes} {...props} />; } Font.Text = (props: Props)

    => ( <Font size="small" weight="normal" {...props} /> ); Font.SecondaryText = (props: Props) => ( <Font size="xSmall" uppercase={true} weight="normal" {...props} /> ); Font.Title = (props: Props) => ( <Font size="medium" weight="normal" {...props} /> ); Font.Featured = (props: Props) => ( <Font size="large" weight="light" {...props} /> ); Font.Headline = (props: Props) => ( <Font size="xLarge" weight="bold" {...props} /> ); 
 Component packages
 components/Font/index.js
  44. 68.

    import Form from "components/Form"; <Form data={settings} submit={submit} onSubmit={onSubmit}> <Form.Field name="name"

    /> <Form.Field name="headline" /> <Form.Field name="email" /> <Form.Field name="newsletter" type="select" options={OPTIONS} /> <Form.Field name="header" type={HeaderUploadInput} /> <Form.Buttons submitText="Update" /> </Form> 
 Component packages

  45. 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<Props> { componentDidMount() { this.handleResize(); window.addEventListener('resize', this.handleResize); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); } shouldComponentUpdate() { 
 Renderless components
 components/WindowResize/index.js
  46. 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
  47. 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
  48. 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 <Font.Text color="error">{message}</Font.Text>; } ErrorMessage.defaultProps = { name: "base"}; ErrorMessage.contextTypes = formContextTypes; 
 Function as props
 components/Form/ErrorMessage/index.js
  49. 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<Props, State> { state = { time: moment() }; intervalId = null; componentDidMount() { this.updateTime(); 
 Function as props
 components/Clock/index.js
  50. 82.

    time: Moment }; export default class Clock extends React.Component<Props, State>

    { 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
  51. 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
  52. 94.
  53. 95.
  54. 96.
  55. 97.
  56. 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 ( <div className={className(styles.item, className)}> <Link to={paths.topics.show(topic)} className={styles.image}> components/TopicItem/index.js 
 Domain component

  57. 105.

    type Props = { topic: Topic, className?: string }; export

    default function TopicItem({ topic, className }: Props) { return ( <div className={className(styles.item, className)}> <Link to={paths.topics.show(topic)} className={styles.image}> <TopicImage topic={topic} size={50} /> </Link> <Link to={paths.topics.show(topic)} className={styles.info}> <Font.Title>{topic.name}</Font.Title> <Font.SilentText>{topic.description}</Font.SilentText> </Link> <TopicFollowButton topic={topic} /> </div> ); } components/TopicItem/index.js 
 Domain component

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

  59. 109.
  60. 110.
  61. 112.

    fragment TopicFollowButton on Topic { id name isFollowed } 


    GraphQL fragment
 components/TopicFollowButton/Fragment.graphql
  62. 113.

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

    isFollowed
 } } } components/TopicItem/Mutation.graphql 
 GraphQL mutation

  63. 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<Props, {}> { render() { const isFollowed = this.props.topic.isFollowed; 
 Action button
 components/TopicItem/index.graphql
  64. 115.

    topic: Topic, toggleFollow: Function }; export class TopicFollowButton extends React.Component<Props,

    {}> { render() { const isFollowed = this.props.topic.isFollowed; return ( <LoadButton active={isFollowed} className={this.props.className} onClick={this.handleClick} data-test="topic-toggle-follow-button" > {isFollowed ? "Following" : "Follow"} </LoadButton> ); } 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
  65. 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
  66. 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
  67. 118.
  68. 119.
  69. 120.

    <Box actions={<SearchField />}> <InfiniteScroll loading={<TopicPlaceHolder />}> <TopicsList> <TopicItem> <TopicImage />

    <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem> </TopicsList> </InfiniteScroll> </Box>
  70. 125.
  71. 126.