MobX Tutorial for React with Typescript

A MobX Tutorial for React applications using Typescript

Typescript

29 Views, 3257 Words - 17 min read

Youtube Follow Along

https://youtu.be/ElgfQdq-Htk

Setup Project

React app with Typescript and react router

BASH
1
yarn 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://www.youtube.com/watch?v=NecmpvjOkiA

MobX

So how does it manage this? 3 Core concepts of MobX

State

Data that drives your application

  1. Domain specific ā€” List of todo items

  2. 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)

  1. AllĀ derivationsĀ are updatedĀ automaticallyĀ andĀ atomicallyĀ when theĀ stateĀ changes. As a result, it is never possible to observe intermediate values.

  2. AllĀ derivationsĀ are updatedĀ synchronouslyĀ by default. This means that, for example,Ā actionsĀ can safely inspect a computed value directly after altering theĀ state.

  3. 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.

  4. 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

JSON
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

BASH
1
yarn 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

BASH
1
yarn add -D eslint @typescript-eslint/parser eslint-plugin-mobx

3. Create .eslintrc.js

BASH
1
touch .eslintrc.js
JAVASCRIPT
1
// .eslintrc.js
2
module.exports = {
3
parser: "@typescript-eslint/parser",
4
plugins: ["mobx"],
5
extends: ["plugin:mobx/recommended", "react-app", "react-app/jest"],
6
rules: {
7
"mobx/missing-observer": "off",
8
},
9
10
/*
11
rules: {
12
// these values are the same as recommended
13
"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

JSON
1
"eslintConfig": {
2
"extends": [
3
"react-app",
4
"react-app/jest"
5
]
6
},

5. Test that everything is working

  1. Go To App.tsx

Should know see eslint warning

Component App is missing observer.eslint(mobx/missing-observer)

  1. 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
JAVASCRIPT
1
makeObservable(target, annotations?, options?)

To-dos are boring lets do something different

1. Create Athlete class

TYPESCRIPT
1
// Athelete.ts
2
import { makeObservable, observable } from "mobx";
3
4
class Athlete {
5
name: string;
6
age: number;
7
teamHistory: string[];
8
constructor(name: string, age: number) {
9
this.name = name;
10
this.age = age;
11
this.teamHistory = [];
12
13
makeObservable(this, { teamHistory: true, // true will infer the best annotation like makeAutoObservable, false explictly does not annotate
14
name: observable, age: observable });
15
}
16
}
17
18
export 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

CSS
1
// index.css
2
body {
3
margin: 0;
4
font-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
}
9
10
code {
11
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
12
}
13
14
table {
15
font-family: arial, sans-serif;
16
border-collapse: collapse;
17
width: 50%;
18
}
19
20
td,
21
th {
22
border: 1px solid #dddddd;
23
text-align: left;
24
padding: 8px;
25
}

3. Create a Roster.tsx component

TYPESCRIPT
1
import React from "react";
2
import { observer } from "mobx-react";
3
import Athlete from "./Athlete";
4
5
const lebronJames = new Athlete("Lebron James", 37);
6
const stephCurry = new Athlete("Steph Curry", 34);
7
8
const Roster = observer(function Roster() {
9
return (
10
<table>
11
<tr>
12
<th>Name</th>
13
<th>Age</th>
14
</tr>
15
{[lebronJames, stephCurry].map((athlete) => {
16
return (
17
<tr key={athlete.name}>
18
<td>{athlete.name}</td>
19
<td>{athlete.age}</td>
20
</tr>
21
);
22
})}
23
</table>
24
);
25
});
26
27
export default Roster;

4. Add WishBirthday action to Athlete class

TYPESCRIPT
1
import { action, makeObservable, observable } from "mobx";
2
3
class Athlete {
4
name: string;
5
age: number;
6
teamHistory: string[];
7
8
constructor(name: string, age: number) {
9
this.name = name;
10
this.age = age;
11
this.teamHistory = [];
12
13
makeObservable(this, {
14
teamHistory: true, // true will infer the best annotation like makeAutoObservable, false explictly does not annotate
15
name: observable,
16
age: observable,
17
wishHappyBirthday: action,
18
});
19
}
20
21
wishHappyBirthday() {
22
this.age++;
23
}
24
}
25
26
export default Athlete;

5. Add birthday button to table

TYPESCRIPT
1
import React from "react";
2
import { observer } from "mobx-react";
3
import Athlete from "./Athlete";
4
5
const lebronJames = new Athlete("Lebron James", 37);
6
const stephCurry = new Athlete("Steph Curry", 34);
7
8
function Roster() {
9
return (
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) => {
17
return (
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()}>
23
Wish happy birthday šŸŽŠ
24
</button>
25
</td>
26
</tr>
27
);
28
})}
29
</table>
30
);
31
};
32
33
export default observer(Roster);

6. Test

Clicking happy birthday should now modify the players age and automatically update the Roster component!

7. Trade players

TYPESCRIPT
1
// Athete.ts
2
import { action, makeObservable, observable } from "mobx";
3
4
class Athlete {
5
name: string;
6
age: number;
7
teamHistory: string[]; // this should be automatically made out to be observable as Collections (arrays, maps, sets) are by default
8
9
constructor(name: string, age: number) {
10
this.name = name;
11
this.age = age;
12
this.teamHistory = [];
13
14
makeObservable(this, {
15
teamHistory: true, // true will infer the best annotation like makeAutoObservable, false explictly does not annotate
16
name: observable,
17
age: observable,
18
wishHappyBirthday: action,
19
tradePlayer: action,
20
});
21
}
22
23
wishHappyBirthday() {
24
this.age++;
25
}
26
27
tradePlayer(team: string) {
28
this.teamHistory.push(team);
29
}
30
}
31
32
export default Athlete;

Update Roster.tsx

TYPESCRIPT
1
// Roster.tsx
2
<td>{athlete.teamHistory}</td>
3
<button type="button" onClick={() => athlete.tradePlayer("Lakers")}>
4
Trade
5
</button>
6
</tr>

8. Finish up the trade form component

TYPESCRIPT
1
// TradeForm.tsx
2
import React, { useState } from "react";
3
import Athlete from "./Athlete";
4
5
type Props = {
6
athlete: Athlete;
7
};
8
9
// eslint-disable-next-line mobx/missing-observer
10
function TradeForm({ athlete }: Props) {
11
const [teamName, setTeamName] = useState<string>("");
12
return (
13
<>
14
<input type="text" placeholder="Team name..." onChange={(e) => setTeamName(e.target.value)} />
15
<span>
16
<button type="button" onClick={() => athlete.tradePlayer(teamName)}>
17
Trade
18
</button>
19
</span>
20
</>
21
);
22
}
23
24
export default TradeForm;
TYPESCRIPT
1
// Roster.tsx
2
<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

TYPESCRIPT
1
// MoneyForm.tsx
2
import { observer } from "mobx-react-lite";
3
import React, { useState } from "react";
4
5
// eslint-disable-next-line mobx/missing-observer
6
function MoneyForm() {
7
const [total, setTotal] = useState<number>(0);
8
const [years, setYears] = useState(0);
9
const [salary, setSalary] = useState(0);
10
11
return (
12
<div style={{ display: "flex", flexDirection: "column" }}>
13
<h1 style={{ marginBottom: 0 }}>Money Talks</h1>
14
<p>Total: {total}</p>
15
<input
16
type="number"
17
placeholder="Years..."
18
style={{ height: "40px" }}
19
onChange={(e) => setYears(Number(e.target.value))}
20
/>
21
<input
22
type="number"
23
placeholder="Yearly salary..."
24
style={{ height: "40px" }}
25
onChange={(e) => setSalary(Number(e.target.value))}
26
/>
27
<button type="button" onClick={() => setTotal(years * salary)}>
28
Calculate total
29
</button>
30
</div>
31
);
32
}
33
34
export default MoneyForm;

2. Now lets modify it to use mobx

TYPESCRIPT
1
import { action, observable } from "mobx";
2
import { observer } from "mobx-react-lite";
3
import React from "react";
4
5
type FormState = {
6
total: number;
7
years: number;
8
salary: number;
9
};
10
11
const formState: FormState = observable({
12
total: 0,
13
years: 0,
14
salary: 0,
15
});
16
17
// eslint-disable-next-line mobx/missing-observer
18
function MoneyForm() {
19
const calculateTotal = action((formState: FormState) => {
20
formState.total = formState.years * formState.salary;
21
});
22
23
return (
24
<div style={{ display: "flex", flexDirection: "column" }}>
25
<h1 style={{ marginBottom: 0 }}>Money Talks</h1>
26
<p>Total: {formState.total}</p>
27
<input
28
type="number"
29
placeholder="Years..."
30
style={{ height: "40px" }}
31
onChange={action((e) => {
32
formState.years = Number(e.target.value);
33
})}
34
/>
35
<input
36
type="number"
37
placeholder="Yearly salary..."
38
style={{ height: "40px" }}
39
onChange={action((e) => {
40
formState.salary = Number(e.target.value);
41
})}
42
/>
43
<button type="button" onClick={() => calculateTotal(formState)}>
44
Calculate total
45
</button>
46
</div>
47
);
48
}
49
50
export 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
TYPESCRIPT
1
import { action, computed, observable, toJS } from "mobx";
2
import { observer } from "mobx-react-lite";
3
import React from "react";
4
5
type FormState = {
6
total: number;
7
years: number;
8
salary: number;
9
};
10
11
const formState: FormState = observable({
12
total: 0,
13
years: 0,
14
salary: 0,
15
});
16
17
// eslint-disable-next-line mobx/missing-observer
18
function MoneyForm() {
19
const calculateTotal = action((formState: FormState) => {
20
formState.total = formState.years * formState.salary;
21
});
22
23
const totalValue = computed(() => formState.salary * formState.years);
24
25
return (
26
<div style={{ display: "flex", flexDirection: "column" }}>
27
<h1 style={{ marginBottom: 0 }}>Money Talks</h1>
28
<>Total: {toJS(totalValue)}</>
29
<input
30
type="number"
31
placeholder="Years..."
32
style={{ height: "40px" }}
33
onChange={action((e) => {
34
formState.years = Number(e.target.value);
35
})}
36
/>
37
<input
38
type="number"
39
placeholder="Yearly salary..."
40
style={{ height: "40px" }}
41
onChange={action((e) => {
42
formState.salary = Number(e.target.value);
43
})}
44
/>
45
<button type="button" onClick={() => calculateTotal(formState)}>
46
Calculate total
47
</button>
48
</div>
49
);
50
}
51
52
export 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

TYPESCRIPT
1
// Athlete.ts
2
constructor(name: string, age: number) {
3
this.name = name;
4
this.age = age;
5
this.teamHistory = [];
6
7
makeAutoObservable(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

TYPESCRIPT
1
// TeamStore.tsx
2
import { makeAutoObservable } from "mobx";
3
import React, { useContext, useEffect, useRef } from "react";
4
import Athlete from "./Athlete";
5
6
export default class TeamStore {
7
constructor(players: Athlete[]) {
8
this.players = players;
9
makeAutoObservable(this);
10
}
11
12
state: string = "";
13
setState = (state: string) => {
14
this.state = state;
15
};
16
17
mascot: string = "";
18
setMascot = (mascot: string) => {
19
this.mascot = mascot;
20
};
21
22
players: Athlete[] = [];
23
24
get teamName(): string {
25
return this.state + this.mascot;
26
}
27
28
get totalYearlyCost(): number {
29
return this.players.reduce((totalSalary, currentAthlete) => totalSalary + currentAthlete.salary, 0);
30
}
31
32
addPlayer = (player: Athlete) => {
33
this.players.push(player);
34
}
35
}
36
37
const TeamStoreContext = React.createContext<TeamStore>(null as unknown as TeamStore);
38
39
export const useTeamStore = () => useContext(TeamStoreContext);
40
41
type Props = {
42
children: React.ReactNode;
43
players: Athlete[];
44
};
45
46
export function TeamStoreProvider({ children, players }: Props) {
47
const store = useRef(new TeamStore(players));
48
49
return <TeamStoreContext.Provider value={store.current}>{children}</TeamStoreContext.Provider>;
50
}

2. Update App.tsx

TYPESCRIPT
1
import React from "react";
2
import "./App.css";
3
import Athlete from "./Athlete";
4
import MoneyForm from "./MoneyForm";
5
import Roster from "./Roster";
6
import { TeamStoreProvider } from "./TeamStore";
7
8
const lebronJames = new Athlete("Lebron James", 37, 5);
9
const stephCurry = new Athlete("Steph Curry", 34, 4);
10
11
function getPlayersFromBackend(): Athlete[] {
12
return [lebronJames, stephCurry];
13
}
14
15
function App() {
16
// fetch team
17
const players = getPlayersFromBackend();
18
19
return (
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
}
32
33
export default App;

3. Update Roster.tsx

TYPESCRIPT
1
import React from "react";
2
import { observer } from "mobx-react";
3
import TradeForm from "./TradeForm";
4
import { useTeamStore } from "./TeamStore";
5
6
function Roster() {
7
const { players } = useTeamStore();
8
return (
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) => {
18
return (
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()}>
24
Wish 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
}
37
38
export default observer(Roster);

4. Repurpose money form to add players to roster

TYPESCRIPT
1
import React from "react";
2
import { action, observable } from "mobx";
3
import { observer } from "mobx-react-lite";
4
import Athlete from "./Athlete";
5
import { useTeamStore } from "./TeamStore";
6
7
type FormState = {
8
name: string;
9
age: number;
10
salary: number;
11
};
12
13
const initialState: FormState = {
14
name: "",
15
age: 0,
16
salary: 0,
17
};
18
19
let formState: FormState = observable({
20
name: "",
21
age: 0,
22
salary: 0,
23
});
24
25
// eslint-disable-next-line mobx/missing-observer
26
function MoneyForm() {
27
const { totalYearlyCost, addPlayer } = useTeamStore();
28
29
return (
30
<div style={{ display: "flex", flexDirection: "column" }}>
31
<h1 style={{ marginBottom: 0 }}>Money Talks</h1>
32
<>Total: {totalYearlyCost} Million</>
33
<input
34
type="text"
35
placeholder="Player name..."
36
style={{ height: "40px" }}
37
value={formState.name}
38
onChange={action((e) => {
39
formState.name = e.target.value;
40
})}
41
/>
42
<input
43
type="number"
44
placeholder="Player age..."
45
style={{ height: "40px" }}
46
value={formState.age}
47
onChange={action((e) => {
48
formState.age = Number(e.target.value);
49
})}
50
/>
51
<input
52
type="number"
53
placeholder="Yearly salary..."
54
style={{ height: "40px" }}
55
value={formState.salary}
56
onChange={action((e) => {
57
formState.salary = Number(e.target.value);
58
})}
59
/>
60
<button
61
type="button"
62
onClick={action((e) => {
63
addPlayer(new Athlete(formState.name, formState.age, formState.salary));
64
formState = initialState;
65
})}
66
>
67
Add Player
68
</button>
69
</div>
70
);
71
}
72
73
export default observer(MoneyForm);

5. Add TeamInfo Component

TYPESCRIPT
1
import { observer } from "mobx-react";
2
import React from "react";
3
import { useTeamStore } from "./TeamStore";
4
5
// eslint-disable-next-line mobx/missing-observer
6
function TeamNameInfo() {
7
const { teamName, setMascot } = useTeamStore();
8
return (
9
<>
10
<h1 style={{ marginBottom: 1 }}>Team: {teamName}</h1>
11
<input
12
type="text"
13
placeholder="Change mascot"
14
onChange={(e) => setMascot(e.target.value)}
15
style={{ marginBottom: 8 }}
16
/>{" "}
17
</>
18
);
19
}
20
21
export default observer(TeamNameInfo);

5. Update TeamStore

TYPESCRIPT
1
import { makeAutoObservable } from "mobx";
2
import React, { useContext, useEffect, useRef } from "react";
3
import Athlete from "./Athlete";
4
5
export default class TeamStore {
6
constructor(players: Athlete[]) {
7
this.players = players
8
makeAutoObservable(this);
9
}
10
11
state: string = "Maine";
12
13
mascot: string = "";
14
setMascot = (mascot: string) => {
15
this.mascot = mascot;
16
};
17
18
players: Athlete[] = [];
19
setPlayers = (players: Athlete[]) => {
20
this.players = players;
21
};
22
23
get teamName(): string {
24
return `${this.state} ${this.mascot}`;
25
}
26
27
get totalYearlyCost(): number {
28
return this.players.reduce((totalSalary, currentAthlete) => totalSalary + currentAthlete.salary, 0);
29
}
30
31
addPlayer = (player: Athlete) => {
32
this.setPlayers([...this.players, player]);
33
};
34
}
35
36
const TeamStoreContext = React.createContext<TeamStore>(null as unknown as TeamStore);
37
38
export const useTeamStore = () => useContext(TeamStoreContext);
39
40
type Props = {
41
children: React.ReactNode;
42
players: Athlete[];
43
};
44
45
export function TeamStoreProvider({ children, players }: Props) {
46
const store = useRef(new TeamStore(players));
47
48
return <TeamStoreContext.Provider value={store.current}>{children}</TeamStoreContext.Provider>;
49
}

6. Update Roster

TYPESCRIPT
1
import React from "react";
2
import { observer } from "mobx-react";
3
import TradeForm from "./TradeForm";
4
import { useTeamStore } from "./TeamStore";
5
6
function Roster() {
7
const { players } = useTeamStore();
8
return (
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) => {
18
return (
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()}>
24
Wish 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
}
37
38
export default observer(Roster);

7. Finilize App.tsx

TYPESCRIPT
1
import React from "react";
2
import "./App.css";
3
import Athlete from "./Athlete";
4
import MoneyForm from "./MoneyForm";
5
import Roster from "./Roster";
6
import TeamNameInfo from "./TeamNameInfo";
7
import { TeamStoreProvider } from "./TeamStore";
8
9
const lebronJames = new Athlete("Lebron James", 37, 5);
10
const stephCurry = new Athlete("Steph Curry", 34, 9);
11
12
function getPlayersFromBackend(): Athlete[] {
13
return [lebronJames, stephCurry];
14
}
15
16
function App() {
17
// fetch players
18
const players = getPlayersFromBackend();
19
20
return (
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
}
34
35
export default App;