Writing modern SPA applications

part 3: REACT.JS

different approaches for

rendering ui

Render once, then bind events and modify dom

rerender entire component on state change

virtual dom

virtual dom

virtual dom

  • faster than always updating and then rerendering and repainting DOM (which is slow)
     
  • does not require rebinding of events (which is slow)
     
  • optimizied with simple heuristic to make tree comparison as fast as possible
     
  • make UI changes predictable and trackable

component based

application design

traditional mvc architecture

component-based architecture

what exactly is this

component

component structure

component lifecycle

wait a minute

how children work

JSX UNDER THE HOOD

const foo = (
  <div className="button">
    <button type="submit"/> 
  </div>
)

// this gets transpiled to

const foo = (
  React.createElement("div", {
    className: "button"
    children: [
      React.createElement("button", { type: "submit" })
    ]
  })
)

components can be

objects

react.component

import React, { Component } from 'react'
import { Link } from 'react-router-dom'

class Dropdown extends Component {
  onToggle() {
    this.setState({ visible: !this.state.visible })
  }

  render() {
    return (
      <div className="dropdown">
        <button className="dropdown__button" onClick={this.onToggle}>
          Menu
        </button>
        {this.state.visible && 
        <div className="dropdown__content">
          <Link to="/">Home</Link>
          <Link to="/about">About</Link>
          <Link to="/pricing">Pricing</Link>
        </div>}
      </div>
    )
  }
}

components can be

functions

stateless component

import React from 'react'

const Button = function Button({ type, ...props }) {
  const className = type === 'white' ? 'button__white' : 'button__black'
  return (
    <button className={className} {...props}/>
  )
}


const Input = observer(function Input({ field }) {
  return (
    <div className={classNames("form-group", {"has-error": field.error})}>
      {field.error && 
      <span className="form-group__error">{field.error}</span>}

      <input className="form-control" {...field.bind()}/>
    </div>
  )
})

components can be

composed

higher order component

const StyledInput = function StyledInput(props) {
  return (
    <Input style={{ color: 'red' }} {...props}/>
  )
}

// example use:

<StyledInput name="foo"/>
<StyledInput name="bar"/>

higher order component

class UserFetch extends React.Component {
  componentWillMount() {
    fetch('/api/users').then((users) => this.setState({ users: users }))
  }

  render() {
    const WrappedComponent = React.Children.only(this.props.children)

    if(typeof this.state.users === 'undefined') {
      return <div className="loading-spinner"/>
    } else {
      return React.cloneElement(WrappedComponent, { users: this.state.users })
    }
  }
}

// example:
const UserCount = ({ users }) => <div>User count: {users.length}</div>
const Users = () => (
  <div>
    <UserFetch><MyComponent/></UserFetch>
  </div>
)

higher order component

function withUserFetching(WrappedComponent) {
  return class extends React.Component {
    componentWillMount() {
      fetch('/api/users').then((users) => this.setState({ users: users }))
    }

    render() {
      if(typeof this.state.users === 'undefined') {
        return <div className="loading-spinner"/>
      } else {
        return <WrappedComponent users={this.state.users}/>
      }
    }
  }
}

// example:
const FetchingUserCount = withUserFetching(UserCount)
const Users = () => (
  <div>
    <FetchingUserCount/>
  </div>
)

HOC RULES:

  • Compose instead of using inheritance to get behaviours you need
     
  • Don't modify the original class, wrap it instead
     
  • Pass unrelated props to underlying component when possible ({...props})
     
  • Avoid using fat arrow syntax and use named functions instead, they show up in DevTools making them easier to spot and debug
     
  • Do not apply HOC-making functions in render() method

HOC CAVEATS

  • Since we are composing by wrapping, static methods are not passed over and refs refer to the wrapping component, not the one underneath
     
  • Statics can be copied manually or with hoist-non-react-statics
     
  • To access proper refs you can pass ref callback via custom prop
const Input = function Input({ inputRef, ...props }) {
  return <input ref={inputRef} {...props}/>
}

handling and passing

state and props

PASSING STATE DOWN WITH PROPS

pushing state up with callbacks

state management with

mobx

what is mobx

how does it work

mobx

  • Everything in MobX works around observables. They are properties of objects for which access can be tracked.
     
  • Tracking of access happens inside function passed to autorun (or reaction). The function is then re-run every time observable value changes.
     
  • Actions are functions wrapped in transaction - reactions inside them are batched together an run after the function finishes.
     
  • Computed properties are cached properties that get updated when observables used to calculate them change

mobx example

import { observable, autorun } from 'mobx'
class Store {
  @observable foo = 1
}

const store = new Store
autorun(() => console.log(store.foo))

store.foo = 2 // causes console.log to be called and 2 to be printed
store.foo = 3 // ditto, 3 is printed

mobx-react

  • MobX can be connected to React via observer() function
     
  • This function wraps our component and makes it reactive.
     
  • Reactive component is a component that rerenders automatically if any observable used to render it changes.
     
  • @observable can replace this.state and setState completely

mobx-react

import React from 'react'
import { observable } from 'mobx'
import { observer } from 'mobx-react'

class LikeButton extends React.Component {
  @observable liked = false

  render() {
    return (
      <div>
        {this.liked ? "I like this" : "I don't like this"}
        <button onClick={() => this.liked = !this.liked}/>
      </div>
    )
  }
}

export default observer(LikeButton)

data can be kept

inside stores

stores

import { observable, action, computed } from 'mobx'

class UserStore {
  @observable users = []
  @observable current = null

  fetch() {
    fetch("/api/users").then(this.setUsers)
  }

  @action setUsers(users) {
    this.users = users
  }

  @computed get currentUser() {
    if(this.current !== null) {
      return this.users.get(this.current)
    }
  }
}

stores

  • Stores are source of data inside the application
     
  • They can use Models, Repositories and other OO patterns
     
  • Stores should be created per domain (users, projects etc)
     
  • If relation between two entities is containment  (one belongs to other), it makes sense to put them in one store
     
  • There is always more than one way of doing things and there are many libraries that solve the problem of data storage

passing stores

using provider/inject

Provider

const userStore = new UserStore
const projectStore = new ProjectStore
const stores = { userStore, projectStore }

const App = function App() {
  return (
    <Provider {..stores}>
      <Header/>
      <Routing/>
      <Footer/>
    </Provider>
  )
}

inject using functions

const Header = function Header({ userStore }) {
  return (
    <header>
      {userStore.currentUser 
        ? <div>You are not logged in</div>
        : <div>You are logged in as {userStore.currentUser.name}
      }
    </header>
  )
}

export default inject('userStore')(observer(Header))

inject using decorators

@inject('userStore')
@observer
class Footer extends React.Component {
  render() {
    const { userStore } = this.props
    return (
      <div>There are {userStore.users.length} users loaded</div>
    )
  }
}

time for

summary

react

  • React is a component-based library, not "just a View in MVC"
     
  • React components are re-rendered when their props or state change
     
  • Components can be nested in each other and create a tree structure
     
  • Applications written in React are using composing (wrapping) instead of OO inheritance to share common functionality
     
  • Virtual DOM is used to make sure the amount of perf-heavy operations (event binding, DOM manipulation) is minimal

mobx

  • MobX is a library for making things reactive
     
  • Core features: observable, action, computed, autorun
     
  • MobX can be used to make React components reactive with observer(), stores can be passed with Provider/inject
     
  • Common pattern of handling data involves creating Stores and keeping data there so they can be shared between multiple unrelated components inside application

that's it

questions?

Writing modern SPA applications part 3

By Michał Matyas

Writing modern SPA applications part 3

  • 742