Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@cmdcolin
Last active July 21, 2021 22:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cmdcolin/94d1cbc285e6319cc3af4b9a8556f03f to your computer and use it in GitHub Desktop.
Save cmdcolin/94d1cbc285e6319cc3af4b9a8556f03f to your computer and use it in GitHub Desktop.
Introduction to React and mobx-state-tree

Welcome

We are happy to have you coding on our project! There is definitely a learning curve to get started coding our app, so I wanted to make this short guide to introduce some helpful concepts like the tech stack we use.

Two technologies we rely on include React and mobx-state-tree

These both have a learning curve, and it is not really essential to fully understand both, it will take time to sort of get accustomed to both of them, but it helps to get an introduction

Jump to working example at the bottom of the page

How do we use React in JBrowse?

We use functional components in JBrowse for the most part

Function components are very nice because they take some parameters and produce JSX as output. As an example

function MyComponent(props) {
    const message = 'Hello '+props.name+' welcome to React!'
    return <div>{message}</div>
}

Then in a different part of the code you could use "MyComponent" like this

function HelloWorldApp(props) {
    return (
        <div>
            <h1>Hello world, this is a basic app</h1>
            <MyComponent name="Colin" />
        </div>
    )
}

We "call" our component not by using a function call, but by just putting it in the "JSX" syntax here e.g. <MyComponent name="Colin" />

The name="Colin" is a "prop" to our function component, and so the MyComponent function gets an object called props as an argument that now has props.name set

It takes a little bit to wrap your head around this style of both functional coding and syntax of "calling" the component using JSX syntax, but this is the basis of how React works, and it is quite a nice way of coding

What is state management in React?

When you hear people talking about coding in React, they often also talk about state management.

In React, you can kind of think of the UI that you see on the screen as a "pure mathematical function" where the output on the screen is a result of the state given to it.

In the above section, you saw how we had a function

function MyComponent(props) {
    const message = 'Hello '+props.name+' welcome to React!'
    return <div>{message}</div>
}

The UI output, the HTML div, is a function of the inputted state, via the props

What if we want to change the state of our app?

Simple pure functions can be easy to think about, but how can we do things like fetch data from a REST API in our App?

When we make our state more complicated and include asynchronous API fetches and stuff, it helps to have a "state management" library

In JBrowse you will see that we use mobx-state-tree in some cases, and React hooks in others. I'll go over mobx-state-tree to start.

Intro to mobx-state-tree

Note that mobx-state-tree is kind of hard to learn, and personally I struggled a lot with learning it!

Try not to stress though, you will gain more familiarity with experience. I just wanted to write this guide as a short reference to help out, but if it doesn't immediately make sense that is fine.

Basics: initializing a "mobx-state-tree model"

To help learn mobx-state-tree, it helps to consider it first separately from react, and then see how it plays with React

The fundamental of mobx-state-tree is creating an object. This is like a simple JSON object, but it sort of a "wrapper" that adds extra utilities

import {types} from 'mobx-state-tree'
const AppState = types.model({ name: 'Colin' })

We call this AppState just to signify that it stores state, like the name, for our app

How to use our AppState model?

We can call something like this

const ret = AppState.create()
console.log(ret.name)

This will print out 'Colin' to the console

You could also imagine if we wanted to use this in our original MyComponent, we could something like

function HelloWorldApp(props) {
    const model = AppState.create()
    return (
        <div>
            <h1>Hello world, this is a basic app</h1>
            <MyComponent name={model.name} />
        </div>
    )
}

Now we just have our name from the model passed in instead of the hardcoded name "Colin"

mobx-state-tree actions

What if we don't know the name we want to say hello to at the start? We have to use "actions" to change state

const AppState = types.model({
    name: 'Colin'
}).actions(self => ({
    setName(newName) {
        self.name = newName
    }
}))

Now, in our app we can do something like this

const ret = AppState.create()
ret.setName('Harold')
console.log(ret.name)

Logs 'Harold' to the console

Note that our 'initial state' says the name is 'Colin' but we can also make it undefined so it has to be initialized by the user who calls create

const AppState = types.model({
    name: types.string
})


