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

  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