Resolving the Great State Debate

I'm Jamon Holmgren

@jamonholmgren

Cofounder & CTO at Infinite Red

Twitter: @jamonholmgren

Jamon Holmgren

@jamonholmgren

Pacific Northwesterner

Live near Portland, Oregon

Live with my wife and 4 kids
Work from home

 

Jamon Holmgren

@jamonholmgren

30 people, mostly in US/Canada

Mobile / Web Design

React.js

React Native

AI/Machine Learning (hi Gant!)

Infinite Red

@jamonholmgren

Another React Native Conference!

Portland, Oregon

July, 2020

infinite.red/ChainReactConf

Chain React

@jamonholmgren

Lean Core Initiative

React Native Community Core (WebView)

Ignite

Reactotron

React Native Open Source

@jamonholmgren

Resolving the

Great State Debate

@jamonholmgren

State History

@jamonholmgren

Flux

Reflux

Redux

 

@jamonholmgren

@jamonholmgren

Redux

@jamonholmgren

We started using React Native in 2015

React Native Newbies

Redux was awesome!

Redux

@jamonholmgren

Learning Curve

  • Lots of new terms:
    reducer, dispatch, action creators, thunks, sagas

  • Lots of boilerplate (it is better now)

  • Flux pattern is hard to wrap mind around

Problems with Redux

@jamonholmgren

No clear way to handle side effects

  • Do we use Redux-Sagas? Redux-Thunk?

  • Middleware can become bloated

Problems with Redux

@jamonholmgren

Everybody does Redux differently

  • "The problem isn't with Redux, it's how teams use it."

  • "People put things in app-wide stores that should be local state."

  • "Stores are designed to support a single page and data isn't normalized."

Problems with Redux

@jamonholmgren

Hooks and Context

@jamonholmgren

Hooks replace class components

  • useState ~= this.state, this.setState

  • useEffect ~= componentDidMount, etc

Do hooks replace Redux?

@jamonholmgren

Some hooks can be used for app state

  • useReducer

  • useContext

  • Performance implications

Do hooks replace Redux?

@jamonholmgren

Do hooks replace Redux?

@jamonholmgren

Do hooks replace Redux?

@jamonholmgren

Not really.

There are missing pieces.

That's where MobX/MST comes in.

MobX

@jamonholmgren

  • Jay Meistrich:

  • "Having never used Redux, I hate it."

  • "Having never used MobX, I love it."

  • (upon seeing this slide):

  • "Having read this slide, I love it."

@jamonholmgren

MobX

MobX-React

MobX-State-Tree

 

The MobX Trifecta

@jamonholmgren

@jamonholmgren

MobX provides the underlying functions to build your application state and manage it. It's the "engine".

MobX

@jamonholmgren

"MST is like React, but for data."
- Michel Westsrate, creator of MobX/MST

MST makes MobX a bit more like Redux.

(in the good ways!)

MobX-State-Tree

@jamonholmgren

MobX-React wires up MobX to React

for maximum performance.

MobX-React

@jamonholmgren

MobX

One cohesive system

MobX, MobX-State-Tree (MST),
and MobX-React

@jamonholmgren

MobX, MST, MobX-React

Code examples

A whirlwind tour -- buckle up!

@jamonholmgren

// ...
import { MobXProviderContext } from "mobx-react"

export const PlayersScreen = observer((props) => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  
  // access properties
  rootStore.status
  
  // iterate through arrays
  const teamNames = rootStore.teams.map(team => team.name )
  
  // fire actions
  rootStore.addTeam(newTeam)
  
  // access views
  rootStore.sortedTeams
  
  // ...
})

Example: Accessing State

@jamonholmgren

import { types } from "mobx-state-tree"
import { City } from "../city"

export const Team = types.model({
  id:       types.identifier,
  city:     types.reference(City),
  region:   types.string,
  name:     types.string,
  abbrev:   types.string,
  imgURL:   types.string,
  capacity: types.number,
  strategy: types.enumeration("strategy", [
    "rebuilding", "contending"
  ]),
})

Example: MST Model

@jamonholmgren

import { types, getEnv, flow } from "mobx-state-tree"
import { Team } from "./team"

export const RootStore = types
  .model("RootStore", {
    teams: types.array(Team),
    status: types.enumeration(["pending", "loading", "done", "error"]), "pending"),
  })
  .actions(self => ({
    setTeams: teams => (self.teams = teams),
    setStatus: newStatus => (self.status = newStatus),
    getAll: flow(function* () {
      self.status = "loading"
      const { teams } = yield getEnv(self).api.get()
      self.teams = teams
      self.status = "done"
    })
  }))

Example: MST Root Store

@jamonholmgren

