神刀安全网

Building a Performant Stopwatch with MobX and React

Building a Performant Stopwatch with MobX and React

Inrecent posts we spoke a lot about developing React applications with Redux : It enables the users to easily make testable and structured applications. However, writing a Redux application produces a lot of boilerplate code. One framework alternative is MobX , which solves this problem by doing automation using observables.

In this tutorial we are going to build a simple stop watch App step by step. The source code is available on GitHub . Our endproduct will look like this.

Why MobX?

The main author of MobX, Michel Weststrate puts its best in this tweet:

Building a Performant Stopwatch with MobX and React

So basically MobX is a library that should make your life as a developer a little bit easier. So how does MobX work? The main idea are observable : In the application one marks variables as observable and defines a function that should run when the observables change.

Counter Example

Let’s look at a simple counter example. We assume we have a simple html page like this:

 <div>     <p> Counter : <span id='counter'></span>     <br />     <button onclick='plusClick();'> + </button>     <button onClick='minusClick();'> - </button>   </div> 

The idea is to display in the span 0 at the beginning and increase/decrease the counter when the corresponding button is pressed. Here is the JavaScript code:

import {autorun, observable} from 'mobx';  let CounterStore = observable({   counter: 0, });  const render = () => {   document.getElementById('counter').innerHTML = CounterStore.counter; };  autorun(render);  window.plusClick = () => {   CounterStore.counter++; };  window.minusClick = () => {   CounterStore.counter--; }; 

The code is simple: We save our counter in a Object called CounterStore and make it observable. We implement the render function so that it updates our display and tell MobX that when the observables change, it should run this function. The result can be seen here:

The source code can be found in this GitHub repository .

Implementation of a stop watch

Now that we have learned the basics of MobX, let’s have a look how to use it with React. First of all, we will need to install the bindings for React:

npm install --save-dev mobx-react 

These bindings work with decorators , an upcoming JavaScript feature. You can read more about them here .

In the following example we are going to use the strict mode . In addition to autorun and observables, we will need to understand two more concepts:

  • computed values: These are values that can be derived from the observable state. For example, if the observable state contains the first name and the last name of a person, the computed value could be their full name. These computed values work as getters and are only updated if the dependent value changes.

  • actions: Actions will be used to group the code. In the strict mode, one can only change the values of the observables with calling an action. This makes the code more safe.

Let’s begin by creating a Timer class. The timer class will hold two values: The passed milliseconds since the timer started running and saved milliseconds for the combined time that has passed in previous runnings.

import {observable, computed, action} from 'mobx'; import {v4} from 'node-uuid'; import moment from 'moment'; import format from 'format-number-with-string';  export class Timer {   @observable milliseconds;   @observable savedMilliseconds;    constructor(initialMilliseconds = 0) {     this.milliseconds = initialMilliseconds;     this.savedMilliseconds = 0;     this.id = v4();   }    @action saveTime() {     this.savedMilliseconds += this.milliseconds;     this.milliseconds = 0;   }    @action reset() {     this.milliseconds = this.savedMilliseconds = 0;   }    @computed get totalMilliSeconds() {     return this.milliseconds + this.savedMilliseconds;   }    @computed get display() {     const tenMilliSeconds = parseInt(this.totalMilliSeconds / 10, 10);      const seconds = parseInt(tenMilliSeconds / 100, 10);     const minutes = parseInt(seconds / 60, 10);      return `${minutes} : ${format(seconds % 60, '00')} :  ${format(tenMilliSeconds % 100, '00')}`;   } } 

The code is pretty straight forward: We have the two actions saveTime and reset to manipulate the internal observable values. The computed values are totalMilliSeconds which adds the saved and the current time together and display which gives a formatted string.

It’s now time to write the Store class which manages the running state, the start time, the current timer and the saved laps:

