Repozytorium Web Developera

Archiwum z lat 2013-2018, treści mogą być nieaktualne.

React patterns

  • 10 React mini-patterns, worth reading:
    • #4 Controlling CSS with props
    • #5 The switching component
    • #7 Almost-components: Don’t prematurely componentize. Components aren’t like teaspoons; you can have too many.
    • #9 The store is the component’s servant: For this last point, I recommend a single module that does all the massaging of incoming data (oh la la). Renaming props, casting strings to numbers, objects into arrays, date strings to date objects, whatever. Do it all in the one place, and unit test the crap out of it.
  • The Common Patterns of React
    • Higher Order Components
    • Function as Child Component
    • React Context for data propagation
    • Notification callbacks
    • Delegation for passing multiple callbacks
    • Container and Presentation Components
    • Compound Components

Container Component approach

The container of a component is responsible for fetching data. Below you can see an example of a container component, that is passing its state to its child component:


import React from 'react'
import CommentList from './component/CommentList'

class CommentListContainer extends React.Component {
  state = {
    comments: []
  }

  componentDidMount () {
    fetchSomeComments(comments =>
      this.setState({ comments: comments })
    )
  }

  render () {
    return (
      <CommentList comments={this.state.comments} />
    )
  }
}

Below is the component, which data is passed to by the container - in this example comments were passed:


import React from 'react'

const CommentList = ({
  comments = []
}) => {
  return (
    <ul>
      {
        comments.map(comment => (
          <li>{comment.body}—{comment.author}</li>
        ))
      }
    </ul>
  )
}

Below is an example without using Container Component approach:


class CommentList extends React.Component {
  state = {
    comments: []
  }

  componentDidMount () {
    fetchSomeComments(comments =>
      this.setState({ comments: comments })
    )
  }

  render () {
    return (
      <ul>
        {
          this.state.comments.map(comment => (
            <li>{comment.body}—{comment.author}</li>
          ))
        }
      </ul>
    )
  }
}

Stateless component

Stateless component doesn't have internal state or lifecycle methods. It is using functional approach and an example component could look like below:


const CommentList = ({
  comments = []
}) => {
  return (
    <ul>
      {
        comments.map(comment => (
          <li>{comment.body}—{comment.author}</li>
        ))
      }
    </ul>
  )
}

Below one is an equivalent:


const CommentList = props => {
  return (
    <ul>
      {
        props.comments.map(comment => (
          <li>{comment.body}—{comment.author}</li>
        ))
      }
    </ul>
  )
}

CommentList.defaultProps = {
  comments: []
}

Anyway, don’t convert all classes to functional components because some lint rule told you it’s good. Don’t separate "presentational components" until you actually use them twice. Etc. Separating everything out prematurely also makes changes harder, just in a different way.

Reusing parent components like lists for an another type of a child component by using React.createElement method

In below example, we will use the List component to render lists of two types of components: Profile and Post component.

Profile.js


import React from 'react'

export default class Profile extends React.Component {
  renderDetails (key, label) {
    if (this.props[key]) {
      return (
        <div className="detail">{ label } { this.props[key] }</div>
      )
    }
  }

  render () {
    return (
      <li>
        <img href={ this.props.imagePath } align="left" width="30" height="30" />
        <div className="profile-description">
          { this.props.description }
        </div>
        { this.renderDetails('email', 'Email:') }
        { this.renderDetails('twitter', 'Twitter:') }
        { this.renderDetails('phone', 'Phone:') }
      </li>
    )
  }
}

List.js


import React from 'react'
import Profile from './Profile'

class List extends React.Component {
  render () {
    return (
      <ul>
        {
          this.props.profile.map((profile, index) => {
            let newProps = Object.assign({ key: index }, profile)
            return React.createElement(this.props.itemRenderer, newProps)
          })
        }
      </ul>
    )
  }
}

List.propTypes = { itemRenderer: React.PropTypes.func }
List.defaultProps = { profile: [], itemRenderer: Profile }

export default List

index.js


import React from 'react'
import ReactDOM from 'react-dom'
import List from './components/List'
import Profile from './components/Profile'
import Post from './components/Post'

