Skip to content

New Coding Manual

alisman edited this page Apr 15, 2023 · 9 revisions

State Management

Mobx philosophy

Single Source of Truth

In our application we strive to maintain a "single source of truth", the combination of application state and data. Everything that is computed or derived from that state, should be computed, and in such a way that it will automatically recompute when its underlying inputs change. This is what Mobx allows us to do with its observables and computations (computed getters). Furthermore, Mobx's React integration triggers our view to re-render every time any referenced observable (or computation of observable) is mutated.

Store state vs component state

Keep data/state/derived computations that are shared across features of an application in a global page store. Put feature specific and ephemeral state in "local" component state. A good example of ephemeral state is tooltip state--whether or not a given tooltip is visible or not. This is clearly not a global concern and so does not need to be modeled in a global store. On the other hand, the filter state of a cBioPortal Query is something that is observed by many different components on a page and should thus be kept in a global store (or even in the url).

Mutating state

Whenever mutation Mobx state is mutated (by assigning to on observable), keep in mind that Mutations cause SYNCRONOUS reactions in observers of that state (e.g. React components). If multiple pieces of state are updated, that can cause redundant render cycles, which will impact performance. The @action decorator, or the runInAction function provided by Mobx, solves this problem by allowing multiple mutations before reactions are triggered.

@computed as a caching mechanism

You will see heavy us of computed in our application. Computed serves as a caching mechanism because Mobx will memoize the computation and only recompute when the obserables on which it depends have changed. Note that if in the course of user interaction, a particular computed comes to have NO observers (e.g. if views of that computed are hidden/dismounted) then the cache will clear and the computed will recompute when it is next needed even if its dependencies have not changed. This is a nice way to limit the footprint of the cache, though it sometimes results in recomputations that don't seem necessary.

MakeMobxView

Class components need to call makeObservable in constructor in order to make observable decorators kick in (this is due to a change in js decorator implementation). Mobx has reluctantly deprecated decorators but you can still use them if you add special configuration.

In functional components, we can achieve local observable properties and computed getters using the useLocalObservable hook.

React component style

Since we started rebuilding the portal frontend with React, functional components have come into style in the React community. Our application heavily uses class components. New development should use functional components and React hooks. This complicates our use of Mobx because Mobx doesn't integrate as elegantly into functional components as it does with decorators on class properties and getters. Nevertheless we favor Mobx state over "normal" React state.

Almost all components should be made observers.. This allows Mobx to isolate renders in leaf components without needlessly re-rendering parents.

Bootstrap and React-Bootstrap

We are stuck on Bootstrap 3.4.1 (documentation). Upgrading to 4 would be a herculean task and I don't see any reason to do it. We use React-Bootstrap which gives us some interactive component (e.g. dialogs and tabs). You can either use these components or, when you want more control and customization, just use Bootstrap markup.

We adopted CSS modules a couple years after starting project so there is still a lot of legacy CSS (SASS actually). CSS modules allow us to avoid name collisions without having to adopt painful class name conventions. You know you are using CSS modules when you are editing a file [name].module.scss. Be aware that you have to import these into your components and then reference them in className attributes. Also, be aware that your classes will be munged on output. This is how uniqueness is enforced. But it means you won't see your nice human-readable class names in the DOM.

Functional programming style

The idea of functional programming is to isolate your business logic in easily-tested, pure functions which are then invoked by control code responsible for execution flow and state mutation.

As a rule of thumb, avoid for loops (do while, etc). Instead use operations like map/reduce/filter which can be chained. Use Lodash when necessary.

No:

const output = [];
for (i=0;i>arr.length;i++) {
    if (arr[i].type === SOMETYPE) {
        output.push({ name: arr[i].first + ' ' + arr[i].last });
    }
}

Yes:

const output = arr.filter(el=>el.type === SOMETYPE)
                  .map(el=>{ return { name: el.first + ' ' + el.last } })

Data loading and loading status

Data loading should be done in remoteData (a factory for MobxPromise). MobxPromise models the state of loading process (isPending, isComplete, isError, result).

We strive to never reference result property unless we've checked that isComplete is true. It would be nice if typescript could do this for us, in the way that it does e.g. for undefined, but it cannot.

You want to check loading status of your promises (this is what actually fires the loading process) outside of your feature components so that you can show either the loading spinner OR the feature. This is analogous to React's Suspense system. This allows your feature components to avoid knowledge of loading state. They can accept the data as properties.

Use MobxView to load multiple MobxPromises (remoteData) and handle their loading state in the aggregate.