Note

Please be advised that this article is based on personal experimentation without any prior knowledge of React. The information may be incorrect. Please use at your own discretion.

In this article I will present what I learned about React from a Python developer point of view.

Package Manager

Python React
setup.py / setup.cfg package.json
requirements.txt package.json / yarn.lock
pip yarn

Yarn is the pip of JavaScript. The differences are:

  • It uses a virtualenv by default named node_modules.
  • It installs command line tools in node_modules/.bin/.
  • It generates a lock file to pin dependencies' version at install time.
  • It can execute "scripts" defined in the package.json, similar to tox.

Applications can be bootstraped using create-react-app.

This sets up a package.json file with commands to develop and distribute the application. This also generates a README.md file with 'get-started' information.

Create a new project:

#!/bin/sh -e
type -p yarn || {
  echo "Install yarn: https://yarnpkg.com/en/docs/install#centos-stable"
}
yarn create react-app my-app
cd my-app
# Check README.md for project structure details

# Start a hot-reload development server
yarn start
# Run test
yarn test
# Build production files
yarn build

# Add dependencies
yarn add patternfly-react react-router react-router-dom

More information about: Yarn, create-react-app and Use react-scripts.

Linter

Python React
flake8 eslint

Example configuration file:

# .eslintrc
---
parser: babel-eslint
plugins:
  - standard
  - jest
rules:
  no-console: off
  semi: [error, never]
  quotes: [error, single]
  lines-between-class-members: error
  space-before-function-paren: error
  react/prop-types: error
  react/jsx-key: error
  react/no-did-mount-set-state: error
  react/no-did-update-set-state: error
  react/no-deprecated: error
extends:
  # Replace recommened by all or strict for pedantic code style.
  - eslint:recommended
  - plugin:react/recommended
settings:
  react:
    version: 16.4
env:
  jest/globals: true
  browser: true

Notes: add a linter command to the package.json:

yarn add --dev eslint-plugin-react eslint-plugin-standard eslint-plugin-jest

// Add a new script in the package.json file:
//   "lint": "eslint --ext .js --ext .jsx src"

// Run lint
yarn lint

More information about: ESLint and React lint.

Language

Python React
Python ECMAScript 6
Jinja JSX

React applications are written in ECMAScript 6 (ES6) and the JSX syntax extension. ES6 is the new version of JavaScript (ES5), and JSX enables UI using HTML elements inline.

ES6

This python function:

def add(a, b):
  return (a + b)

can be written as:

function add (a, b) {
  return (a + b)
}
// or
const add = (a, b) => { return (a + b) }
// or using implicit return
const add = (a, b) => (a + b)

This python object's variables:

obj = {'a': 1, 'b': 2}
a = obj['a']

can be written as:

const obj = {a: 1, b: 2}
const a = obj['a']
// or
const a = obj.a
// or using destructuring assignment syntax
const { a } = obj

This python import statement:

import os.path as path

can be written as:

import { path } from 'os'

This python array/string manipulation:

array = [1, 2, 3, 4]
array.remove(2)
# array is now [1, 3, 4]

string = "Hello Python"
string[6:-2]
# return "Pyth"

can be written as:

const array = [1, 2, 3, 4]
array.splice(1, 1)
// splice(starting index, number of elem) removed the 2
// array is now [1, 3, 4]

const string = "Hello Python"
string.slice(6, -2)
// return "Pyth"
// slice(a, b) is similary to python [a:b]

More information about: Array reference and String reference.


This python exception handling code:

try:
  raise RuntimeError()
except Exception as e:
  print("Oops", e)

can be written as:

try {
  throw Error()
} catch (error) {
  console.error("Oops", error)
}

Convenient iterators:

const list = [{name: 'a'}, {name: 'b'}, {name: 'c'}]

for (let item of list){
  console.log(item.name)
}
// output a, b, c

list.forEach((item, idx) => {console.log(idx, item.name)})
// output 1 a, 2 b, 3 c

list.map((item) => (item.name))
// return ["a", "b", "c"]

