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
- 2,819