Pro Yearly is on sale from $80 to $50! »

React Patterns in ProductHunt

React Patterns in ProductHunt

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

July 08, 2018
Tweet

Transcript

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

  2. Radoslav Stankov @rstankov blog.rstankov.com github.com/rstankov
 twitter.com/rstankov

  3. None
  4. None
  5. None
  6. None
  7. early 2014 jQuery spaghetti October 2014 Backbone February 2015 React

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

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

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

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

    Febuary 2017 GraphQL
  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
  16. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

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

    entry.js paths.js reducers.js routes.js
  18. 
 https://speakerdeck.com/rstankov/react-at-product-hunt-wad


  19. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  20. 
 https://speakerdeck.com/rstankov/graphql-at-product-hunt


  21. components/ graphql/
 layouts/ lib/ modules/ pages/ styles/ types/ utils/
 config.js

    entry.js paths.js reducers.js routes.js
  22. ! Generic " Utility # Domain $ Pages 
 Component

    types

  23. ! Generic " Utility # Domain 
 Component types


  24. ! Generic " Utility # Domain 
 Component types


  25. Generic components

  26. None
  27. components/ Font/
 index.js styles.css 
 
 Component as folder


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


    Component as folder

  29. components/ Font/
 index.js styles.css 
 
 Component as folder


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

    <Font.Text>{text}</Font.Text>
  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

  32. components/ Font/
 index.js styles.css 
 
 CSS Modules


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

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

    component.js
 import styles from './styles.css'; <span className={styles.text}> </span>
  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>

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

    <Font.Text>text</Font.Text> // -> <span class="text">text</span>
  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>
  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>
  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

  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
  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 %
  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
  43. import * as React from "react"; import Font from "components/Font";

    <Font.Text>text</Font.Text> // -> <span class="text">Text</span>
  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

  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
  46. yarn install "classnames"

  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
  48. https://github.com/facebook/flow

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

    string, description?: string, followers_count: number, posts_count: number, slug: string, };
  50. function isFollowing(user: User, topic: Topic): boolean { return user.followedTopicsIds.indexOf(topic.id) !==

    -1; } 
 

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

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

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

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

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

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

  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);
  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); ' (
  57. <UserImage user={user} width={50} height={30} /> <UserImage user={user} variant="small" />

  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" />
  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() { /* ... */ } }
  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() { /* ... */ } }
  61. None
  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
  63. None
  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
  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
  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
  67. import Font from "components/Font";
 <Font.Text>text</Font.Text> <Font.SecondaryText>text</Font.SecondaryText> <Font.Title>text</Font.Title> <Font.Featured>text</Font.Featured> <Font.Headline>text</Font.Headline> 


    Component packages

  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

  69. ! Generic " Utility # Domain 
 Component types


  70. ✅ Generic " Utility # Domain 
 Component types


  71. ✅ Generic " Utility # Domain 
 Component types


  72. Utility components

  73. <WindowResize onResize={handleResize} />

  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
  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
  76. <WindowResize onResize={handleResize} />
 <ScrollObserver onScroll={handleScroll} /> <KeyHandler keyEventName="press" keyValue="s" onKeyHandle={onPress}

    /> <AddBodyClassName className={className} />
  77. 
 Function as props
 <Form.ErrorMessage name="name">
 {error => <Font.Text color="error">{error}</Font.Text>}

    </Form.ErrorMessage>
  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
  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
  80. <Clock>
 {currentTime => ( <Time time={currentTime} format="hh:mm:ss" /> )} </Clock>

    
 Function as props

  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
  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
  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
  84. <Visible if="isLoggedIn"> <LogoutButton /> </Visible>


  85. <Visible if="isLoggedIn"> <LogoutButton /> </Visible>
 <Visible unless="isLoggedIn"> <LoginButton /> </Visible>

  86. <Visible if="isLoggedIn"> {isLoggedIn => (isLoggedIn ? <LogoutButton /> : <LoginButton

    />)} </Visible>
  87. <Visible unless="screenSize" args="mobile"> <SideBar /> </Visible>

  88. <Visible if="screenSize" args="mobile"> {isMobile => (isMobile ? <TabBar /> :

    <Sidebar />)} </Visible>
  89. <Visible if="featureEnabled" args="secretFeature"> <Component /> </Visible>

  90. ✅ Generic " Utility # Domain 
 Component Types


  91. ✅ Generic ✅ Utility # Domain 
 Component Types


  92. ✅ Generic ✅ Utility # Domain 
 Component Types


  93. Domain components

  94. None
  95. None
  96. None
  97. None
  98. <TopicItem> <TopicImage /> <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem>

  99. <TopicItem> <TopicImage /> <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem>

  100. <TopicItem> <TopicImage /> <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem>

  101. <TopicItem> <TopicImage /> <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem>

  102. <TopicItem> <TopicImage /> <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem>

  103. <TopicItem> <TopicImage /> <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem>

  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

  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

  106. components/ TopicItem/
 Fragment.graphql index.js styles.css 
 Component as folder


  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

  108. TopicItem TopicImage TopicFollowButton

  109. None
  110. None
  111. components/ TopicFollowButton/
 Fragment.graphql Mutation.graphql index.js styles.css 
 Component as folder


  112. fragment TopicFollowButton on Topic { id name isFollowed } 


    GraphQL fragment
 components/TopicFollowButton/Fragment.graphql
  113. mutation TopicFollowToggle($input: TopicFollowToggleInput!) { topicFollowToggle(input: $input) { node {
 id

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

  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
  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
  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
  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
  118. None
  119. None
  120. <Box actions={<SearchField />}> <InfiniteScroll loading={<TopicPlaceHolder />}> <TopicsList> <TopicItem> <TopicImage />

    <Font.Title /> <Font.SilentText /> <TopicFollowButton /> </TopicItem> </TopicsList> </InfiniteScroll> </Box>
  121. TopicPage TopicItem TopicImage TopicFollowButton

  122. ✅ Generic ✅ Utility # Domain 
 Component Types


  123. ✅ Generic ✅ Utility ✅ Domain 
 Component Types


  124. ✅ Generic ✅ Utility ✅ Domain 
 Component Types


  125. None
  126. Thanks '

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