import * as React from "react"
import { FlatList } from "react-native"
import { observer, MobXProviderContext } from "mobx-react"
import { TeamRow } from "./team-row"

export const TeamsScreen = observer((props) => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { teams } = rootStore

  return (
    <FlatList
      data={teams}
      keyExtractor={t => t.id}
      renderItem={({ item }) => 
        <TeamRow imgURL={item.imgURL} name={item.name} />
      }
    />
  )
})

Example: MobX-React

@jamonholmgren

// Display a "warmer" to ask for a permission type
warm: flow(function* (permissionType) {
  self.permissionType = permissionType

  // check if already permitted and succeed if so
  let permission = yield check(permissionType)
  if (permission === "authorized") return true

  // otherwise show warmer
  self.navigateTo("warmer")

  // wait for the warmer to dismiss
  yield when(() => !self.isWarmerActive)

  // now check permissions again
  permission = yield check(permissionType)
  return permission === "authorized"
})

Example: Side Effects

@jamonholmgren

import { types, getEnv, flow } from "mobx-state-tree"
import { Team } from "./team"

export const RootStore = types
  .model("RootStore", {
    teams: types.array(Team),
    status: types.enumeration(["pending", "loading", "done", "error"]), "pending"),
  })
  .actions(self => ({
    // ...
  })
  .views(self => ({
    get sortedTeams() {
      return self.teams.sort((a, b) => {
        return (a.city < b.city) ? -1 : 1
      )
    }
  })

Example: MST Views

React Navigation

@jamonholmgren

@jamonholmgren

React Navigation

Because navigation is part of app state!

@jamonholmgren

React Navigation

@jamonholmgren

React Navigation

import { types, onSnapshot, getRoot } from 'mobx-state-tree'
import { Team } from '../models/team'
import { User } from '../models/user'

export const NavigationStore = types
  .model('NavigationStore', {
    profileScreenParams: types.model('profileScreenParams', {
      user: types.maybe(types.safeReference(User))
    }),
    teamScreenParams: types.model('TeamScreenParams', {
      team: types.maybe(types.safeReference(Team))
    })
  })

@jamonholmgren

React Navigation

// instead of this
navigation.navigate('ProfileScreen', { user: someUser }))

// you'd do this
navigationStore.setUser(someUser)
navigation.navigate('ProfileScreen')

@jamonholmgren

React Navigation Hooks

// ... other imports
import { useNavigation } from 'react-navigation-hooks'

const MyLinkButton = (props) => {
  const { navigate } = useNavigation()
  
  return <View><Button onPress={() => navigate("Home")} /></View>
}

@jamonholmgren

React Navigation

React Navigation 5.0 will have a new component-centric API
(see Satya and Michał's talk)

What about GraphQL?

@jamonholmgren

@jamonholmgren

MST-GQL (experimental)

@jamonholmgren

MST-GQL

GraphQL

MST Models <> GraphQL Schemas

Generates MST models from your endpoint type information

 

@jamonholmgren

MST-GQL

Jamon Holmgren & Morgan Laco

Ancient City Ruby-Rails-React

Jacksonville Beach, FL

October 3-4, 2019

Mixing it all together

@jamonholmgren

@jamonholmgren

Mixing it all together

Hooks & Context

MobX, MobX State Tree, MobX React

React Navigation

React Navigation Hooks

 

@jamonholmgren

Mixing it all together

Let's look at one component

import React, { useState } from "react"
import { View } from "react-native"
import { useNavigation } from "react-navigation-hooks"
import { observer, MobXProviderContext } from "mobx-react"
import { PlayerList } from "./player-list"

export const PlayersScreen = observer(props => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { players } = rootStore
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  const goBack = () => navigation.goBack(null)

  return (
    <View testID="PlayersScreen">
      <PlayerList currentPlayer={currentPlayer} players={players} goBack={goBack} />
    </View>
  )
})

@jamonholmgren

Mixing it all together

Hooks for local state

import React, { useState } from "react"
import { View } from "react-native"
import { useNavigation } from "react-navigation-hooks"
import { observer, MobXProviderContext } from "mobx-react"
import { PlayerList } from "./player-list"

export const PlayersScreen = observer(props => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { players } = rootStore
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  const goBack = () => navigation.goBack(null)

  return (
    <View testID="PlayersScreen">
      <PlayerList currentPlayer={currentPlayer} players={players} goBack={goBack} />
    </View>
  )
})

@jamonholmgren

Mixing it all together

Context for accessing MST stores

import React, { useState } from "react"
import { View } from "react-native"
import { useNavigation } from "react-navigation-hooks"
import { observer, MobXProviderContext } from "mobx-react"
import { PlayerList } from "./player-list"

export const PlayersScreen = observer(props => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { players } = rootStore
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  const goBack = () => navigation.goBack(null)

  return (
    <View testID="PlayersScreen">
      <PlayerList currentPlayer={currentPlayer} players={players} goBack={goBack} />
    </View>
  )
})

