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