let profileData = [ ... ] // psuedo code, this has all our profile data
let postsData = [ ... ] // psuedo code, this has all our post data

class App extends React.Component {
  render () {
    return (
      <div>
        <List items={ profileData } />
        <List items={ postsData } itemRenderer={ Post } />
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('mount-point'))

Source

HOC - Higher Order Component

HOC extends the state/behavior of the inner component in a composable way. You can use many HOCs on the inner component. HOCs are similar to Higher Order Functions. There are several ways to create HOC what you'll find described in this article.

formGroup.js


import React from 'react'
import { isString } from 'lodash'

function formGroup (Component, config) {
  const FormGroup = React.createClass({
    __renderLabel () {
      // check if the passed value is a string using Lodash#isString
      if (isString(this.props.label)) {
        return (
          <label className="form-label" htmlFor={ this.props.name }>
            { this.props.label }
          </label>
        )
      }
    },

    __renderElement () {
      // We need to see if we passed a Component or an Element
      // such as Profile vs. <input type="text" />
      if (React.isValidElement(Component)) {
        return React.cloneElement(Component, this.props)
      }
      return (
        <Component { ...this.props } />
      )
    },

    render () {
      return (
        <div className="form-group">
          { this.__renderLabel() }
          { this.__renderElement() }
        </div>
      )
    }
  })

  return (
    <FormGroup { ...config } />
  )
}

export default formGroup

index.js


import React from 'react'
import ReactDOM from 'react-dom'
import formGroup from './higherOrderComponents/formGroup'

let MyComponent = React.createClass({
  render () {
    return (
      <div>
        {
          formGroup(
            <input type="text" />,
            { label: 'First Name:', name: 'firstName' }
          )
        }
      </div>
    )
  }
})

ReactDOM.render(<MyComponent />, document.getElementById('mount-point'))

Source

Render Props - Function as Child Component - as an alternative to HOCs

You have to create a component with a render property (i.e. <Mouse render={value => <span>{value}</span>). The component can pass values (value) to a function defined in render prop (i.e. in render you use {this.props.render(this.state)} to pass this.state to the render function property). Then you can access the passed values in the render function (<Mouse render={value => <span>{value}</span>).

The render props were used in library react-motion which helps you create smooth animations in React.


class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

Switching component (components mapper)

Discussed in 10 React mini-patterns, #5 The switching component.


import HomePage from './HomePage.jsx'
import AboutPage from './AboutPage.jsx'
import UserPage from './UserPage.jsx'
import FourOhFourPage from './FourOhFourPage.jsx'

const PAGES = {
  home: HomePage,
  about: AboutPage,
  user: UserPage
}

const Page = (props) => {
  const Handler = PAGES[props.page] || FourOhFourPage
  
  return <Handler {...props} />
}

Page.propTypes = {
  page: PropTypes.oneOf(Object.keys(PAGES)).isRequired
}

Don’t prematurely componentize

Discussed in 10 React mini-patterns, #7 Almost-components.

Don’t prematurely componentize. Components aren’t like teaspoons; you can have too many.
What I am not saying: “take something that you think should be a component, and make it part of the parent component.”
What I am saying: “take something that you don’t think should be a component, and make it a bit more like its own component (if it can be).”

I think


const SearchSuggestions = (props) => {
  // renderSearchSuggestion() behaves as a pseduo SearchSuggestion component
  // keep it self contained and it should be easy to extract later if needed
  const renderSearchSuggestion = listItem => (
    <li key={listItem.id}>{listItem.name} {listItem.id}</li>
  )
  
  return (
    <ul>
      {props.listItems.map(renderSearchSuggestion)}
    </ul>
  )
}

Don't process data in component but in store

Discussed in 10 React mini-patterns, #9 The store is the component’s servant.

For this last point, I recommend a single module that does all the massaging of incoming data (oh la la). Renaming props, casting strings to numbers, objects into arrays, date strings to date objects, whatever.
Do it all in the one place, and unit test the crap out of it.

fetch(`/api/search?${queryParams}`)
  .then(response => response.json())
  .then(normalizeSearchResultsApiData) // the do-it-all data massager
  .then(normalData => {
    // dispatch normalData to the store here
  })