const ret = AppState.create({ name: 'Harold' }) // have to pass an initial name

mobx-state-tree async actions

This allows us to change the name by looking at an API

const AppState = types.model({
    name: 'Colin'
}).actions(self => ({
    setName(newName) {
        self.name = newName
    },
    async fetchName() {
        const result = await fetch('http://some_random_website/getNewBabyName')
        const data = await result.json()
        self.setName(data.name)
    }
})

Therefore, we call the action setName from another action, fetchName, which performa an asynchronous REST API request

mobx-state-tree error handling with actions

We could also add error handling too

const AppState = types.model({
    name: 'Colin'
}).volatile(self => ({
    error: undefined
}).actions(self => ({
    setName(newName) {
        self.name = newName
    },
    setError(error) {
        self.error = error
    },
    async fetchName() {
        try {
            const result = await fetch('http://some_random_website/getNewBabyName')
            const data = await result.json()
            self.setName(data.name)
        } catch(error) {
            self.setError(error)
        }
    }
})

Now if some error were to happen, we can store this in a "volatile" variable error. The volatile is used because we don't want to, for example, persist the error when we save the state of our App, but now any component that observes our state model would see that an error would be set if it referenced it

mobx-state-tree can create 'derived state' called views

Based on the currently stored name of the person in our mobx-state-tree model, we can dynamically compute a 'hello message' that gets updated whenever the name changes

This is called 'derived' or 'computed' states, or 'views'

This means, based on the state in the MST model, we "autocompute" that takes into account an error if there is one

const AppState = types.model({
    name: 'Colin'
}).volatile(self => ({
    error: undefined
})).actions(self => ({
    setName(newName) {
        self.name = newName
    },
    setError(error) {
        self.error = error
    },
    async fetchName() {
        try {
            const result = await fetch('http://some_random_website/getNewBabyName')
            const data = await result.json()
            self.setName(data.name)
        } catch(error) {
            self.setError(error)
        }
    }
})).views(self => ({
    get helloMessage() {
        if(self.error) {
            return 'Hello! We don't know who to say hello to because there was an error getting the name! Here is the error message: '+self.error.message
        }
        else {
            return 'Hello '+self.name+'!'
        }
    }
}))

Now we can say

const ret = AppState.create()
await ret.fetchName()
console.log(ret.helloMessage) // note: helloMessage is a "function", but it is a "getter" in ES6 terminology so it is called without parentheses

Now if our "fetchName" API request succeeded it would getNewBabyName from a remote API and say hello to it

If the fetchName request failed, then ret.helloMessage would tell us the error message from the API

How do we integrate mobx-state-tree and React?


import {types} from 'mobx-state-tree'
import {observer} from 'mobx-react'
import React from 'react'

const AppState = types.model({
    name: 'Colin'
}).volatile(self => ({
    error: undefined
})).actions(self => ({
    setName(newName) {
        self.name = newName
    },
    setError(error) {
        self.error = error
    },
    async fetchName() {
        try {
            const result = await fetch('http://some_random_website/getNewBabyName')
            const data = await result.json()
            self.setName(data.name)
        } catch(error) {
            self.setError(error)
        }
    }
})).views(self => ({
    get helloMessage() {
        if(self.error) {
            return 'There was an error getting the name! Here is the error message: '+self.error.message
        }
        else {
            return self.name
        }
    }
}))
function HelloWorldApp(props) {
    const model = AppState.create()
    return (
        <div>
            <h1>Hello world, this is a basic app</h1>
            <MyComponent model={model} />
            <button onClick={() => { model.fetchName() }}>Fetch new name</button>
        </div>
    )
}
const MyComponent = observer(function MyComponent(props) {
    const model = props.model
    const message = model.helloMessage
    return <div>{message}</div>
})

This is the "synthesis" of React, mobx-state-tree, and the glue between the two is called mobx-react, which gives us the "observer" function

The "observer" function allows MyComponent to rerender when it detects that it's parameter "model" has changed to have a new name or helloMessage

Codesandbox

Here is a working codesandbox with this demo in it

https://codesandbox.io/s/determined-shape-3gwho?file=/src/App.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment