React best practices

Christoffer Niska

CTO at Nord Software

crisu83 on GitHub

Build process

NPM scripts

Automated tests

Stateless components

import React from 'react'

const Button => ({ onClick, isDisabled }) => (
  <button className="button" onClick={onClick} disabled={isDisabled}/>
)

export default Button

components/button.js

High-order components

Providers

import React, { Component, PropTypes, Children } from 'react'

class ThemeProvider extends Component {
  static propTypes = {
    themeName: PropTypes.string.isRequired,
    children: PropTypes.element.isRequired
  }

  static childContextTypes = {
    theme: PropTypes.object.isRequired
  }

  getChildContext() {
    return {
      theme: {
        name: this.props.themeName
      }
    }
  }

  render() {
    return Children.only(this.props.children)
  }
}
import React from 'react'
import { render } from 'react-dom'
import ThemeProvider './theme/provider'

render(
  <ThemeProvider name="my-theme">
    // ...
  </ThemeProvider>
, document.getElementById('root'))

theme/provider.js

index.js

Decorators

import { Component, PropTypes } from 'react'

const Themeable = ComposedComponent => class extends Component {
  static contextTypes = {
    theme: PropTypes.object.isRequired
  }

  render() {
    const { theme } = this.context
    return <ComposedComponent {...this.props} themeName={theme.name}/>
  }
}
import React from 'react'
import Themeable from './theme/decorator';

@Themeable
const Button => ({ themeName }) => (
  // ...
)

export default Button

theme/decorator.js

components/button.js

Immutable application state

import { expect } from 'chai'
import { fromJS } from 'immutable'

describe('Todos state', () => {

  it('adds todos', () => {
    const state = fromJS([]);
    const nextState = module.handleAdd(state, { payload: { id: 1, text: 'Buy beer' } })
    expect(nextState).to.include(fromJS({ id: 1, text: 'Buy beer' }))
  })

  it('removes todos', () => {
    const state = fromJS([
      { id: 1, text: 'Buy beer' },
      { id: 2, text: 'Pick up the kids' }
    ])
    const nextState = module.handleRemove(state, { payload: 2 })
    expect(nextState).to.be.empty
  })

})

state/modules/todos-spec.js

import { fromJS, List } from 'immutable'
import { createAction, handleActions } from 'redux-actions'

const ADD = 'todos/ADD';
const REMOVE = 'todos/REMOVE';

export function handleAdd(state, action) {
  return state.push(fromJS(action.payload)))
}

export function handleRemove(state, action) {
  return state.filter(todo => todo.get('id') !== action.payload))
}

const defaultState = List()

const reducer = handleActions({
  [ADD]: handleAdd,
  [REMOVE]: handleRemove
}, defaultState)

export const addTodo = createAction(ADD)
export const removeTodo = createAction(REMOVE)

export default reducer

state/modules/todos.js

import { combineReducers, applyMiddleware, createStore } from 'redux'
import promiseMiddleware from 'redux-promise'
import { default as todos } from './modules/todos';

export function finalCreateStore(initialState, history) { 
  return createStore(
    combineReducers({ todos, ... }),
    initialState,
    applyMiddleware(promiseMiddleware)
  )
}

state/index.js

import React from 'react'
import { render } from 'react-dom'
import { Router, browserHistory } from 'react-router'
import { Provider as StateProvider } from 'react-redux'
import { finalCreateStore } from 'state/index'
import routes from './routes';

const store = finalCreateStore();

render(
  <StateProvider store={store}>
    <Router history={browserHistory} routes={routes}/>
  </StateProvider>, 
  document.getElementById('root')
)

index.js

import React from 'react'
import { connect, bindActionCreators } from 'react-redux'
import { addTodo, removeTodo } from '../state/modules/todos'

export const TodoItem = ({ data, onRemove }) => (
  <li className="todo-item" key={data.id}>
    {data.text} <span onClick={onRemove.bind(null, id)}>x</span>
  </li>
)

export const TodoList = ({ data, onRemove }) => (
  <ul className="todo-list">{data.map(todo => <TodoItem data={todo} onRemove={onRemove}/>)}</ul>
)

function mapStateToProps(state) {
  return { data: state.todos }
}

@connect(
  mapStateToProps, 
  bindActionCreators({ addTodo: onAdd, removeTodo: onRemove })
)
export const Todos = ({ data, onAdd, onRemove }) => (
  <div className="todos">
    <TodoList data={data} onRemove={onRemove}/>
    ...
  </div>
)

export default Todos

index.js

Prop types and default props

import React, { PropTypes } from 'react'
import { List } from 'immutable'

export const TodoItem ...

TodoItem.propTypes = {
  data: PropTypes.shape({
    id: PropTypes.number.isRequired
    text: PropTypes.string.isRequired,
  }).isRequired,
  onRemove: PropTypes.func.isRequired
}

export const TodoList ...

TodoList.propTypes = {
  data: PropTypes.instanceOf(List),
  onRemove: PropTypes.func
}

TodoList.defaultProps = {
  data: List()
}

components/todos.js

Routing

import React from 'react'
import { Route } from 'react-router'
import Todos from './components/todos';

const routes = {
  index: '/',
  todos: '/todos'
}

export function getRoute(name) {
  return routes[name]
}

export default (
  <Route path={routes.index}>
    <Route path={routes.todos} component={Todos}/>
    ...
  </Route>
)

routes.js

import React from 'react'
import { render } from 'react-dom'
import { Router, browserHistory } from 'react-router'
import routes from './routes';

render(
  <Router history={browserHistory} routes={routes}/>, 
  document.getElementById('root')
)

index.js

Named components

export default (
  <Route path={routes.index} component={App}>
    <Route path={routes.todos} components={{ main: Todos, sidebar: TodosSidebar }}/>
    ...
  </Route>
)

routes.js

import React from 'react'

export const App = ({ main, sidebar }) => (
  <div className="app">
    <div className="app-sidebar">{sidebar}</div>
    <div className="app-main">{main}</div>
  </div>
)

export default App

components/app.js

Single responsibility principle

Thank you!

Questions?

React best practices

By Christoffer Niska

React best practices

  • 1,782