export class TimerStore {    @observable isRunning;   @observable timer;   @observable startTime;    @observable laps;    constructor() {     this.isRunning = false;     this.timer = new Timer();     this.laps = [];   }    @computed get mainDisplay() {     return this.timer.display;   }    @computed get hasStarted() {     return this.timer.totalMilliSeconds !== 0;   }    @action measure() {     if (!this.isRunning) return;      this.timer.milliseconds = moment().diff(this.startTime);      setTimeout(() => this.measure(), 10);   }    @action startTimer() {     if (this.isRunning) return;     this.isRunning = true;     this.startTime = moment();     this.measure();   }    @computed get length() {     return this.laps.length;   }    @computed get lapTime() {     return this.laps.map((el) => el.totalMilliSeconds)       .reduce((x, y) => x + y, 0);   }    @action lapTimer() {     this.laps.push(new Timer(this.timer.totalMilliSeconds - this.lapTime));   }    @computed get lapData() {     const data = [];     for (let i = 0; i < this.laps.length; i++) {       data.push({         lap: this.laps[i],         text: `Lap ${i + 1}`,       });     }     return data.reverse();   }    @action stopTimer() {     this.timer.saveTime();     this.isRunning = false;   }    @action resetTimer() {     this.timer.reset();     this.laps = [];     this.isRunning = false;   }  } 

One of the nice things about MobX here is that we don’t have any difficulties of using asynchronous functions like setTimeout : MobX does only care that observables change and that we use action to change it. It will automatically react to those changes and rerender the necessary components. In our measure function we just check whether isRunning is set and update the milliseconds. Now it’s time to build our actual component.

import React from 'react'; import ReactDOM from 'react-dom'; import Main from './main';  import {TimerStore} from './TimerStore'; import {useStrict} from 'mobx';  useStrict(true);  const timerStore = new TimerStore();  ReactDOM.render(   <Main     timerStore={timerStore}   />, document.getElementById('app') ); 

As a side note, this will be our only component that has a state. We pass the store down to all the children. We create now a simple stateless Component that displays the laps with a left Text (Patch 1, Patch 2, …) and on the right side the display:

import React from 'react'; import {observer} from 'mobx-react'; import {timerStyle} from './styles';  export const TimerDisplay = observer(({leftText, rightText}) => {   return (     <div style={timerStyle.main} >       <div style={timerStyle.left} >         {leftText}       </div>       <div style={timerStyle.right} >         {rightText}       </div>     </div>   ); }); 

Lastly, we create our main component: It will contains simple buttons. These buttons will display depending on the store. When clicked they will call an action of the store. MobX will automatically update the component that it needs if an observable is changed.

/**  * In this file, we create a React component  * which incorporates components providedby material-ui.  */  import React from 'react'; import {observer} from 'mobx-react'; import {TimerDisplay} from './TimerDisplay'; import {buttonStyle, mainStyle} from './styles';  const Main = observer(({timerStore}) => {   let firstButton;   let secondButton;    if (!timerStore.isRunning) {     secondButton = (       <button         style={{...buttonStyle, color: '#4bd761'}}         onClick={() => timerStore.startTimer()}       >         start       </button>     );      firstButton = (       <button         style={buttonStyle}         onClick={() => timerStore.resetTimer()}       >         reset       </button>     );      if (!timerStore.hasStarted) {       firstButton = null;     }   } else {     secondButton = (       <button         style={{...buttonStyle, color: '#fd3d2a'}}         onClick={() => timerStore.stopTimer()}       >         stop       </button>     );      firstButton = (       <button         style={buttonStyle}         onClick={() => timerStore.lapTimer()}       >         lap       </button>     );   }    return (     <div style={{fontSize: 30}}>       <div         style={mainStyle.display}       >         {timerStore.mainDisplay}       </div>       <div>         <div style={mainStyle.buttons}>           {firstButton}           {secondButton}         </div>         <div>           {timerStore.lapData.map((el) =>             <TimerDisplay               key={el.lap.id}               leftText={el.text}               rightText={el.lap.display}             />             )}         </div>       </div>     </div>   ); });  export default Main; 

And now we are done with our simple application. For the curious the source code is available at this GitHub repository .

Further reading

MobX is great framework for building fast and performant apps. The choice between Redux and MobX comes down to how much control we want to have about the application and how much we let our frameworks do the job. To learn more about MobX we highly recommend this video and the GitHub repository . There is also an interesting blog post about why it might be a good idea to use the power of MobX to avoid setState in React.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Building a Performant Stopwatch with MobX and React

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址