Maintenance Guide

The information presented in this document was mainly written by and for the core group of developers from the Société des Arts Technologiques, who are maintaining this project. As such, some sections and links may refer to private repositories exclusive to the core development team. Nevertheless, this document should still provide useful information to any developer wishing to contribute to Scenic.

This guide is aimed at developers who wish to understand everything about the inner workings of Scenic, line-by-line. It explains everything about the tools and technologies used in the project. To get up and running quickly without having to read this whole document, you should read the Development Guide instead.

Technology Overview

Scenic's UI framework is written using widely used and maintained frameworks and libraries from the JavaScript community. Scenic endorses a component-based architecture, which makes the codebase more readable, maintainable and testable in the long run. The aforementioned frameworks are well-documented and embrace modern Javascript features.

Tool / Language Layer Description
React View A UI library using the JSX syntax. Forces the development of the UI with the Component pattern.
MobX Controller The controller is split between multiple stores: they each manage the state of a specific component.
Vanilla JS (ES6) Models The models must be a clear and consistant definition of every complex data structure in the UI.
Jest Test The testing framework for both the unit tests and the integration tests
Enzyme Test A test utility for React components

The Jest testing framework is used alongside Enzyme. Jest provides an easy way to add unit tests for each new line of code, while Enzyme adds the capacity to manipulate React components using a fake DOM. Every new addition to the codebase must be tested with a minimum unit test coverage of 80%.

Architecture

The front-end architecture respects the MVC (Model/View/Controller) design:

img

When the user interacts with the UI, the flow is always the same:

  1. The user interacts with a component (ex: The user clicks on a button)
  2. The component triggers an action that will alter the model of the application. This action is asynchronous.
  3. The model is an observable object. Every time it is modified, some of its values get computed (ex: if an array is modified, its length is computed).
  4. The component observes every observable and computed values from the model, and gets rendered every time one of these values is updated.

View layer

The view layer is built with React components: we prefer using Functionnal Components to ensure that the UI is minimal and efficient. They are also easier to test and maintain, since each component is a highly specialized and minimal function dedicated to rendering a single UI element of the application.

Some Components can be an observers; an observer watches for changes to a specific Store's values (which can be observable or computed). A Store only provides values that match its purpose: a grocery store cannot sell construction tools. A Component can observe multiple Stores and a Store can depend on other Stores: we use MobX's reactions when a store needs to implement some specific logic that depends on another store's state'.

Controller layer

A Store's action methods are used to modify its own values. However, these changes sometime require back-end requests: in these cases, the action will call an `API object` in order to make an asynchronous call to the back-end. These actions contain minimal logic and should be used only when some observable data is changed. The name convention dictates the following naming patterns:

Kind Type Name
A collection of properties that defines a statement of the application A Map of observable values properties
The setter of the collection of properties A MobX action setProperty
The handler triggered when the Back-End or another store changes the properties A synchronous function handlePropertyChange
The function that requests property updates from the Back-End An asynchronous function applyPropertyChange

Observable values can either be a JavaScript primitive object or a Model object. A complex observable value needs to be defined as a JavaScript class in order to provide some minimal implicit documentation and a collection of observable values must be exposed with Javascript data structures such as Map or Set. These structures are better than objects or arrays because it garanties iterability-in-order and it has very handy methods when you want to store data.

Computed values should be used to transform observable values into another kind of data. The MobX documentation provides some precious exemples but you should use computed values only when you want to provide an observable information that derives from some observable value. You can compose abstract collection of values from multiple sources of observable values.

The interaction between a Store's actions, observables and computed values can be summarized using the following diagram:

img

API layer

Stores can request information or an action from the Back-End: APIs are used for these kind of requests. Each store performing such a request will use an asynchronous function described by an API. This allows the centralization of all API calls to the Back-End in a minimal set of files.

Model layer

The values manipulated by the stores can be complex: we need to describe these complex values as Models. These are instance of regular JavaScript classes. A model provides a clear description of the data it represents and an easy way to create and mock it in tests. We also usually add handy functions to these classes in order to manipulate them as JSON objects: toJSON and fromJSON.

Interaction between layers in Scenic

img

This UML diagram showcases every element of the front-end architecture. It also explicitly states where these elements are stored inside the src folder. This makes the UI modular and easily testable : each element has a specific concern and is clearly defined.

sequenceDiagram participant UI Component participant Store participant Model loop Each User Interaction UI Component->>Store: Trigger alt Need Back-End update Store-xAPI: Request Back-End Update API->>Model: Apply Changes else Store->>Model: Apply Changes end Model-->>UI Component: Render on Changes end

Configuration

