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
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
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
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.
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.
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
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"
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
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
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
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
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
Here is a working codesandbox with this demo in it
https://codesandbox.io/s/determined-shape-3gwho?file=/src/App.js