list.map((item) => {
  if (item.name === 'a') {
     return 'A'
  } else {
     return item.name
  }
})
list.map((item) => (item.name === 'a' ? 'A' : item.name))
list.map((item) => (item.name === 'a' && 'A' || item.name))
// return ["A", "b", "c"]

list.filter(item => item.name !== 'a').map(item => item.name)
list.filter((item, idx) => idx >= 1).map(item => item.name)
// return ["b", "c"]

Note: use web console to try code snippets.

JSX

This pseudo python code:

title = 'Hello Python'
print('<h1>%s</h1>' % title)

Can be written as:

const title = 'Hello React'
return <h1>{title}</h1>

To embed dynamic content in UI elements, use {} delimiter.

const list = [{name: 'a'}, {name: 'b'}, {name: 'c'}]
return (
  <ul>
    {list.map(item => (<li>item.name</li>))}
  </ul>
)

More information about: JSX.

Component

Python React
class Component
self this

React components are similar to Python class, and they can be used as UI elements.

This pseudo python code:

class Title:
  def __init__(self, title):
    self.title = title

  def render(self):
    return '<h1>%s</h1>' % self.title

print(Title('Hello Python').render())

can be written as:

class Title extends React.Component {
  render () {
    const { name } = this.props
    return (<h1>{name}</h1>)
  }
}
const title = <Title name='Hello React' />

Notes about components:

  • Properties are static attributes given by the parent component:
    • They are set as HTML properties.
    • They are accessed through this.props.
    • They can't be changed.
  • Variables are stored in state:
    • They can be initialized as component constructor or class member.
    • They are set using this.setState({variableName: variableValue}).
    • They are accessed through this.state.
  • Component lifecycle methods are:
    • constructor(): invoked once when the component is created. State can be initialized during construction.
    • render(): invoked each time the states or property are updated. State can't be changed during render.
    • componentDidMount(): invoked immediately after a component is inserted into the DOM tree. State can be changed during componentDidMount. Network operations are usualy done here.
    • componentDidUpdate(prevProps, prevState): invoked immediately after updating occurs. This method is not called for the initial render. Network operations can be done here too. Be careful when updating the state; check prevState before to avoid a rendering loop.
    • componentWillUnmount(): invoked immediately after a component is removed from the DOM tree.

Any other component's function is static and this (self) reference is not available. To bind a function to the instance, you need to use oneline syntax:

class Counter extends React.Component {
  constructor () {
    super()
    this.state = {value: 0}
  }
  // This clicked method doesn't work, it is not binded
  clicked () {
    this.setState({value: this.state.value + 1})
  }
  // This clicked method works
  clicked = () => {
    this.setState({value: this.state.value + 1})
  }
  render () {
    return (
      <Button onClick={this.clicked}>
        {this.state.value}
      </Button>
    )
  }
}

More information about: Component.

Immutability

React manages component rendering through state update. Be carreful to not modify the state directly

state = {
  items: []
  object: {}
}
// This doesn't work. This will not re-render a component:
this.state.items.push('New item')
this.state.object.name = 'New name'

// This works but it's not recommended. use setState() method
const { items, object } = this.state;
items.push('New item');
object.name =  'New name';
this.setState({
  items: items,
  object: object
});

// Better is to treat this.state as if it were immutable and use setState callback
// ... operator is the javascript spread syntax
this.setState(prevState => ({
  items: [...prevState.items, 'New item'],
  object: {
    ...prevState.object,
    name: 'New name'
  }
}))

The spread syntax used to create a new element doesn't works with nested array or object. So React provides immutability helpers:

import update from 'react-addons-update'

newItems = update(items, {$push: ['New item']});
newObject = update(object, {$merge: {name: 'New name'}})

// To remove item, splice can be used:
const items = [1, 2, 3, 4, 5]
update(items, {$splice: [[1, 1]]})         // Removes 2
update(items, {$splice: [[1, 1, 0]]})      // Replaces 2 by 0
update(items, {$splice: [[4, 1], [0, 1]]}) // Removes 5 and 1
// NOTE: $splice parameter order matter, always go from highest index to lowest

More information about: Immutability Helpers.

Routing

Python React
argparse/click react-router

To load different components based on users' actions, use react-router:

  • the App component needs to be inside a <Router> object.
  • the App component uses <Switch> and <Route> to load needed component.
  • Navigation is performed with <Link>.
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom'
import { withRouter, Link, Redirect, Route, Switch } from 'react-router-dom'

class PageWelcome extends React.Component {
  render () { return (<h1>Page Welcome</h1>) }
}
class PageAbout extends React.Component {
  render () { return (<h1>Page About</h1>) }
}
class PageView extends React.Component {
  render () { return (<h1>Show {this.props.match.params.itemName}</h1>) }
}

class App extends React.Component {
  render () {
    return (
      <div>
        <ul>
          <li><Link to='/about'>About</Link></li>
          <li><Link to='/view/item1'>Show item 1</Link></li>
          <li><Link to='/view/item42'>Show item 42</Link></li>
        </ul>
        // React router will render the route component based on url
        <Switch>
          <Route path='/welcome' component={PageWelcome} />
          <Route path='/about' component={PageAbout} />
          <Route path='/view/:itemName' component={PageView} />
          <Redirect from='*' to='/welcome' key='default-route' />
        </Switch>
      </div>
    )
  }
}
// withRouter enables react router and adds location and history props
export default withRouter(App)

// Router top-level component needs to be used
ReactDOM.render(<Router><App /></Router>,
                document.getElementById('root'))

Notes about router:

  • BrowserRouter uses HTML5 URL, HashRouter uses '#/' anchor URL.
  • The Switch selects which page to render based on the URL.
  • The Route path property can include parameters that are automatically set to the props.match.params property.

More information about: Router.

To serve a BrowserRouter build installed in /usr/share/app, use this apache configuration:

<Directory /usr/share/app>
  Require all granted
</Directory>
Alias / /usr/share/app/
<Location />
  RewriteEngine on
  RewriteBase /
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_FILENAME} !-l
  # Any request that isn't a local file is served with index.html
  RewriteRule . /index.html [L]
</Location>

Note: to publish build with a sub-directory, change the 'homepage' setting in package.json to set a custom location for the static files.

HTTP Access

Python React
requests axios

The Axios library uses Promise, here is a demo that fetches the Software Factory zuul version number:

import React from 'react'
import Axios from 'axios'

const url = 'https://softwarefactory-project.io/zuul/api/tenant/local/status'

class StatusPage extends React.Component {
  state = {
    status: null
  }

  componentDidMount () {
    Axios.get(url)
      .then(response => {
        this.setState({status: response.data})
      })
      .catch(error => {
        console.log('Oops...')
      })
  }

  render () {
    const { status } = this.state
    if (!status) {
      return <p>Loading...</p>
    }
    return <p>Zuul version: {status.zuul_version}</p>
  }
}

Notes about Axios:

  • HTTP Verbs are function name:
    • Axios.post(url, data)
    • Axios.put(url, data)
    • Axios.delete(url)
    • ...
  • Axios takes care of json codec and it is compatible with older browsers.

More information about: Axios

PatternFly

The Patternfly-react module enables React binding.

List view example:

import { ListView } from 'patternfly-react'
import 'patternfly/dist/css/patternfly.min.css'
import 'patternfly/dist/css/patternfly-additions.min.css'

const itemList = [{'title': 'An item', 'content': 'Item content'}]
const listView = (
  <ListView>
    {itemList.map((item, idx) => (
      <ListView.Item
        heading={item.title}
        hideCloseIcon={true}
        key={idx}
        expanded
        >
        {item.content}
      </ListView.Item>
   ))}
  </ListView>
)

Table example:

import { Table } from 'patternfly-react'

const headFormat = value => <Table.Heading>{value}</Table.Heading>
const cellFormat = (value) => <Table.Cell>{value}</Table.Cell>
const columns = [{
  header: {label: 'Title', formatters: [headFormat]},
  property: 'title',
  cell: {formatters: [cellFormat]}
}, {
  header: {label: 'Content', formatters: [headFormat]},
  property: 'content',
  cell: {formatters: [cellFormat]}
}]
const table = (
  <Table.PfProvider
     striped
     bordered
     hover
     columns={columns}
     >
     <Table.Header/>
     <Table.Body
        rows={itemList}
        rowKey="title"
        />
  </Table.PfProvider>
)

Application framework example:

import React from 'react'
import { withRouter } from 'react-router'
import { Link, Redirect, Route, Switch } from 'react-router-dom'
import { Masthead } from 'patternfly-react'
import 'patternfly/dist/css/patternfly.min.css'
import 'patternfly/dist/css/patternfly-additions.min.css'

import logo from './images/logo.png'
// Routes can be defined using custom array, store it in a dedicated module.
import { routes } from './routes'

class App extends React.Component {
  constructor () {
    super()
    this.menu = routes()
  }

  // Automatically render a menu with buttons for route with a title.
  renderMenu = () => {
    const { location } = this.props
    const activeItem = this.menu.find(
      item => location.pathname === item.to
    )
    return (
      <ul className="nav navbar-nav navbar-primary">
        {this.menu.filter(item => item.title).map(item => (
          <li key={item.to} className={item === activeItem ? 'active' : ''}>
            <Link to={item.to}>{item.title}</Link>
          </li>
        ))}
      </ul>
    )
  }

  // Automatically render the Switch and Route from the routes custom array.
  renderContent = () => {
    const allRoutes = []
    this.menu.map((item, index) => {
      allRoutes.push(
        <Route key={index} exact
               path={item.to}
               component={item.component} />
      )
      return allRoutes
    })
    return (
      <Switch>
        {allRoutes}
        <Redirect from="*" to="/" key="default-route" />
      </Switch>
    )
  }

  // Render the body of the application.
  render () {
    return (
      <React.Fragment>
        <Masthead
          iconImg={logo}
          navToggle
          thin
          >
          <div className="collapse navbar-collapse">
            {this.renderMenu()}
            <ul className="nav navbar-nav navbar-utility">
              <li>
                <a href="https://docs.example.com/"
                   rel="noopener noreferrer" target="_blank">
                  Documentation
                </a>
              </li>
            </ul>
          </div>
        </Masthead>
        <div className="container-fluid container-cards-pf">
          {this.renderContent()}
        </div>
      </React.Fragment>
    )
  }
}
export default withRouter(App)
// routes.js
// A custom routing structure that is easy to maintain.
import Welcome from './pages/Welcome'
const routes = () => [
  {
    title: 'Welcome',
    to: '/',
    component: Welcome
  },
]
export { routes }

More information about: Icon lists, Patterns, Patternfly-react.

Store

Python React
global redux

To share a global context with any component, use a store with Redux and Thunk.

Redux lets you dispatch action and connect store to component's properties. This enables you to access global variable from nested components without having to pass the property all the way down. This also handles state transition and it provides powerful management.

Similarly to the react-router Browser, the App component needs to be inside a Provider object:

// index.js | the main entry point
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom'
import { Provider } from 'react-redux'

import { createMyStore } from './reducers'
import App from './App'

const store = createMyStore()
ReactDOM.render(
  <Provider store={store}>
    <Router><App /></Router>
  </Provider>,
  document.getElementById('root'))

Here is a reducer for the "Zuul status fetch" demoed previously:

// api.js | keep the network code in a dedicated module
import Axios from 'axios'
const api = 'https://softwarefactory-project.io/zuul/api/tenant/local/'
function fetchStatus () {
   return Axios.get(api + 'status')
}
export { fetchStatus }
// reducers.js | store management
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import { fetchStatus } from './api'

// Reducers process action and update state accordingly.
const statusReducer = (state = null, action) => {
  // state = null is the default state
  switch (action.type) {
    case 'FETCH_STATUS_SUCCESS':
      // when success action is dispatched, state becomes status
      return action.status
    default:
      return state
  }
}
function createMyStore () {
  // We can have multiple reducers for each context variable.
  return createStore(combineReducers({
    status: statusReducer,
  }), applyMiddleware(thunk))
}