Scenic is bundled into a single page application or webapp with the Webpack configuration bundler. All necessary configurations are located in the config folder. Make sure to specify the config path for each new tool that you use (this is often done with the --config argument). The bundler will inject every tool used in the webapp and will bundle everything into a single file : dist/main.js.

The result of the bundler's work is located in the dist folder. The whole contents of this folder will be used to serve the webapp.

img

Aliases

The alias entry is used to simplify import paths. We use it a lot in the source code :

Alias Description
~ Root of Scenic's source code
@folder Simplifies the access to some widely used folders such as components, stores, models and api. The @ is used to explicitely signal the use of an alias.
// Use
import packageJson from '~/package.json'
import List from '@assets/list.svg'

// Instead of
import packageJson from '../../package.json'
import List from '../../../../assets/list.svg'

Locales

Trranslation files are located in the assets/locales folders. They are used by the i18next library to localize Scenic's interface in French and English. I18next is initialization is handled by the i18n.js file.

Entry point

The entry key of the Webpack configuration map is very important because it specifies the entry point for the application. In JavaScript, the entry point is usually an `index.js` file, but this is more of a convention rather than a hard standard.

The entry key also allows you to add additional entry points for specific libraries before building the bundle : this is used for Babel polyfills.

Modules / Loaders

Scenic uses a lot of different file types, such as JS, SVG, PNG, MD, SCSS, etc. These file types are handled by Webpack using loaders, which are located in the module section of the Webpack configuration. Each module has a rule for some file types (which match a specific regex), which specify which loaders to use. All modules here are declared in the `config/webpack.config.js` configuration file. Read the Webpack Loaders documentation for more info.

Developping with JavaScript / Babel

Scenic's JavaScript code is transpiled using Babel as part of the build process, which makes the codebase compliant with every modern browser. Babel needs to be properly configured in order to work:

  1. Install all necessary Babel modules.

  2. Configure Babel using a config/babel.config.js configuration file.

  3. Import this configuration in Webpack and apply it to every Javascript file.

These steps will allow you to use new ECMAScript features without compatibility problems. However you must declare each feature you use inside your Babel configuration, in order to document which features you are using in your code.

Path naming

Paths are named in order to minimize writing when importing a component into another component. You need to be aware that a file named index.js will be accessible when you import its parent folder. This is a Webpack functionality and is part of JavaScript's logic, but it can be hard to understand.

For example, this seems to import a folder, but in reality it will import the index.js file present in the folder.

import ControlButton from '~/components/ControlButton'

This way of specifying imports has one drawback : since all files are named index.js, it can be difficult to find the right file unless your folder structure is crystal-clear.

Import Feature

Scenic uses the import syntax. This is an ECMAScript6 feature.

Async / Await Features

Asynchronous functions and Promises provide a good fix to the Callback Hell problem. Asynchronous functions are built on top of Promises: you should use when you need to request some data from the Back-End.

Other technologies

Sassy CSS

The SCSS langage is a compromise between standard CSS and the SASS syntax. All SCSS styles are located either in the `styles` folder, or inside a component's folder.

The styles are handled by Webpack's style-loader, which can support a lot of different syntaxes if combined with some loaders such as sass-loader (compiles SASS) and resolve-url-loader (resolves imports made in styles). All styles are loaded using a combination of style-loader and other loaders.

We embed the style sheet inside each component by importing the styles directly in the JS code.

The SVG Case

We use a specific loader for SVG files: the SVGR loader, which transforms all imported svg files into React components.

This creates a problem: you may want to import SVG assets as React components and still import other SVG assets as simple SVGs. You can achieve this by adding an issuer key in your Webpack rule. Let's take a look at this specific part of UI-Components' Webpack configuration :

[{
    test: /\.svg$/,
    use: ['babel-loader', '@svgr/webpack']
}, {
    test: /\.svg$/,
    issuer: /\s?css$/,
    loader: 'url-loader'
}]

In this case, the SVG will not be imported as a React component, but will rather use the url-loader, which lets you use the CSS url() function as an added bonus.

You can also import an SVG in your JavaScript code and modify it dynamically using different styles. Try to modify the currentColor property to see this in action:

<svg xmlns="http://www.w3.org/2000/svg" style="fill: currentColor;">
  <g id="Layer_2" data-name="Layer 2"></g>
</svg>

Markdown

When writing documentation, always use Markdown: it is easy to write and easy to understand.

Minimal Exemple

The following example will showcase a minimal app with an MVC architecture, built with React and MobX.

The sequence diagram

This sequence diagram summarizes the various interactions between the components of the app:

sequenceDiagram User->>NameForm: Input the user name NameForm->>UserStore: Store the user name UserStore->>Hello: Update and render the user name Hello->>User: See its name on the screen

