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. early 2014 jQuery spaghetti October 2014 Backbone February 2015 React

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

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

    2015 Redux January 2016 React-Router April 2016 Redux Ducks Febuary 2017 GraphQL
  7. 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
  8. 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

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

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

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

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

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

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

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

  21. 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
  22. 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
  23. export type Topic = { id: number, image_uuid?: ?string, name:

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

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

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

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

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

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

  28. 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);
  29. 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); ' (
  30. <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" />
  31. <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() { /* ... */ } }
  32. <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() { /* ... */ } }
  33. /* @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
  34. /* @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
  35. 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
  36. 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
  37. 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

  38. /* @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
  39. 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
  40. /* @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
  41. 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
  42. /* @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
  43. 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
  44. 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
  45. /* @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

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

  47. #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

  48. fragment TopicFollowButton on Topic { id name isFollowed } 


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

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

  50. /* @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
  51. 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
  52. ); } 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
  53. 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
  54. <Box actions={<SearchField />}> <InfiniteScroll loading={<TopicPlaceHolder />}> <TopicsList> <TopicItem> <TopicImage />

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