// Actions to be dispatched.
function fetchStatusAction () {
  return (dispatch) => {
    return fetchStatus ()
      .then(response => {
        dispatch({type: 'FETCH_STATUS_SUCCESS', status: response.data})
      })
      .catch(error => {
        throw (error)
      })
  }
}
export {
  createMyStore,
  fetchStatusAction,
}

Then we can connect the store to the Status page and a Refresh button:

// Status.jsx
import React from 'react'
import { connect } from 'react-redux'

class Status extends React.Component {
  render () {
    // This property is automatically set by redux
    const { status } = this.props
    if (!status) {
      return <p>Loading...</p>
    }
    return (
      <p>Zuul version: {status.zuul_version}</p>
    )
  }
}

// The connect method binds the store status state to
// the component status property.
// When the status changes, the component is automatically updated.
export default connect(
  state => ({
    status: state.status
  })
)(Status)
// App.jsx
import React from 'react'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'

import Status from './Status'
import { fetchStatusAction } from './reducers'

class App extends React.Component {
  render () {
    return (
      <div>
        {/* Clicking the button dispatch the fetchStatusAction and redux
            will update the Status component. */}
        <button onClick={() => {this.props.dispatch(fetchStatusAction())}}>
          Fetch status
        </button>
        <Status />
      </div>
    )
  }
}
// Connect also adds a dispatch function property to dispatch action.
export default withRouter(connect()(App))

More information about: Redux basics and Thunk.

Tests

Python React
unittest jest
tox yarn

Jest is configured by the create-react-app command. The test script automatically load every file ending with ".test.jsx".

Tests scenario are defined using the it() function and assertion are using expect:

it('demo expect', () => {
  expect(null).toBeNull()
  expect(42).toBe(42)
  expect('test').toMatch('test')
  expect([1, 2]).toContain(2)
  expect(() => {throw Error()}).toThrow(Error)
  // Add not for negation
  expect(42).not.toBe(43)
})

Here are a couple of tests for the Status store demoed previously:

// Status.test.jsx
import React from 'react'
import ReactTestUtils from 'react-dom/test-utils'
import { Provider } from 'react-redux'

import Status from './Status'
import { createMyStore } from './reducers'

it('status render zuul version', () => {
  const store = createMyStore()
  // Dispatch a custom action to shortcut the Axios function.
  store.dispatch({type: 'FETCH_STATUS_SUCCESS', status: {zuul_version: 42}})
  const component = ReactTestUtils.renderIntoDocument(
    <Provider store={store}>
      <Status />
    </Provider>
  )
  // Check that the status is properly updated.
  const statusDom = ReactTestUtils.findRenderedDOMComponentWithTag(
    component, 'p')
  expect(statusDom.textContent).toEqual('Zuul version: 42')
})
// App.test.jsx
import React from 'react'
import ReactTestUtils from 'react-dom/test-utils'
import { Provider } from 'react-redux'
import { BrowserRouter as Router } from 'react-router-dom'

import App from './App'
import * as api from './api'

// Mock the fetchStatus Promise
api.fetchStatus = jest.fn().mockImplementation(
  () => {
    return Promise.resolve({data: {zuul_version: 43}})
  }
)
it('clicking the button fetch the status', () => {
  const store = createMyStore()
  const component = ReactTestUtils.renderIntoDocument(
    <Provider store={store}>
      <Router>
        <App />
      </Router>
    </Provider>
  )
  const buttonDom = ReactTestUtils.findRenderedDOMComponentWithTag(
    component, 'button')
  ReactTestUtils.Simulate.click(buttonDom)
  expect(api.fetchStatus.mock.calls.length).toBe(1);
})

Use "CI=true" environment to make tests exit after execution.

More information about: Jest and ReactTestUtils.

All the references

I hope you find this application stack as interesting as I do. That's it folks!