Youtube Follow Along
Setup Project
React app with Typescript and react router
1yarn create react-app my-app --template typescript
MobX
What is mobX?
https://mobx.js.org/README.html
đź’ˇ Battle tested library used for managing application state.
Their Philosophy
- Straightforward: Minimalistic boilerplate free code that captures your intent. Reactivity system detects all your changes and propagates them out to where they are being used
- Effortless optimal rendering: All changes to and uses of your data are tracked at runtime. Guarantees that computations depending on your state, like React components, run only when strictly needed.
- Architectural freedom: Unopinionated and allows you to manage your application state outside of any UI framework.
What is reactivity?
Declarative programming model that takes care of keeping the DOM (document object model) in sync with the updates to current state
- https://mobx.js.org/understanding-reactivity.html
- https://dev.to/siddharthshyniben/implementing-reactivity-from-scratch-51op
- https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
- Reactive programming is programming with asynchronous data streams**.**
- When state changes automagically updates
https://www.youtube.com/watch?v=NecmpvjOkiA
So how does it manage this? 3 Core concepts of MobX
State
Data that drives your application
- Domain specific — List of todo items
- View state — Currently selected item
State can be stored in any data structure: arrays, classes, objects, maps. MobX doesn’t care, only thing you need to do is mark state as observable which allows MobX to track it
- Collections such as arrays, Maps, and Sets are made observable by default
Actions
Piece of code that changes the state
- User events — inputing new value into spreadsheet
- Backend data pushes
Actions must be marked as action
Derivations
Anything that can be derived from the state without any further interaction is a derivation
- User interface
- Derived data — number of todos
- Backend integrations — Sending changes to the server
2 Main types of derivations
- Computed values: Can always be derived from the current observable state using a pure function
- Keyword: computed
đź’ˇ Pure functions: Given the same input will always return the same output and produces no side effects
- Reactions: Side effects that need to happen automatically when the state changes (bridge between imperative and reactive programming)
Quick Summary
3 Main annotations
- Observable: Defines trackable field that stores the state
- Action: marks a method as action that will modify state
- Computed: marks a getter that will derive new facts from the state and cache its output (kinda like a useMemo)
- All derivations are updated automatically and atomically when the state changes. As a result, it is never possible to observe intermediate values.
- All derivations are updated synchronously by default. This means that, for example, actions can safely inspect a computed value directly after altering the state.
- Computed values are updated lazily. Any computed value that is not actively in use will not be updated until it is needed for a side effect (I/O). If a view is no longer in use it will be garbage collected automatically.
- All computed values should be pure. They are not supposed to change state.
https://mobx.js.org/the-gist-of-mobx.html
Setup
Clean up, for some reason create react app installation has libraries in dependencies that should be in dev dependencies so lets move those over
1{2"name": "mobx-tutorial",3"version": "0.1.0",4"private": true,5"dependencies": {6"react": "^18.2.0",7"react-dom": "^18.2.0",8"react-scripts": "5.0.1",9"web-vitals": "^2.1.0"10},11"scripts": {12"start": "react-scripts start",13"build": "react-scripts build",14"test": "react-scripts test",15"eject": "react-scripts eject"16},17"eslintConfig": {18"extends": [19"react-app",20"react-app/jest"21]22},23"browserslist": {24"production": [25">0.2%",26"not dead",27"not op_mini all"28],29"development": [30"last 1 chrome version",31"last 1 firefox version",32"last 1 safari version"33]34},35"devDependencies": {36"@typescript-eslint/parser": "^5.32.0",37"@testing-library/jest-dom": "^5.14.1",38"@testing-library/react": "^13.0.0",39"@testing-library/user-event": "^13.2.1",40"@types/jest": "^27.0.1",41"@types/node": "^16.7.13",42"@types/react": "^18.0.0",43"@types/react-dom": "^18.0.0",44"eslint": "^8.21.0",45"eslint-plugin-mobx": "^0.0.9",46"typescript": "^4.4.2"47}48}
1. Install MobX
1yarn add mobx mobx-react
Things to note from their documentation
“If you have used MobX before, or if you followed online tutorials, you probably saw MobX with decorators like @observable. In MobX 6, we have chosen to move away from decorators by default, for maximum compatibility with standard JavaScript. They can still be used if you enable them though.
2. Install eslint and plugins for MobX
1yarn add -D eslint @typescript-eslint/parser eslint-plugin-mobx
3. Create .eslintrc.js
1touch .eslintrc.js
1// .eslintrc.js2module.exports = {3parser: "@typescript-eslint/parser",4plugins: ["mobx"],5extends: ["plugin:mobx/recommended", "react-app", "react-app/jest"],6rules: {7"mobx/missing-observer": "off",8},910/*11rules: {12// these values are the same as recommended13"mobx/exhaustive-make-observable": "warn",14"mobx/unconditional-make-observable": "error",15"mobx/missing-make-observable": "error",16"mobx/missing-observer": "warn",17"mobx/no-anonymous-observer": "warn"18}19*/20};
4. Remove “eslintConfig” from package.json
1"eslintConfig": {2"extends": [3"react-app",4"react-app/jest"5]6},
5. Test that everything is working
- Go To App.tsx
Should know see eslint warning
Component App is missing observer.eslint(mobx/missing-observer)
- Save the file and see it magic!
State
Lets learn about how MobX manages state by first creating a class and looking how we can use observable annotation
makeObservable
- Define annotation (observable, action, computed) per property
1makeObservable(target, annotations?, options?)
To-dos are boring lets do something different
1. Create Athlete class
1// Athelete.ts2import { makeObservable, observable } from "mobx";34class Athlete {5name: string;6age: number;7teamHistory: string[];8constructor(name: string, age: number) {9this.name = name;10this.age = age;11this.teamHistory = [];1213makeObservable(this, { teamHistory: true, // true will infer the best annotation like makeAutoObservable, false explictly does not annotate14name: observable, age: observable });15}16}1718export default Athlete;
- We are defining 2 state values, name and age as well as setting them to observable annotation. Setting these fields to observable allows MobX to now start tracking them
2. Quickly update index.css for some simple table styling
1// index.css2body {3margin: 0;4font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",5"Droid Sans", "Helvetica Neue", sans-serif;6-webkit-font-smoothing: antialiased;7-moz-osx-font-smoothing: grayscale;8}910code {11font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;12}1314table {15font-family: arial, sans-serif;16border-collapse: collapse;17width: 50%;18}1920td,21th {22border: 1px solid #dddddd;23text-align: left;24padding: 8px;25}
3. Create a Roster.tsx component
1import React from "react";2import { observer } from "mobx-react";3import Athlete from "./Athlete";45const lebronJames = new Athlete("Lebron James", 37);6const stephCurry = new Athlete("Steph Curry", 34);78const Roster = observer(function Roster() {9return (10<table>11<tr>12<th>Name</th>13<th>Age</th>14</tr>15{[lebronJames, stephCurry].map((athlete) => {16return (17<tr key={athlete.name}>18<td>{athlete.name}</td>19<td>{athlete.age}</td>20</tr>21);22})}23</table>24);25});2627export default Roster;
4. Add WishBirthday action to Athlete class
1import { action, makeObservable, observable } from "mobx";23class Athlete {4name: string;5age: number;6teamHistory: string[];78constructor(name: string, age: number) {9this.name = name;10this.age = age;11this.teamHistory = [];1213makeObservable(this, {14teamHistory: true, // true will infer the best annotation like makeAutoObservable, false explictly does not annotate15name: observable,16age: observable,17wishHappyBirthday: action,18});19}2021wishHappyBirthday() {22this.age++;23}24}2526export default Athlete;
5. Add birthday button to table
1import React from "react";2import { observer } from "mobx-react";3import Athlete from "./Athlete";45const lebronJames = new Athlete("Lebron James", 37);6const stephCurry = new Athlete("Steph Curry", 34);78function Roster() {9return (10<table>11<tr>12<th>Name</th>13<th>Age</th>14<th>Is it their birthday??</th>15</tr>16{[lebronJames, stephCurry].map((athlete) => {17return (18<tr key={athlete.name}>19<td>{athlete.name}</td>20<td>{athlete.age}</td>21<td>22<button type="button" style={{ width: "100%" }} onClick={() => athlete.wishHappyBirthday()}>23Wish happy birthday 🎊24</button>25</td>26</tr>27);28})}29</table>30);31};3233export default observer(Roster);
6. Test
Clicking happy birthday should now modify the players age and automatically update the Roster component!
7. Trade players
1// Athete.ts2import { action, makeObservable, observable } from "mobx";34class Athlete {5name: string;6age: number;7teamHistory: string[]; // this should be automatically made out to be observable as Collections (arrays, maps, sets) are by default89constructor(name: string, age: number) {10this.name = name;11this.age = age;12this.teamHistory = [];1314makeObservable(this, {15teamHistory: true, // true will infer the best annotation like makeAutoObservable, false explictly does not annotate16name: observable,17age: observable,18wishHappyBirthday: action,19tradePlayer: action,20});21}2223wishHappyBirthday() {24this.age++;25}2627tradePlayer(team: string) {28this.teamHistory.push(team);29}30}3132export default Athlete;
Update Roster.tsx
1// Roster.tsx2<td>{athlete.teamHistory}</td>3<button type="button" onClick={() => athlete.tradePlayer("Lakers")}>4Trade5</button>6</tr>
8. Finish up the trade form component
1// TradeForm.tsx2import React, { useState } from "react";3import Athlete from "./Athlete";45type Props = {6athlete: Athlete;7};89// eslint-disable-next-line mobx/missing-observer10function TradeForm({ athlete }: Props) {11const [teamName, setTeamName] = useState<string>("");12return (13<>14<input type="text" placeholder="Team name..." onChange={(e) => setTeamName(e.target.value)} />15<span>16<button type="button" onClick={() => athlete.tradePlayer(teamName)}>17Trade18</button>19</span>20</>21);22}2324export default TradeForm;
1// Roster.tsx2<th>Trade player form</th>3...4<td>5<TradeForm athlete={athlete} />6</td>
If we go ahead and type and trade you can see it will show
Actions
Lets create a money form and check out how we can use actions to manage some local state
Lets Create a money form here and see how we can improve it with mobx
1. Create MoneyForm.tsx
1// MoneyForm.tsx2import { observer } from "mobx-react-lite";3import React, { useState } from "react";45// eslint-disable-next-line mobx/missing-observer6function MoneyForm() {7const [total, setTotal] = useState<number>(0);8const [years, setYears] = useState(0);9const [salary, setSalary] = useState(0);1011return (12<div style={{ display: "flex", flexDirection: "column" }}>13<h1 style={{ marginBottom: 0 }}>Money Talks</h1>14<p>Total: {total}</p>15<input16type="number"17placeholder="Years..."18style={{ height: "40px" }}19onChange={(e) => setYears(Number(e.target.value))}20/>21<input22type="number"23placeholder="Yearly salary..."24style={{ height: "40px" }}25onChange={(e) => setSalary(Number(e.target.value))}26/>27<button type="button" onClick={() => setTotal(years * salary)}>28Calculate total29</button>30</div>31);32}3334export default MoneyForm;
2. Now lets modify it to use mobx
1import { action, observable } from "mobx";2import { observer } from "mobx-react-lite";3import React from "react";45type FormState = {6total: number;7years: number;8salary: number;9};1011const formState: FormState = observable({12total: 0,13years: 0,14salary: 0,15});1617// eslint-disable-next-line mobx/missing-observer18function MoneyForm() {19const calculateTotal = action((formState: FormState) => {20formState.total = formState.years * formState.salary;21});2223return (24<div style={{ display: "flex", flexDirection: "column" }}>25<h1 style={{ marginBottom: 0 }}>Money Talks</h1>26<p>Total: {formState.total}</p>27<input28type="number"29placeholder="Years..."30style={{ height: "40px" }}31onChange={action((e) => {32formState.years = Number(e.target.value);33})}34/>35<input36type="number"37placeholder="Yearly salary..."38style={{ height: "40px" }}39onChange={action((e) => {40formState.salary = Number(e.target.value);41})}42/>43<button type="button" onClick={() => calculateTotal(formState)}>44Calculate total45</button>46</div>47);48}4950export default observer(MoneyForm);
- We can consolidate all the useState calls to mobX observables essentially and wrap the event handlers in actions
- Action acts as both an annotion and a higher order component
“To leverage the transaction nature of MobX as much as possible, actions should be passed as far outward as possible”
3. Use computed for total
Computed values can be used to derive information from other observables
- Also important to note computed values are cached, similar to useMemo
1import { action, computed, observable, toJS } from "mobx";2import { observer } from "mobx-react-lite";3import React from "react";45type FormState = {6total: number;7years: number;8salary: number;9};1011const formState: FormState = observable({12total: 0,13years: 0,14salary: 0,15});1617// eslint-disable-next-line mobx/missing-observer18function MoneyForm() {19const calculateTotal = action((formState: FormState) => {20formState.total = formState.years * formState.salary;21});2223const totalValue = computed(() => formState.salary * formState.years);2425return (26<div style={{ display: "flex", flexDirection: "column" }}>27<h1 style={{ marginBottom: 0 }}>Money Talks</h1>28<>Total: {toJS(totalValue)}</>29<input30type="number"31placeholder="Years..."32style={{ height: "40px" }}33onChange={action((e) => {34formState.years = Number(e.target.value);35})}36/>37<input38type="number"39placeholder="Yearly salary..."40style={{ height: "40px" }}41onChange={action((e) => {42formState.salary = Number(e.target.value);43})}44/>45<button type="button" onClick={() => calculateTotal(formState)}>46Calculate total47</button>48</div>49);50}5152export default observer(MoneyForm);
So now we pretty much understand the gist of mobX lets enhance our Athlete Class to include them all and also make use of the makeAutoObservable
makeAutoObservable(target, overrides, options)
makeObservable on steroids, infers all properties by default
Inference Rules
- All own properties become observable.
- All getters become computed.
- All setters become action.
- All functions on prototype become autoAction.
- All generator functions on prototype become flow. (Note that generator functions are not detectable in some transpiler configurations, if flow doesn't work as expected, make sure to specify flow explicitly.)
- Members marked with false in the overrides argument will not be annotated. For example, using it for read only fields such as identifiers.
1. Replace makeObservable with makeAutoObservable
1// Athlete.ts2constructor(name: string, age: number) {3this.name = name;4this.age = age;5this.teamHistory = [];67makeAutoObservable(this);8}
- Boom works exactly the same!
Using data stores
https://mobx.js.org/defining-data-stores.html
“Using domain specific stores throughout your application can help manage large applications. We follow a similar architecture at our startup and it has been a pleasure to work with.”
These are the responsibilities of a store:
- Instantiate domain objects. Make sure domain objects know the store they belong to.
- Make sure there is only one instance of each of your domain objects. The same user, order or todo should not be stored twice in memory. This way you can safely use references and also be sure you are looking at the latest instance, without ever having to resolve a reference. This is fast, straightforward and convenient when debugging.
- Provide backend integration. Store data when needed.
- Update existing instances if updates are received from the backend.
- Provide a standalone, universal, testable component of your application.
- To make sure your store is testable and can be run server-side, you will probably move doing actual websocket / http requests to a separate object so that you can abstract over your communication layer.
- There should be only one instance of a store.
Lets consolidate all of our app into a single store!
Personally I like to integrate stores as a replacement to React Context and follow a similar style
Some best practices for using MobX with React
https://mobx.js.org/react-optimizations.html
1. Create TeamStore.tsx
1// TeamStore.tsx2import { makeAutoObservable } from "mobx";3import React, { useContext, useEffect, useRef } from "react";4import Athlete from "./Athlete";56export default class TeamStore {7constructor(players: Athlete[]) {8this.players = players;9makeAutoObservable(this);10}1112state: string = "";13setState = (state: string) => {14this.state = state;15};1617mascot: string = "";18setMascot = (mascot: string) => {19this.mascot = mascot;20};2122players: Athlete[] = [];2324get teamName(): string {25return this.state + this.mascot;26}2728get totalYearlyCost(): number {29return this.players.reduce((totalSalary, currentAthlete) => totalSalary + currentAthlete.salary, 0);30}3132addPlayer = (player: Athlete) => {33this.players.push(player);34}35}3637const TeamStoreContext = React.createContext<TeamStore>(null as unknown as TeamStore);3839export const useTeamStore = () => useContext(TeamStoreContext);4041type Props = {42children: React.ReactNode;43players: Athlete[];44};4546export function TeamStoreProvider({ children, players }: Props) {47const store = useRef(new TeamStore(players));4849return <TeamStoreContext.Provider value={store.current}>{children}</TeamStoreContext.Provider>;50}
2. Update App.tsx
1import React from "react";2import "./App.css";3import Athlete from "./Athlete";4import MoneyForm from "./MoneyForm";5import Roster from "./Roster";6import { TeamStoreProvider } from "./TeamStore";78const lebronJames = new Athlete("Lebron James", 37, 5);9const stephCurry = new Athlete("Steph Curry", 34, 4);1011function getPlayersFromBackend(): Athlete[] {12return [lebronJames, stephCurry];13}1415function App() {16// fetch team17const players = getPlayersFromBackend();1819return (20<div className="App">21<header className="App-header">22<TeamStoreProvider players={players}>23<Roster />24<div style={{ marginTop: "32px" }}>25<MoneyForm />26</div>27</TeamStoreProvider>28</header>29</div>30);31}3233export default App;
3. Update Roster.tsx
1import React from "react";2import { observer } from "mobx-react";3import TradeForm from "./TradeForm";4import { useTeamStore } from "./TeamStore";56function Roster() {7const { players } = useTeamStore();8return (9<table>10<tr>11<th>Name</th>12<th>Age</th>13<th>Is it their birthday??</th>14<th>Teams played on</th>15<th>Trade player form</th>16</tr>17{players.map((athlete) => {18return (19<tr key={athlete.name}>20<td>{athlete.name}</td>21<td>{athlete.age}</td>22<td>23<button type="button" style={{ width: "100%" }} onClick={() => athlete.wishHappyBirthday()}>24Wish happy birthday 🎊25</button>26</td>27<td>{athlete.teamHistory}</td>28<td>29<TradeForm athlete={athlete} />30</td>31</tr>32);33})}34</table>35);36}3738export default observer(Roster);
4. Repurpose money form to add players to roster
1import React from "react";2import { action, observable } from "mobx";3import { observer } from "mobx-react-lite";4import Athlete from "./Athlete";5import { useTeamStore } from "./TeamStore";67type FormState = {8name: string;9age: number;10salary: number;11};1213const initialState: FormState = {14name: "",15age: 0,16salary: 0,17};1819let formState: FormState = observable({20name: "",21age: 0,22salary: 0,23});2425// eslint-disable-next-line mobx/missing-observer26function MoneyForm() {27const { totalYearlyCost, addPlayer } = useTeamStore();2829return (30<div style={{ display: "flex", flexDirection: "column" }}>31<h1 style={{ marginBottom: 0 }}>Money Talks</h1>32<>Total: {totalYearlyCost} Million</>33<input34type="text"35placeholder="Player name..."36style={{ height: "40px" }}37value={formState.name}38onChange={action((e) => {39formState.name = e.target.value;40})}41/>42<input43type="number"44placeholder="Player age..."45style={{ height: "40px" }}46value={formState.age}47onChange={action((e) => {48formState.age = Number(e.target.value);49})}50/>51<input52type="number"53placeholder="Yearly salary..."54style={{ height: "40px" }}55value={formState.salary}56onChange={action((e) => {57formState.salary = Number(e.target.value);58})}59/>60<button61type="button"62onClick={action((e) => {63addPlayer(new Athlete(formState.name, formState.age, formState.salary));64formState = initialState;65})}66>67Add Player68</button>69</div>70);71}7273export default observer(MoneyForm);
5. Add TeamInfo Component
1import { observer } from "mobx-react";2import React from "react";3import { useTeamStore } from "./TeamStore";45// eslint-disable-next-line mobx/missing-observer6function TeamNameInfo() {7const { teamName, setMascot } = useTeamStore();8return (9<>10<h1 style={{ marginBottom: 1 }}>Team: {teamName}</h1>11<input12type="text"13placeholder="Change mascot"14onChange={(e) => setMascot(e.target.value)}15style={{ marginBottom: 8 }}16/>{" "}17</>18);19}2021export default observer(TeamNameInfo);
5. Update TeamStore
1import { makeAutoObservable } from "mobx";2import React, { useContext, useEffect, useRef } from "react";3import Athlete from "./Athlete";45export default class TeamStore {6constructor(players: Athlete[]) {7this.players = players8makeAutoObservable(this);9}1011state: string = "Maine";1213mascot: string = "";14setMascot = (mascot: string) => {15this.mascot = mascot;16};1718players: Athlete[] = [];19setPlayers = (players: Athlete[]) => {20this.players = players;21};2223get teamName(): string {24return `${this.state} ${this.mascot}`;25}2627get totalYearlyCost(): number {28return this.players.reduce((totalSalary, currentAthlete) => totalSalary + currentAthlete.salary, 0);29}3031addPlayer = (player: Athlete) => {32this.setPlayers([...this.players, player]);33};34}3536const TeamStoreContext = React.createContext<TeamStore>(null as unknown as TeamStore);3738export const useTeamStore = () => useContext(TeamStoreContext);3940type Props = {41children: React.ReactNode;42players: Athlete[];43};4445export function TeamStoreProvider({ children, players }: Props) {46const store = useRef(new TeamStore(players));4748return <TeamStoreContext.Provider value={store.current}>{children}</TeamStoreContext.Provider>;49}
6. Update Roster
1import React from "react";2import { observer } from "mobx-react";3import TradeForm from "./TradeForm";4import { useTeamStore } from "./TeamStore";56function Roster() {7const { players } = useTeamStore();8return (9<table>10<tr>11<th>Name</th>12<th>Age</th>13<th>Is it their birthday??</th>14<th>Teams played on</th>15<th>Trade player form</th>16</tr>17{players.map((athlete) => {18return (19<tr key={athlete.name}>20<td>{athlete.name}</td>21<td>{athlete.age}</td>22<td>23<button type="button" style={{ width: "100%" }} onClick={() => athlete.wishHappyBirthday()}>24Wish happy birthday 🎊25</button>26</td>27<td>{athlete.teamHistory}</td>28<td>29<TradeForm athlete={athlete} />30</td>31</tr>32);33})}34</table>35);36}3738export default observer(Roster);
7. Finilize App.tsx
1import React from "react";2import "./App.css";3import Athlete from "./Athlete";4import MoneyForm from "./MoneyForm";5import Roster from "./Roster";6import TeamNameInfo from "./TeamNameInfo";7import { TeamStoreProvider } from "./TeamStore";89const lebronJames = new Athlete("Lebron James", 37, 5);10const stephCurry = new Athlete("Steph Curry", 34, 9);1112function getPlayersFromBackend(): Athlete[] {13return [lebronJames, stephCurry];14}1516function App() {17// fetch players18const players = getPlayersFromBackend();1920return (21<div className="App">22<header className="App-header">23<TeamStoreProvider players={players}>24<TeamNameInfo />25<Roster />26<div style={{ marginTop: "16px" }}>27<MoneyForm />28</div>29</TeamStoreProvider>30</header>31</div>32);33}3435export default App;