As you can see, the purpose of this example app is to display the name of the user on the screen.

The main Application

We need to use ReactDOM in order to display the components as HTML elements. This main file is called by convention index.js.

import React, { createContext } from 'react'
import ReactDOM from 'react-dom'

import UserStore from './UserStore'

import NameForm from './NameForm'
import Hello from './Hello'

export const StoreContext = createContext({})

/**
 * The main App component: it instantiates all stores and manages the application lifecycle
 * @returns {external:react/Component} The App component
 */
function App () {
  const userStore = new UserStore()

  return (
    <StoreContext.Provider value={{ userStore }}>
      <NameForm />
      <Hello />
    </StoreContext.Provider>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('scenic')
)

This main file is the entry point of the application and imports all the necessary React dependencies. It also imports three files:

  1. The UserStore.js file: the Controller in the MVC architecture.
  2. The NameForm.js file: an input component to interact with the user.
  3. The Hello.js component: displays a Hello Word to our user.

After importing the required files, we create a React Context in order to easily dispatch all of the app's stores to all of the app's components. This is obviously overkill for this example, but this pattern is frequently used in Scenic. The context is used by the App component, which will eventually initialize the UserStore once mounted by React. Once the Store is created, it will also be available to all the other components of the app, thanks to the context.

Finally, we render all the components as HTML elements with the ReactDOM utility.

The NameForm input

The NameForm component is just an <input />, in charge of getting the user name. It will not react to updates of the user name; it will simply send its content to the store (which will make the update). In this exemple, the store is updated only when the user is presses the Enter key.

import React, { useContext } from 'react'
import { StoreContext } from './App'

/**
 * NameForm is an input that requires the user name. It will send this name only when the ~Enter~ key is pressed.
 * @returns {external:react/Component} The NameForm component
 */
function NameForm () {
  const { userStore } = useContext(StoreContext)

  return (
    <input
      type='text'
      placeholder='Input your name'
      onKeyPress={e => {
        e.key === 'Enter' && userStore.setUserName(e.target.value)
      }}
    />
  )
}

export default NameForm

The UserStore controller

The controller is the logical part of the application: here, every change impacts the UI rendering. This part uses the MobX library that is essential to Scenic. Each controller is called a store and is declared as a regular JavaScript class. The MobX function makeObservable is used in the constructor and will do 3 things :

  1. Declare the attribute userName as an observable attribute. When this attribute is updated, every component using this attribute will also be updated with the new value. It optimizes the UI rendering by only changing a specific part of the DOM rather than reloading the whole application.
  2. Declares the method setUserName as an action. This works like a setter and is the recommended place to modify an observable (MobX is performs optimizations when observables are changed inside actions). This centralizes the modification of each observable value.
  3. Declares the upperUserName getter as a computed getter. Computed values are work like observables, but they are always derived from another observable value. When an observable value is modified, the associated computed getters are also computed and updated. All components using this computed value will also be updated and re-rendered. Computed values are useful to complement some observables, or when you need to transform a data structure into another structure, such as extracting an array of keys from a Map.
import { observable, action, computed } from 'mobx'

/** The UserStore is storing *only* the user information */
class UserStore {
  /** @property {String} userName - The name of the user */
  userName = 'World'

  constructor () {
    makeObservable(this, {
      userName: observable,
      setUserName: action,
      upperUserName: computed
    })
  }

  /** @property {String} upperUserName - The uppercased name of the user */
  get upperUserName () {
    return this.userName.toUpperCase()
  }

  /** Sets the new name of the user */
  setUserName (name) {
    this.userName = name
  }
}

export default UserStore

The Hello component

The Hello component is the part of the UI that needs to be updated every time the user is changing his name. This is why it is declared as an observer component: MobX will link this component to the UserStore and the component will be reloaded any time upperUserName is changed. MobX also detects the explicit use of the computed value so it optimizes its rendering when the setUserName setter is used.

You will notice that every functional component is declared using the function keyword; however, this observer is declared using an arrow function wrapped by the observer method provided by mobx-react. This is more of a convention but be aware this can break some behaviours with the React.Children API.

 import { observer } from 'mobx-react'
 import { StoreContext } from './App'

/**
 * Hello renders the user name. The user can change his name any time with the NameForm, Hello will update itself.
 * @returns {external:mobx-react/ObserverComponent} The Hello component
 */
 const Hello = observer(() => {
   const { userStore } = useContext(StoreContext)

   return (
     Hello, {userStore.upperUserName}
   )
 })

References

Main references

Refer to the (excellent) documentation for each of these tools:

Useful references

A lot has been written about the React-MobX couple and many libraries have been created to support this pattern. You should consider looking at some of these tools (the Awesome Lists provide good starting points):