@jamonholmgren

Mixing it all together

MST for storing application state

import React, { useState } from "react"
import { View } from "react-native"
import { useNavigation } from "react-navigation-hooks"
import { observer, MobXProviderContext } from "mobx-react"
import { PlayerList } from "./player-list"

export const PlayersScreen = observer(props => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { players } = rootStore
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  const goBack = () => navigation.goBack(null)

  return (
    <View testID="PlayersScreen">
      <PlayerList currentPlayer={currentPlayer} players={players} goBack={goBack} />
    </View>
  )
})

@jamonholmgren

Mixing it all together

MobX-React for rendering performantly

import React, { useState } from "react"
import { View } from "react-native"
import { useNavigation } from "react-navigation-hooks"
import { observer, MobXProviderContext } from "mobx-react"
import { PlayerList } from "./player-list"

export const PlayersScreen = observer(props => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { players } = rootStore
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  const goBack = () => navigation.goBack(null)

  return (
    <View testID="PlayersScreen">
      <PlayerList currentPlayer={currentPlayer} players={players} goBack={goBack} />
    </View>
  )
})

@jamonholmgren

Mixing it all together

React Navigation & Hooks for Navigation

import React, { useState } from "react"
import { View } from "react-native"
import { useNavigation } from "react-navigation-hooks"
import { observer, MobXProviderContext } from "mobx-react"
import { PlayerList } from "./player-list"

export const PlayersScreen = observer(props => {
  const rootStore = React.useContext(MobXProviderContext).RootStore
  const { players } = rootStore
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  const goBack = () => navigation.goBack(null)

  return (
    <View testID="PlayersScreen">
      <PlayerList currentPlayer={currentPlayer} players={players} goBack={goBack} />
    </View>
  )
})

@jamonholmgren

Mixing it all together

MST for side effects, async flow control

// Display a "warmer" to ask for a permission type
warm: flow(function* (permissionType) {
  self.permissionType = permissionType

  // check if already permitted and succeed if so
  let permission = yield self.rootStore.check(permissionType)
  if (permission === "authorized") return true

  // otherwise show warmer
  self.navigateTo("warmer")

  // wait for the warmer to dismiss
  yield when(() => !self.isWarmerActive)

  // now check permissions again
  permission = yield self.rootStore.check(permissionType)
  return permission === "authorized"
})

@jamonholmgren

Premixed in Ignite Bowser

React-NavX

@jamonholmgren

@jamonholmgren

React-NavX

A pattern is emerging

Experimental runtime library

Brings everything together

@jamonholmgren

React-NavX Features*

* Subject to change!

useRootStore, useNavStore, useStore

MobX, MobX State Tree, MobX React

React Navigation & Hooks

State Persistence built-in

Reactotron support

@jamonholmgren

React-NavX - main entry file

import React, { useState } from "react"
import { AppRegistry } from "react-native"
import { MainStackNavigator } from "./navigation/main-stack-navigator"
import { AppStore } from "./models/app-store"
import { NavX } from "react-navx"

function App(props) {
  const [appStore] = useState(AppStore.create({}))

  return (
    <NavX
      stores={{ appStore }}
      screen={MainStackNavigator}
    />
  )
}
AppRegistry.registerComponent("MyApp", () => App)

@jamonholmgren

import React, { useState } from "react"
import { View } from "react-native"
import { useStore, useNavigation, observer } from "react-navx"
import { PlayerList } from "./player-list"



export const PlayersScreen = observer(props => {
  const { players } = useStore("AppStore")
  
  const [currentPlayer, setPlayer] = useState(undefined)
  const navigation = useNavigation()
  
  const goBack = () => navigation.goBack(null)

  return (...)
})

React-NavX

@jamonholmgren

React-NavX

Still very new

Expect everything to be broken

But keep an eye on the repo!

https://github.com/infinitered/react-navx

Learn More

@jamonholmgren

All the links!!

@jamonholmgren

Infinite Red Links

Website: infinite.red

Slack: community.infinite.red

Chain React: infinite.red/ChainReactConf

@jamonholmgren

Jamon Holmgren

Twitter: @jamonholmgren

jamonholmgren.com

@jamonholmgren

Thank You!

@jamonholmgren

React Native EU 2019 - Jamon - Resolving the Great State Debate with Hooks, Context, and MobX-State-Tree

By Infinite Red

React Native EU 2019 - Jamon - Resolving the Great State Debate with Hooks, Context, and MobX-State-Tree

Jamon's talk at React Native EU

  • 847
Loading comments...

More from Infinite Red