├── .gitignore
├── .vscode
└── settings.json
├── App.css
├── README.md
├── comparison.mdx
├── conclusion.mdx
├── deck.js
├── demo-1.mdx
├── demo-1
├── App.js
└── Components.js
├── demo-2.mdx
├── demo-2
├── App.js
├── Components.js
└── machine.js
├── demo-3.mdx
├── demo-3
├── App.js
├── Components.js
└── machine.js
├── demo-4.mdx
├── demo-4
├── App.js
├── Components.js
├── machine.js
└── useMachine.js
├── demo-5.mdx
├── demo-5
├── App.js
├── Components.js
├── machine.js
└── useMachine.js
├── demo-6.mdx
├── demo-6
├── App.js
├── Components.js
├── machine.js
└── useMachine.js
├── demo-a.mdx
├── demo-a
├── App.js
├── AppWithMachine.js
└── machine.js
├── demo-b.mdx
├── demo-b
├── App.js
├── AppWithMachine.js
└── machine.js
├── demo-c.mdx
├── demo-c
├── App.js
├── AppWithMachine.js
└── machine.js
├── demo-d.mdx
├── demo-d
├── App.js
├── AppWithMachine.js
└── machine.js
├── guards-2.mdx
├── guards.mdx
├── hierarchy.mdx
├── images
├── createView-state-machine.png
├── designers-flow-diagram.png
├── editView-state-machine.png
├── how-do-we-reach-the-create-state.jpg
├── listView-state-machine.png
├── user-flow.gif
└── visibility-state-machine.png
├── interpreter.mdx
├── introduction.mdx
├── package-lock.json
├── package.json
├── shared
├── Components.js
└── useMachine.js
├── solution.mdx
├── toggle.mdx
└── use-machine.mdx
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.printWidth": 80,
3 | "prettier.trailingComma": "es5"
4 | }
5 |
--------------------------------------------------------------------------------
/App.css:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3,
4 | h4,
5 | h5,
6 | h6 {
7 | color: white !important;
8 | }
9 | .app {
10 | align-items: center;
11 | display: flex;
12 | flex-direction: column;
13 | height: 100vh;
14 | justify-content: center;
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MDX Deck + Code Surfer template
2 |
3 | This project was generated with the `npm init code-surfer-deck` command.
4 |
5 | ## Development
6 |
7 | To run the presentation deck in development mode:
8 |
9 | ```sh
10 | npm start
11 | ```
12 |
13 | Edit the [`deck.mdx`](deck.mdx) file to get started.
14 |
15 | ## Exporting
16 |
17 | To build the presentation deck:
18 |
19 | ```sh
20 | npm run build
21 | ```
22 |
23 | For more documentation see [mdx-deck](https://github.com/jxnblk/mdx-deck) and [code-surfer](https://github.com/pomber/code-surfer)
24 |
--------------------------------------------------------------------------------
/comparison.mdx:
--------------------------------------------------------------------------------
1 | import { Image } from '@mdx-deck/components'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 | # Comparison ♽
5 |
6 | - Conventional implementation ⚛️
7 | - State Machine implementation ⚙️
8 |
9 | ---
10 |
11 |
12 |
13 | ```jsx 1 title="App without State Machine ⚛️"
14 | import React, { useState } from 'react'
15 | import { Button, Modal } from '../shared/Components'
16 |
17 | export const App = () => {
18 | const [visible, setVisible] = useState(false)
19 | const [editing, setEditing] = useState(false)
20 | const [data, setData] = useState([])
21 | const [index, setIndex] = useState(-1)
22 |
23 | let stateValue
24 | if (visible && !editing && data && data.length > 1 && index < 0) {
25 | stateValue = 'list'
26 | } else if (
27 | visible &&
28 | !editing &&
29 | data &&
30 | ((data.length > 0 && index >= 0) || data.length === 1)
31 | ) {
32 | stateValue = 'view'
33 | } else if (visible && editing) {
34 | stateValue = 'edit'
35 | } else if (visible && (!data || data.length === 0)) {
36 | stateValue = 'create'
37 | }
38 |
39 | return (
40 |
41 | {
43 | setVisible(true)
44 | setData(['', ''])
45 | }}
46 | >
47 | Open Multiple
48 |
49 | {
51 | setVisible(true)
52 | setData([''])
53 | }}
54 | >
55 | Open Exists
56 |
57 | setVisible(true)}>Open Empty
58 | {
60 | setVisible(false)
61 | setEditing(false)
62 | setData([])
63 | setIndex(-1)
64 | }}
65 | onEdit={() => setEditing(true)}
66 | onSubmit={() => {
67 | setVisible(false)
68 | setEditing(false)
69 | setData([])
70 | setIndex(-1)
71 | }}
72 | onBack={() => setIndex(-1)}
73 | onView={() => setIndex(0)}
74 | stateValue={stateValue}
75 | />
76 |
77 | )
78 | }
79 | ```
80 |
81 | ```jsx 1,3:4 title="App" subtitle="Replace multiple useState with useMachine"
82 | import React from 'react'
83 | import { Button, Modal } from '../shared/Components'
84 | import { useMachine } from '../shared/useMachine'
85 | import { modalMachine } from './machine'
86 |
87 | export const App = () => {
88 | const [visible, setVisible] = useState(false)
89 | const [editing, setEditing] = useState(false)
90 | const [data, setData] = useState([])
91 | const [index, setIndex] = useState(-1)
92 |
93 | let stateValue
94 | if (visible && !editing && data && data.length > 1 && index < 0) {
95 | stateValue = 'list'
96 | } else if (
97 | visible &&
98 | !editing &&
99 | data &&
100 | ((data.length > 0 && index >= 0) || data.length === 1)
101 | ) {
102 | stateValue = 'view'
103 | } else if (visible && editing) {
104 | stateValue = 'edit'
105 | } else if (visible && (!data || data.length === 0)) {
106 | stateValue = 'create'
107 | }
108 |
109 | return (
110 |
111 | {
113 | setVisible(true)
114 | setData(['', ''])
115 | }}
116 | >
117 | Open Multiple
118 |
119 | {
121 | setVisible(true)
122 | setData([''])
123 | }}
124 | >
125 | Open Exists
126 |
127 | setVisible(true)}>Open Empty
128 | {
130 | setVisible(false)
131 | setEditing(false)
132 | setData([])
133 | setIndex(-1)
134 | }}
135 | onEdit={() => setEditing(true)}
136 | onSubmit={() => {
137 | setVisible(false)
138 | setEditing(false)
139 | setData([])
140 | setIndex(-1)
141 | }}
142 | onBack={() => setIndex(-1)}
143 | onView={() => setIndex(0)}
144 | stateValue={stateValue}
145 | />
146 |
147 | )
148 | }
149 | ```
150 |
151 | ```jsx 7:26 title="App" subtitle="Replace multiple useState with useMachine"
152 | import React from 'react'
153 | import { Button, Modal } from '../shared/Components'
154 | import { useMachine } from '../shared/useMachine'
155 | import { modalMachine } from './machine'
156 |
157 | export const App = () => {
158 | const [visible, setVisible] = useState(false)
159 | const [editing, setEditing] = useState(false)
160 | const [data, setData] = useState([])
161 | const [index, setIndex] = useState(-1)
162 |
163 | let stateValue
164 | if (visible && !editing && data && data.length > 1 && index < 0) {
165 | stateValue = 'list'
166 | } else if (
167 | visible &&
168 | !editing &&
169 | data &&
170 | ((data.length > 0 && index >= 0) || data.length === 1)
171 | ) {
172 | stateValue = 'view'
173 | } else if (visible && editing) {
174 | stateValue = 'edit'
175 | } else if (visible && (!data || data.length === 0)) {
176 | stateValue = 'create'
177 | }
178 |
179 | return (
180 |
181 | {
183 | setVisible(true)
184 | setData(['', ''])
185 | }}
186 | >
187 | Open Multiple
188 |
189 | {
191 | setVisible(true)
192 | setData([''])
193 | }}
194 | >
195 | Open Exists
196 |
197 | setVisible(true)}>Open Empty
198 | {
200 | setVisible(false)
201 | setEditing(false)
202 | setData([])
203 | setIndex(-1)
204 | }}
205 | onEdit={() => setEditing(true)}
206 | onSubmit={() => {
207 | setVisible(false)
208 | setEditing(false)
209 | setData([])
210 | setIndex(-1)
211 | }}
212 | onBack={() => setIndex(-1)}
213 | onView={() => setIndex(0)}
214 | stateValue={stateValue}
215 | />
216 |
217 | )
218 | }
219 | ```
220 |
221 | ```jsx 7 title="App" subtitle="Replace multiple useState with useMachine"
222 | import React from 'react'
223 | import { Button, Modal } from '../shared/Components'
224 | import { useMachine } from '../shared/useMachine'
225 | import { modalMachine } from './machine'
226 |
227 | export const App = () => {
228 | const [current, send] = useMachine(modalMachine, { devTools: true })
229 |
230 | return (
231 |
232 | {
234 | setVisible(true)
235 | setData(['', ''])
236 | }}
237 | >
238 | Open Multiple
239 |
240 | {
242 | setVisible(true)
243 | setData([''])
244 | }}
245 | >
246 | Open Exists
247 |
248 | setVisible(true)}>Open Empty
249 | {
251 | setVisible(false)
252 | setEditing(false)
253 | setData([])
254 | setIndex(-1)
255 | }}
256 | onEdit={() => setEditing(true)}
257 | onSubmit={() => {
258 | setVisible(false)
259 | setEditing(false)
260 | setData([])
261 | setIndex(-1)
262 | }}
263 | onBack={() => setIndex(-1)}
264 | onView={() => setIndex(0)}
265 | stateValue={stateValue}
266 | />
267 |
268 | )
269 | }
270 | ```
271 |
272 | ```jsx 11:45 title="App" subtitle="Replace implicit setState calls into explicit events"
273 | import React from 'react'
274 | import { Button, Modal } from '../shared/Components'
275 | import { useMachine } from '../shared/useMachine'
276 | import { modalMachine } from './machine'
277 |
278 | export const App = () => {
279 | const [current, send] = useMachine(modalMachine, { devTools: true })
280 |
281 | return (
282 |
283 | {
285 | setVisible(true)
286 | setData(['', ''])
287 | }}
288 | >
289 | Open Multiple
290 |
291 | {
293 | setVisible(true)
294 | setData([''])
295 | }}
296 | >
297 | Open Exists
298 |
299 | setVisible(true)}>Open Empty
300 | {
302 | setVisible(false)
303 | setEditing(false)
304 | setData([])
305 | setIndex(-1)
306 | }}
307 | onEdit={() => setEditing(true)}
308 | onSubmit={() => {
309 | setVisible(false)
310 | setEditing(false)
311 | setData([])
312 | setIndex(-1)
313 | }}
314 | onBack={() => setIndex(-1)}
315 | onView={() => setIndex(0)}
316 | stateValue={stateValue}
317 | />
318 |
319 | )
320 | }
321 | ```
322 |
323 | ```jsx 11:27 title="App" subtitle="Replace implicit setState calls into explicit events"
324 | import React from 'react'
325 | import { Button, Modal } from '../shared/Components'
326 | import { useMachine } from '../shared/useMachine'
327 | import { modalMachine } from './machine'
328 |
329 | export const App = () => {
330 | const [current, send] = useMachine(modalMachine, { devTools: true })
331 |
332 | return (
333 |
334 | send({ type: 'OPEN', data: ['', ''] })}>
335 | Open Multiple
336 |
337 | send({ type: 'OPEN', data: [''] })}>
338 | Open Exists
339 |
340 | send({ type: 'OPEN', data: [] })}>
341 | Open Empty
342 |
343 | send('CLOSE')}
345 | onEdit={() => send('EDIT')}
346 | onSubmit={() => send('SUBMIT')}
347 | onBack={() => send('BACK')}
348 | onView={() => send('VIEW')}
349 | stateValue={current.value.visible}
350 | />
351 |
352 | )
353 | }
354 | ```
355 |
356 | ```jsx 1,3:4,7,11:27 title="App with State Machine 🛠"
357 | import React from 'react'
358 | import { Button, Modal } from '../shared/Components'
359 | import { useMachine } from '../shared/useMachine'
360 | import { modalMachine } from './machine'
361 |
362 | export const App = () => {
363 | const [current, send] = useMachine(modalMachine, { devTools: true })
364 |
365 | return (
366 |
367 | send({ type: 'OPEN', data: ['', ''] })}>
368 | Open Multiple
369 |
370 | send({ type: 'OPEN', data: [''] })}>
371 | Open Exists
372 |
373 | send({ type: 'OPEN', data: [] })}>
374 | Open Empty
375 |
376 | send('CLOSE')}
378 | onEdit={() => send('EDIT')}
379 | onSubmit={() => send('SUBMIT')}
380 | onBack={() => send('BACK')}
381 | onView={() => send('VIEW')}
382 | stateValue={current.value.visible}
383 | />
384 |
385 | )
386 | }
387 | ```
388 |
389 |
390 |
391 | ---
392 |
393 | # What about the Machine? ⚙️
394 |
395 | - Isn't it still complex? Yes.
396 | - But it can be [visualized](https://statecharts.github.io/xstate-viz/) easily.
397 | - Visualization reduces cognitive overload
398 |
--------------------------------------------------------------------------------
/conclusion.mdx:
--------------------------------------------------------------------------------
1 | import { Appear } from 'mdx-deck'
2 |
3 | # Advantages 🤑
4 |
5 |
6 |
7 | Now all states are visible! 👀
8 | Easier to test 🧪
9 | All state transitions are well-tested 💯
10 | Is it bug free, as I hypothesized? ❓
11 |
12 |
13 |
14 | ---
15 |
16 | # Caution! ⚠️
17 |
18 |
19 | It's not completely bug free 😧
20 |
21 |
22 | There are still some bugs coming from
23 |
24 | discrepancies between the state & the rendered views
25 |
26 | We should have tested the rendered views as well
27 | Consider Integration Tests
28 |
29 |
30 |
31 | ---
32 |
33 | # Lessons Learned 📝
34 |
35 |
36 |
37 | State Machines must be designed thoroughly 🧐
38 |
39 | Yes, it's easier to test the states,
40 |
41 | but don't forget to test the rendered views! ⚠️
42 |
43 |
44 | There's no silver bullet
45 |
46 | Just as other solutions out there, e.g.
47 |
65 |
66 |
67 |
68 |
69 | ---
70 |
71 | # Key Takeaways 🔑
72 |
73 |
74 |
75 | Use State Machines sparingly ⚠️
76 | Isolate State management into Custom Hooks ⚛️
77 | Always test your code thoroughly 🧪
78 |
79 |
80 |
81 | ---
82 |
83 | ## I barely scratch the surface of State Machines concepts ⚡️
84 |
85 | #### There are a lot more to learn! 🤓
86 |
87 | ---
88 |
89 | # Further Reading 📚
90 |
91 | - [Xstate](https://xstate.js.org/docs/#why) for more State Machines concepts & practices
92 | - [Testing Trophy](https://kentcdodds.com/blog/write-tests) by Kent C. Dodds
93 |
94 | ---
95 |
96 | # 🙏🏼 Thank You 🙏🏼
97 |
98 | - David Khourshid [@davidkpiano](https://twitter.com/davidkpiano)
99 | for his amazing [Xstate](https://xstate.js.org/) library
100 | - Rodrigo Pombo [@pomber](https://twitter.com/pomber)
101 | for his amazing [code-surfer@2.0.0-alpha5](https://www.npmjs.com/package/code-surfer/v/2.0.0-alpha.5)
102 |
103 |
104 | {' '}
105 |
106 | https://bit.ly/jsconf-asia-2019-state-machines
107 |
108 |
109 |
--------------------------------------------------------------------------------
/deck.js:
--------------------------------------------------------------------------------
1 | import 'antd/dist/antd.css'
2 | import './App.css'
3 |
4 | export { nightOwlFull as theme } from 'code-surfer/themes'
5 |
6 | import { slides as comparison } from './comparison.mdx'
7 | import { slides as conclusion } from './conclusion.mdx'
8 | import { slides as demoA } from './demo-a.mdx'
9 | import { slides as demoB } from './demo-b.mdx'
10 | import { slides as demoC } from './demo-c.mdx'
11 | import { slides as demoD } from './demo-d.mdx'
12 | import { slides as interpreter } from './interpreter.mdx'
13 | import { slides as introduction } from './introduction.mdx'
14 | import { slides as toggle } from './toggle.mdx'
15 | import { slides as useMachine } from './use-machine.mdx'
16 |
17 | export const slides = [
18 | ...introduction,
19 | ...toggle,
20 | ...demoA,
21 | ...demoB,
22 | ...demoC,
23 | ...demoD,
24 | ...comparison,
25 | ...conclusion,
26 | ]
27 |
--------------------------------------------------------------------------------
/demo-1.mdx:
--------------------------------------------------------------------------------
1 | import { App } from './demo-1/App.js'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 | # Initial App Demo
5 |
6 | ---
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ```jsx title="Modal Visibility Toggle"
15 | import React, { useState } from 'react'
16 | import { Button, Modal } from './Components'
17 |
18 | export const App = () => {
19 | const [visible, setVisible] = useState(false)
20 |
21 | const handleOpen = () => setVisible(true)
22 | const handleClose = () => setVisible(false)
23 |
24 | return (
25 |
26 | Open Modal
27 |
28 |
29 | )
30 | }
31 | ```
32 |
33 | ```jsx 5,7:8,13 title="Modal Visibility Toggle" subtitle="useState hook conventional usage"
34 | import React, { useState } from 'react'
35 | import { Button, Modal } from './Components'
36 |
37 | export const App = () => {
38 | const [visible, setVisible] = useState(false)
39 |
40 | const handleOpen = () => setVisible(true)
41 | const handleClose = () => setVisible(false)
42 |
43 | return (
44 |
45 | Open Modal
46 |
47 |
48 | )
49 | }
50 | ```
51 |
52 |
53 |
--------------------------------------------------------------------------------
/demo-1/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Modal } from './Components'
3 |
4 | export const App = () => {
5 | const [visible, setVisible] = useState(false)
6 |
7 | const handleOpen = () => setVisible(true)
8 | const handleClose = () => setVisible(false)
9 |
10 | return (
11 |
12 | Open Modal
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/demo-1/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | export const Modal = props => (
7 |
8 | Modal Content
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/demo-2.mdx:
--------------------------------------------------------------------------------
1 | import { App } from './demo-2/App.js'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 |
5 |
6 | ```jsx 5,7:8,13 title="Modal Visibility Toggle" subtitle="useState hook conventional usage"
7 | import React, { useState } from 'react'
8 | import { Button, Modal } from './Components'
9 |
10 | export const App = () => {
11 | const [visible, setVisible] = useState(false)
12 |
13 | const handleOpen = () => setVisible(true)
14 | const handleClose = () => setVisible(false)
15 |
16 | return (
17 |
18 | Open Modal
19 |
20 |
21 | )
22 | }
23 | ```
24 |
25 | ```jsx 3,6,8:10,15 title="Modal Visibility Toggle" subtitle="useState hook with state machine"
26 | import React, { useState } from 'react'
27 | import { Button, Modal } from './Components'
28 | import { modalMachine } from './machine'
29 |
30 | export const App = () => {
31 | const [current, setCurrent] = useState(modalMachine.initialState)
32 |
33 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
34 | const handleClose = () =>
35 | setCurrent(modalMachine.transition(current, 'CLOSE'))
36 |
37 | return (
38 |
39 | Open Modal
40 |
41 |
42 | )
43 | }
44 | ```
45 |
46 |
47 |
48 | ---
49 |
50 |
51 |
--------------------------------------------------------------------------------
/demo-2/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Modal } from './Components'
3 | import { modalMachine } from './machine'
4 |
5 | export const App = () => {
6 | const [current, setCurrent] = useState(modalMachine.initialState)
7 |
8 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
9 | const handleClose = () =>
10 | setCurrent(modalMachine.transition(current, 'CLOSE'))
11 |
12 | return (
13 |
14 | Open Modal
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/demo-2/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | export const Modal = props => (
7 |
8 | Modal Content
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/demo-2/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | initial: 'invisible',
5 | states: {
6 | invisible: {
7 | on: {
8 | OPEN: 'visible'
9 | }
10 | },
11 | visible: {
12 | on: {
13 | CLOSE: 'invisible'
14 | }
15 | }
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/demo-3.mdx:
--------------------------------------------------------------------------------
1 | import { App } from './demo-3/App.js'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 |
5 |
6 | ```jsx 6 title="Using Interpreter" subtitle="We're keeping track of the current machine state already"
7 | import React, { useState } from 'react'
8 | import { Button, Modal } from './Components'
9 | import { modalMachine } from './machine'
10 |
11 | export const App = () => {
12 | const [current, setCurrent] = useState(modalMachine.initialState)
13 |
14 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
15 | const handleClose = () =>
16 | setCurrent(modalMachine.transition(current, 'CLOSE'))
17 |
18 | return (
19 |
20 | Open Modal
21 |
22 |
23 | )
24 | }
25 | ```
26 |
27 | ```jsx 8 title="Using Interpreter" subtitle="Reference the service"
28 | import React, { useState, useRef } from 'react'
29 | import { Button, Modal } from './Components'
30 | import { modalMachine } from './machine'
31 |
32 | export const App = () => {
33 | const [current, setCurrent] = useState(modalMachine.initialState)
34 |
35 | const serviceRef = useRef(null)
36 |
37 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
38 | const handleClose = () =>
39 | setCurrent(modalMachine.transition(current, 'CLOSE'))
40 |
41 | return (
42 |
43 | Open Modal
44 |
45 |
46 | )
47 | }
48 | ```
49 |
50 | ```jsx 4,11:12,14:15 title="Using Interpreter" subtitle="Create the service only once"
51 | import React, { useState, useRef } from 'react'
52 | import { Button, Modal } from './Components'
53 | import { modalMachine } from './machine'
54 | import { interpret } from 'xstate'
55 |
56 | export const App = () => {
57 | const [current, setCurrent] = useState(modalMachine.initialState)
58 |
59 | const serviceRef = useRef(null)
60 |
61 | if (serviceRef.current === null) {
62 | serviceRef.current = interpret(modalMachine).onTransition(state => {
63 | //
64 | })
65 | }
66 |
67 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
68 | const handleClose = () =>
69 | setCurrent(modalMachine.transition(current, 'CLOSE'))
70 |
71 | return (
72 |
73 | Open Modal
74 |
75 |
76 | )
77 | }
78 | ```
79 |
80 | ```jsx 13:15 title="Using Interpreter" subtitle="Update the current machine state when a transition occurs"
81 | import React, { useState, useRef } from 'react'
82 | import { Button, Modal } from './Components'
83 | import { modalMachine } from './machine'
84 | import { interpret } from 'xstate'
85 |
86 | export const App = () => {
87 | const [current, setCurrent] = useState(modalMachine.initialState)
88 |
89 | const serviceRef = useRef(null)
90 |
91 | if (serviceRef.current === null) {
92 | serviceRef.current = interpret(modalMachine).onTransition(state => {
93 | if (state.changed) {
94 | setCurrent(state)
95 | }
96 | })
97 | }
98 |
99 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
100 | const handleClose = () =>
101 | setCurrent(modalMachine.transition(current, 'CLOSE'))
102 |
103 | return (
104 |
105 | Open Modal
106 |
107 |
108 | )
109 | }
110 | ```
111 |
112 | ```jsx 19 title="Using Interpreter" subtitle="Get the current serviceRef"
113 | import React, { useState, useRef } from 'react'
114 | import { Button, Modal } from './Components'
115 | import { modalMachine } from './machine'
116 | import { interpret } from 'xstate'
117 |
118 | export const App = () => {
119 | const [current, setCurrent] = useState(modalMachine.initialState)
120 |
121 | const serviceRef = useRef(null)
122 |
123 | if (serviceRef.current === null) {
124 | serviceRef.current = interpret(modalMachine).onTransition(state => {
125 | if (state.changed) {
126 | setCurrent(state)
127 | }
128 | })
129 | }
130 |
131 | const service = serviceRef.current
132 |
133 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
134 | const handleClose = () =>
135 | setCurrent(modalMachine.transition(current, 'CLOSE'))
136 |
137 | return (
138 |
139 | Open Modal
140 |
141 |
142 | )
143 | }
144 | ```
145 |
146 | ```jsx 21:23 title="Using Interpreter" subtitle="Start the service when the component mounts"
147 | import React, { useState, useRef, useEffect } from 'react'
148 | import { Button, Modal } from './Components'
149 | import { modalMachine } from './machine'
150 | import { interpret } from 'xstate'
151 |
152 | export const App = () => {
153 | const [current, setCurrent] = useState(modalMachine.initialState)
154 |
155 | const serviceRef = useRef(null)
156 |
157 | if (serviceRef.current === null) {
158 | serviceRef.current = interpret(modalMachine).onTransition(state => {
159 | if (state.changed) {
160 | setCurrent(state)
161 | }
162 | })
163 | }
164 |
165 | const service = serviceRef.current
166 |
167 | useEffect(() => {
168 | service.start()
169 | }, [])
170 |
171 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
172 | const handleClose = () =>
173 | setCurrent(modalMachine.transition(current, 'CLOSE'))
174 |
175 | return (
176 |
177 | Open Modal
178 |
179 |
180 | )
181 | }
182 | ```
183 |
184 | ```jsx 24:26 title="Using Interpreter" subtitle="Stop the service when the component unmounts"
185 | import React, { useState, useRef, useEffect } from 'react'
186 | import { Button, Modal } from './Components'
187 | import { modalMachine } from './machine'
188 | import { interpret } from 'xstate'
189 |
190 | export const App = () => {
191 | const [current, setCurrent] = useState(modalMachine.initialState)
192 |
193 | const serviceRef = useRef(null)
194 |
195 | if (serviceRef.current === null) {
196 | serviceRef.current = interpret(modalMachine).onTransition(state => {
197 | if (state.changed) {
198 | setCurrent(state)
199 | }
200 | })
201 | }
202 |
203 | const service = serviceRef.current
204 |
205 | useEffect(() => {
206 | service.start()
207 |
208 | return () => {
209 | service.stop()
210 | }
211 | }, [])
212 |
213 | const handleOpen = () => setCurrent(modalMachine.transition(current, 'OPEN'))
214 | const handleClose = () =>
215 | setCurrent(modalMachine.transition(current, 'CLOSE'))
216 |
217 | return (
218 |
219 | Open Modal
220 |
221 |
222 | )
223 | }
224 | ```
225 |
226 | ```jsx 29:30 title="Using Interpreter" subtitle="Use the service to send events"
227 | import React, { useState, useRef, useEffect } from 'react'
228 | import { Button, Modal } from './Components'
229 | import { modalMachine } from './machine'
230 | import { interpret } from 'xstate'
231 |
232 | export const App = () => {
233 | const [current, setCurrent] = useState(modalMachine.initialState)
234 |
235 | const serviceRef = useRef(null)
236 |
237 | if (serviceRef.current === null) {
238 | serviceRef.current = interpret(modalMachine).onTransition(state => {
239 | if (state.changed) {
240 | setCurrent(state)
241 | }
242 | })
243 | }
244 |
245 | const service = serviceRef.current
246 |
247 | useEffect(() => {
248 | service.start()
249 |
250 | return () => {
251 | service.stop()
252 | }
253 | }, [])
254 |
255 | const handleOpen = () => service.send('OPEN')
256 | const handleClose = () => service.send('CLOSE')
257 |
258 | return (
259 |
260 | Open Modal
261 |
262 |
263 | )
264 | }
265 | ```
266 |
267 | ```jsx 12:14 title="Using Interpreter" subtitle="Bonus: Redux Dev Tools integration"
268 | import React, { useState, useRef, useEffect } from 'react'
269 | import { Button, Modal } from './Components'
270 | import { modalMachine } from './machine'
271 | import { interpret } from 'xstate'
272 |
273 | export const App = () => {
274 | const [current, setCurrent] = useState(modalMachine.initialState)
275 |
276 | const serviceRef = useRef(null)
277 |
278 | if (serviceRef.current === null) {
279 | serviceRef.current = interpret(modalMachine, {
280 | devTools: true,
281 | }).onTransition(state => {
282 | if (state.changed) {
283 | setCurrent(state)
284 | }
285 | })
286 | }
287 |
288 | const service = serviceRef.current
289 |
290 | useEffect(() => {
291 | service.start()
292 |
293 | return () => {
294 | service.stop()
295 | }
296 | }, [])
297 |
298 | const handleOpen = () => service.send('OPEN')
299 | const handleClose = () => service.send('CLOSE')
300 |
301 | return (
302 |
303 | Open Modal
304 |
305 |
306 | )
307 | }
308 | ```
309 |
310 |
311 |
312 | ---
313 |
314 |
315 |
--------------------------------------------------------------------------------
/demo-3/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 | import { interpret } from 'xstate'
3 | import { Button, Modal } from './Components'
4 | import { modalMachine } from './machine'
5 |
6 | export const App = () => {
7 | const [current, setCurrent] = useState(modalMachine.initialState)
8 |
9 | const serviceRef = useRef(null)
10 |
11 | if (serviceRef.current === null) {
12 | serviceRef.current = interpret(modalMachine, {
13 | devTools: true,
14 | }).onTransition(state => {
15 | if (state.changed) {
16 | setCurrent(state)
17 | }
18 | })
19 | }
20 |
21 | const service = serviceRef.current
22 |
23 | useEffect(() => {
24 | service.start()
25 |
26 | return () => {
27 | service.stop()
28 | }
29 | }, [service])
30 |
31 | const handleOpen = () => service.send('OPEN')
32 | const handleClose = () => service.send('CLOSE')
33 |
34 | return (
35 |
36 | Open Modal
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/demo-3/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | export const Modal = props => (
7 |
8 | Modal Content
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/demo-3/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | initial: 'invisible',
5 | states: {
6 | invisible: {
7 | on: {
8 | OPEN: 'visible'
9 | }
10 | },
11 | visible: {
12 | on: {
13 | CLOSE: 'invisible'
14 | }
15 | }
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/demo-4.mdx:
--------------------------------------------------------------------------------
1 | import { App } from './demo-4/App.js'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 |
5 |
6 | ```jsx 7:29 title="useMachine hook" subtitle="Replace the extracted chunk of code with the custom hook"
7 | import React, { useState, useRef, useEffect } from 'react'
8 | import { Button, Modal } from './Components'
9 | import { modalMachine } from './machine'
10 | import { interpret } from 'xstate'
11 |
12 | export const App = () => {
13 | const [current, setCurrent] = useState(modalMachine.initialState)
14 |
15 | const serviceRef = useRef(null)
16 |
17 | if (serviceRef.current === null) {
18 | serviceRef.current = interpret(modalMachine, {
19 | devTools: true,
20 | }).onTransition(state => {
21 | if (state.changed) {
22 | setCurrent(state)
23 | }
24 | })
25 | }
26 |
27 | const service = serviceRef.current
28 |
29 | useEffect(() => {
30 | service.start()
31 |
32 | return () => {
33 | service.stop()
34 | }
35 | }, [])
36 |
37 | const handleOpen = () => service.send('OPEN')
38 | const handleClose = () => service.send('CLOSE')
39 |
40 | return (
41 |
42 | Open Modal
43 |
44 |
45 | )
46 | }
47 | ```
48 |
49 | ```jsx 7 title="useMachine hook" subtitle="🎉"
50 | import React, { useState, useRef, useEffect } from 'react'
51 | import { Button, Modal } from './Components'
52 | import { modalMachine } from './machine'
53 | import { interpret } from 'xstate'
54 |
55 | export const App = () => {
56 | const [current, send] = useMachine(modalMachine, { devTools: true })
57 |
58 | const handleOpen = () => service.send('OPEN')
59 | const handleClose = () => service.send('CLOSE')
60 |
61 | return (
62 |
63 | Open Modal
64 |
65 |
66 | )
67 | }
68 | ```
69 |
70 | ```jsx 1,4 title="useMachine hook" subtitle="Refactor import statements accordingly"
71 | import React from 'react'
72 | import { Button, Modal } from './Components'
73 | import { modalMachine } from './machine'
74 | import { useMachine } from './useMachine'
75 |
76 | export const App = () => {
77 | const [current, send] = useMachine(modalMachine, { devTools: true })
78 |
79 | const handleOpen = () => service.send('OPEN')
80 | const handleClose = () => service.send('CLOSE')
81 |
82 | return (
83 |
84 | Open Modal
85 |
86 |
87 | )
88 | }
89 | ```
90 |
91 | ```jsx 9:10 title="useMachine hook" subtitle="Refactor send event"
92 | import React from 'react'
93 | import { Button, Modal } from './Components'
94 | import { modalMachine } from './machine'
95 | import { useMachine } from './useMachine'
96 |
97 | export const App = () => {
98 | const [current, send] = useMachine(modalMachine, { devTools: true })
99 |
100 | const handleOpen = () => send('OPEN')
101 | const handleClose = () => send('CLOSE')
102 |
103 | return (
104 |
105 | Open Modal
106 |
107 |
108 | )
109 | }
110 | ```
111 |
112 | ```jsx 7:17 title="useMachine hook" subtitle="That's it, now we're ready to add more features! 😎"
113 | import React from 'react'
114 | import { Button, Modal } from './Components'
115 | import { modalMachine } from './machine'
116 | import { useMachine } from './useMachine'
117 |
118 | export const App = () => {
119 | const [current, send] = useMachine(modalMachine, { devTools: true })
120 |
121 | const handleOpen = () => send('OPEN')
122 | const handleClose = () => send('CLOSE')
123 |
124 | return (
125 |
126 | Open Modal
127 |
128 |
129 | )
130 | }
131 | ```
132 |
133 |
134 |
135 | ---
136 |
137 |
138 |
--------------------------------------------------------------------------------
/demo-4/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from './Components'
3 | import { modalMachine } from './machine'
4 | import { useMachine } from './useMachine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | const handleOpen = () => send('OPEN')
10 | const handleClose = () => send('CLOSE')
11 |
12 | return (
13 |
14 | Open Modal
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/demo-4/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | export const Modal = props => (
7 |
8 | Modal Content
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/demo-4/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | initial: 'invisible',
5 | states: {
6 | invisible: {
7 | on: {
8 | OPEN: 'visible'
9 | }
10 | },
11 | visible: {
12 | on: {
13 | CLOSE: 'invisible'
14 | }
15 | }
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/demo-4/useMachine.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react'
2 | import { interpret } from 'xstate'
3 |
4 | export function useMachine(machine, options) {
5 | const [current, setCurrent] = useState(machine.initialState)
6 |
7 | const serviceRef = useRef(null)
8 |
9 | if (serviceRef.current === null) {
10 | serviceRef.current = interpret(machine, options).onTransition(state => {
11 | if (state.changed) {
12 | setCurrent(state)
13 | }
14 | })
15 | }
16 |
17 | const service = serviceRef.current
18 |
19 | useEffect(() => {
20 | service.start()
21 |
22 | return () => {
23 | service.stop()
24 | }
25 | }, [service])
26 |
27 | return [current, service.send]
28 | }
29 |
--------------------------------------------------------------------------------
/demo-5.mdx:
--------------------------------------------------------------------------------
1 | import { App } from './demo-5/App.js'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 |
5 |
6 | ```jsx title="Implement Modal states" subtitle="Using the Hierarchical State"
7 | import React from 'react'
8 | import { Button, Modal } from './Components'
9 | import { modalMachine } from './machine'
10 | import { useMachine } from './useMachine'
11 |
12 | export const App = () => {
13 | const [current, send] = useMachine(modalMachine, { devTools: true })
14 |
15 | const handleOpen = () => send('OPEN')
16 | const handleClose = () => send('CLOSE')
17 |
18 | return (
19 |
20 | Open Modal
21 |
22 |
23 | )
24 | }
25 | ```
26 |
27 | ```jsx 11:14 title="Implement Modal states" subtitle="Define event handlers"
28 | import React from 'react'
29 | import { Button, Modal } from './Components'
30 | import { modalMachine } from './machine'
31 | import { useMachine } from './useMachine'
32 |
33 | export const App = () => {
34 | const [current, send] = useMachine(modalMachine, { devTools: true })
35 |
36 | const handleOpen = () => send('OPEN')
37 | const handleClose = () => send('CLOSE')
38 | const handleView = () => send('VIEW')
39 | const handleEdit = () => send('EDIT')
40 | const handleBack = () => send('BACK')
41 | const handleSubmit = () => send('SUBMIT')
42 |
43 | return (
44 |
45 | Open Modal
46 |
47 |
48 | )
49 | }
50 | ```
51 |
52 | ```jsx 21 title="Implement Modal states" subtitle="Pass down the value of 'visible' inner state"
53 | import React from 'react'
54 | import { Button, Modal } from './Components'
55 | import { modalMachine } from './machine'
56 | import { useMachine } from './useMachine'
57 |
58 | export const App = () => {
59 | const [current, send] = useMachine(modalMachine, { devTools: true })
60 |
61 | const handleOpen = () => send('OPEN')
62 | const handleClose = () => send('CLOSE')
63 | const handleView = () => send('VIEW')
64 | const handleEdit = () => send('EDIT')
65 | const handleBack = () => send('BACK')
66 | const handleSubmit = () => send('SUBMIT')
67 |
68 | return (
69 |
70 | Open Modal
71 |
76 |
77 | )
78 | }
79 | ```
80 |
81 | ```jsx 22:25 title="Implement Modal states" subtitle="Pass down the event handlers"
82 | import React from 'react'
83 | import { Button, Modal } from './Components'
84 | import { modalMachine } from './machine'
85 | import { useMachine } from './useMachine'
86 |
87 | export const App = () => {
88 | const [current, send] = useMachine(modalMachine, { devTools: true })
89 |
90 | const handleOpen = () => send('OPEN')
91 | const handleClose = () => send('CLOSE')
92 | const handleView = () => send('VIEW')
93 | const handleEdit = () => send('EDIT')
94 | const handleBack = () => send('BACK')
95 | const handleSubmit = () => send('SUBMIT')
96 |
97 | return (
98 |
99 | Open Modal
100 |
108 |
109 | )
110 | }
111 | ```
112 |
113 |
114 |
115 | ---
116 |
117 |
118 |
119 | ```jsx 48 title="Implement Modal states" subtitle="Render Modal conditionally"
120 | import { Button as AntButton, List, Modal as AntModal } from 'antd'
121 | import React from 'react'
122 |
123 | export const Button = props =>
124 |
125 | const PrimaryButton = props =>
126 |
127 | const ListView = ({ onView }) => (
128 |
129 |
130 | Item 1
131 |
132 |
133 | Item 2
134 |
135 |
136 | Item 3
137 |
138 |
139 | )
140 |
141 | const DetailView = ({ onBack, onEdit }) => (
142 | <>
143 | Back
144 | Edit
145 | >
146 | )
147 |
148 | const DetailEdit = ({ onSubmit }) => (
149 | Submit
150 | )
151 |
152 | export const Modal = ({
153 | onBack,
154 | onEdit,
155 | onView,
156 | onSubmit,
157 | stateValue,
158 | ...props
159 | }) => (
160 |
167 | Modal Content
168 |
169 | )
170 | ```
171 |
172 | ```jsx 48:56 title="Implement Modal states" subtitle="Render Modal conditionally"
173 | import { Button as AntButton, List, Modal as AntModal } from 'antd'
174 | import React from 'react'
175 |
176 | export const Button = props =>
177 |
178 | const PrimaryButton = props =>
179 |
180 | const ListView = ({ onView }) => (
181 |
182 |
183 | Item 1
184 |
185 |
186 | Item 2
187 |
188 |
189 | Item 3
190 |
191 |
192 | )
193 |
194 | const DetailView = ({ onBack, onEdit }) => (
195 | <>
196 | Back
197 | Edit
198 | >
199 | )
200 |
201 | const DetailEdit = ({ onSubmit }) => (
202 | Submit
203 | )
204 |
205 | export const Modal = ({
206 | onBack,
207 | onEdit,
208 | onView,
209 | onSubmit,
210 | stateValue,
211 | ...props
212 | }) => (
213 |
220 | {stateValue === 'list' ? (
221 |
222 | ) : stateValue === 'view' ? (
223 |
224 | ) : stateValue === 'edit' ? (
225 |
226 | ) : stateValue === 'create' ? (
227 |
228 | ) : null}
229 |
230 | )
231 | ```
232 |
233 |
234 |
235 | ---
236 |
237 |
238 |
--------------------------------------------------------------------------------
/demo-5/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from './Components'
3 | import { modalMachine } from './machine'
4 | import { useMachine } from './useMachine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | const handleOpen = () => send('OPEN')
10 | const handleClose = () => send('CLOSE')
11 | const handleView = () => send('VIEW')
12 | const handleEdit = () => send('EDIT')
13 | const handleBack = () => send('BACK')
14 | const handleSubmit = () => send('SUBMIT')
15 |
16 | return (
17 |
18 | Open Modal
19 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/demo-5/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, List, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | const PrimaryButton = props =>
7 |
8 | const ListView = ({ onView }) => (
9 |
10 |
11 | Item 1
12 |
13 |
14 | Item 2
15 |
16 |
17 | Item 3
18 |
19 |
20 | )
21 |
22 | const DetailView = ({ onBack, onEdit }) => (
23 | <>
24 | Back
25 | Edit
26 | >
27 | )
28 |
29 | const DetailEdit = ({ onSubmit }) => (
30 | Submit
31 | )
32 |
33 | export const Modal = ({
34 | onBack,
35 | onEdit,
36 | onView,
37 | onSubmit,
38 | stateValue,
39 | ...props
40 | }) => (
41 |
48 | {stateValue === 'list' ? (
49 |
50 | ) : stateValue === 'view' ? (
51 |
52 | ) : stateValue === 'edit' ? (
53 |
54 | ) : stateValue === 'create' ? (
55 |
56 | ) : null}
57 |
58 | )
59 |
--------------------------------------------------------------------------------
/demo-5/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | initial: 'invisible',
5 | states: {
6 | invisible: {
7 | id: 'invisible',
8 | on: {
9 | OPEN: 'visible'
10 | }
11 | },
12 | visible: {
13 | on: {
14 | CLOSE: 'invisible'
15 | },
16 | initial: 'list',
17 | states: {
18 | list: {
19 | on: {
20 | VIEW: 'view'
21 | }
22 | },
23 | view: {
24 | on: {
25 | EDIT: 'edit',
26 | BACK: 'list'
27 | }
28 | },
29 | edit: {
30 | on: {
31 | SUBMIT: '#invisible'
32 | }
33 | },
34 | create: {
35 | on: {
36 | SUBMIT: '#invisible'
37 | }
38 | }
39 | }
40 | }
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/demo-5/useMachine.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react'
2 | import { interpret } from 'xstate'
3 |
4 | export function useMachine(machine, options) {
5 | const [current, setCurrent] = useState(machine.initialState)
6 |
7 | const serviceRef = useRef(null)
8 |
9 | if (serviceRef.current === null) {
10 | serviceRef.current = interpret(machine, options).onTransition(state => {
11 | if (state.changed) {
12 | setCurrent(state)
13 | }
14 | })
15 | }
16 |
17 | const service = serviceRef.current
18 |
19 | useEffect(() => {
20 | service.start()
21 |
22 | return () => {
23 | service.stop()
24 | }
25 | }, [service])
26 |
27 | return [current, service.send]
28 | }
29 |
--------------------------------------------------------------------------------
/demo-6.mdx:
--------------------------------------------------------------------------------
1 | import { App } from './demo-6/App.js'
2 | import { CodeSurferLayout } from 'code-surfer'
3 |
4 |
5 |
6 | ```jsx title="Utilize Guards" subtitle="by sending event payload"
7 | import React from 'react'
8 | import { Button, Modal } from './Components'
9 | import { modalMachine } from './machine'
10 | import { useMachine } from './useMachine'
11 |
12 | export const App = () => {
13 | const [current, send] = useMachine(modalMachine, { devTools: true })
14 |
15 | const handleOpen = () => send('OPEN')
16 | const handleClose = () => send('CLOSE')
17 | const handleView = () => send('VIEW')
18 | const handleEdit = () => send('EDIT')
19 | const handleBack = () => send('BACK')
20 | const handleSubmit = () => send('SUBMIT')
21 |
22 | return (
23 |
24 | Open Modal
25 |
33 |
34 | )
35 | }
36 | ```
37 |
38 | ```jsx 9 title="Utilize Guards" subtitle="Refactor event handlers to send payload"
39 | import React from 'react'
40 | import { Button, Modal } from './Components'
41 | import { modalMachine } from './machine'
42 | import { useMachine } from './useMachine'
43 |
44 | export const App = () => {
45 | const [current, send] = useMachine(modalMachine, { devTools: true })
46 |
47 | const handleOpen = () => send('OPEN')
48 | const handleClose = () => send('CLOSE')
49 | const handleView = () => send('VIEW')
50 | const handleEdit = () => send('EDIT')
51 | const handleBack = () => send('BACK')
52 | const handleSubmit = () => send('SUBMIT')
53 |
54 | return (
55 |
56 | Open Modal
57 |
65 |
66 | )
67 | }
68 | ```
69 |
70 | ```jsx 9:11 title="Utilize Guards" subtitle="Refactor event handlers to send payload"
71 | import React from 'react'
72 | import { Button, Modal } from './Components'
73 | import { modalMachine } from './machine'
74 | import { useMachine } from './useMachine'
75 |
76 | export const App = () => {
77 | const [current, send] = useMachine(modalMachine, { devTools: true })
78 |
79 | const handleOpenMultiple = () => send({ type: 'OPEN', data: ['', ''] })
80 | const handleOpenSingle = () => send({ type: 'OPEN', data: [''] })
81 | const handleOpenEmpty = () => send({ type: 'OPEN', data: [] })
82 | const handleClose = () => send('CLOSE')
83 | const handleView = () => send('VIEW')
84 | const handleEdit = () => send('EDIT')
85 | const handleBack = () => send('BACK')
86 | const handleSubmit = () => send('SUBMIT')
87 |
88 | return (
89 |
90 | Open Modal
91 |
99 |
100 | )
101 | }
102 | ```
103 |
104 | ```jsx 20:22 title="Utilize Guards" subtitle="Add trigger to the event handlers"
105 | import React from 'react'
106 | import { Button, Modal } from './Components'
107 | import { modalMachine } from './machine'
108 | import { useMachine } from './useMachine'
109 |
110 | export const App = () => {
111 | const [current, send] = useMachine(modalMachine, { devTools: true })
112 |
113 | const handleOpenMultiple = () => send({ type: 'OPEN', data: ['', ''] })
114 | const handleOpenSingle = () => send({ type: 'OPEN', data: [''] })
115 | const handleOpenEmpty = () => send({ type: 'OPEN', data: [] })
116 | const handleClose = () => send('CLOSE')
117 | const handleView = () => send('VIEW')
118 | const handleEdit = () => send('EDIT')
119 | const handleBack = () => send('BACK')
120 | const handleSubmit = () => send('SUBMIT')
121 |
122 | return (
123 |
124 | Multiple Items
125 | Single Item
126 | No Item
127 |
135 |
136 | )
137 | }
138 | ```
139 |
140 |
141 |
142 | ---
143 |
144 |
145 |
--------------------------------------------------------------------------------
/demo-6/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from './Components'
3 | import { modalMachine } from './machine'
4 | import { useMachine } from './useMachine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | const handleOpenMultiple = () => send({ type: 'OPEN', data: ['', ''] })
10 | const handleOpenSingle = () => send({ type: 'OPEN', data: [''] })
11 | const handleOpenEmpty = () => send({ type: 'OPEN', data: [] })
12 | const handleClose = () => send('CLOSE')
13 | const handleView = () => send('VIEW')
14 | const handleEdit = () => send('EDIT')
15 | const handleBack = () => send('BACK')
16 | const handleSubmit = () => send('SUBMIT')
17 |
18 | return (
19 |
20 | Multiple Items
21 | Single Item
22 | No Item
23 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/demo-6/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, List, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | const PrimaryButton = props =>
7 |
8 | const ListView = ({ onView }) => (
9 |
10 |
11 | Item 1
12 |
13 |
14 | Item 2
15 |
16 |
17 | Item 3
18 |
19 |
20 | )
21 |
22 | const DetailView = ({ onBack, onEdit }) => (
23 | <>
24 | Back
25 | Edit
26 | >
27 | )
28 |
29 | const DetailEdit = ({ onSubmit }) => (
30 | Submit
31 | )
32 |
33 | export const Modal = ({
34 | onBack,
35 | onEdit,
36 | onView,
37 | onSubmit,
38 | stateValue,
39 | ...props
40 | }) => (
41 |
48 | {stateValue === 'list' ? (
49 |
50 | ) : stateValue === 'view' ? (
51 |
52 | ) : stateValue === 'edit' ? (
53 |
54 | ) : stateValue === 'create' ? (
55 |
56 | ) : null}
57 |
58 | )
59 |
--------------------------------------------------------------------------------
/demo-6/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine(
4 | {
5 | initial: 'invisible',
6 | states: {
7 | invisible: {
8 | id: 'invisible',
9 | on: {
10 | OPEN: [
11 | {
12 | cond: 'empty',
13 | target: 'visible.create'
14 | },
15 | {
16 | cond: 'single',
17 | target: 'visible.view'
18 | },
19 | { target: 'visible' }
20 | ]
21 | }
22 | },
23 | visible: {
24 | on: {
25 | CLOSE: 'invisible'
26 | },
27 | initial: 'list',
28 | states: {
29 | list: {
30 | on: {
31 | VIEW: 'view'
32 | }
33 | },
34 | view: {
35 | on: {
36 | EDIT: 'edit',
37 | BACK: 'list'
38 | }
39 | },
40 | edit: {
41 | on: {
42 | SUBMIT: '#invisible'
43 | }
44 | },
45 | create: {
46 | on: {
47 | SUBMIT: '#invisible'
48 | }
49 | }
50 | }
51 | }
52 | }
53 | },
54 | {
55 | guards: {
56 | empty: (context, event) =>
57 | !event.data || (event.data && event.data.length === 0),
58 | single: (context, event) => event.data && event.data.length === 1
59 | }
60 | }
61 | )
62 |
--------------------------------------------------------------------------------
/demo-6/useMachine.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react'
2 | import { interpret } from 'xstate'
3 |
4 | export function useMachine(machine, options) {
5 | const [current, setCurrent] = useState(machine.initialState)
6 |
7 | const serviceRef = useRef(null)
8 |
9 | if (serviceRef.current === null) {
10 | serviceRef.current = interpret(machine, options).onTransition(state => {
11 | if (state.changed) {
12 | setCurrent(state)
13 | }
14 | })
15 | }
16 |
17 | const service = serviceRef.current
18 |
19 | useEffect(() => {
20 | service.start()
21 |
22 | return () => {
23 | service.stop()
24 | }
25 | }, [service])
26 |
27 | return [current, service.send]
28 | }
29 |
--------------------------------------------------------------------------------
/demo-a.mdx:
--------------------------------------------------------------------------------
1 | import { Image } from '@mdx-deck/components'
2 | import { App } from './demo-a/App.js'
3 | import { App as AppWithMachine } from './demo-a/AppWithMachine.js'
4 | import { CodeSurferLayout } from 'code-surfer'
5 |
6 | # Let's start the journey! 🛣
7 |
8 | Each step consists of:
9 |
10 | 1. Conventional implementation using Hooks ⚛️
11 | 2. State Machine definition ⚙️
12 | 3. State Machine integration 🛠
13 |
14 | ---
15 |
16 | # Requirement 1️⃣
17 |
18 | ## Toggle Modal Visibility 👀
19 |
20 | ---
21 |
22 |
23 |
24 | ---
25 |
26 |
27 |
28 | ```jsx title="1️⃣ Toggle Modal Visibility 👀"
29 | import React, { useState } from 'react'
30 | import { Button, Modal } from '../shared/Components'
31 |
32 | export const App = () => {
33 | return (
34 |
35 | Open Modal
36 |
37 |
38 | )
39 | }
40 | ```
41 |
42 | ```jsx 5 title="1️⃣ Toggle Modal Visibility 👀" subtitle="Add 'visible' useState"
43 | import React, { useState } from 'react'
44 | import { Button, Modal } from '../shared/Components'
45 |
46 | export const App = () => {
47 | const [visible, setVisible] = useState(false)
48 |
49 | return (
50 |
51 | Open Modal
52 |
53 |
54 | )
55 | }
56 | ```
57 |
58 | ```jsx 9 title="1️⃣ Toggle Modal Visibility 👀" subtitle="setVisible 👀 on Button click"
59 | import React, { useState } from 'react'
60 | import { Button, Modal } from '../shared/Components'
61 |
62 | export const App = () => {
63 | const [visible, setVisible] = useState(false)
64 |
65 | return (
66 |
67 | setVisible(true)}>Open Modal
68 |
69 |
70 | )
71 | }
72 | ```
73 |
74 | ```jsx 10 title="1️⃣ Toggle Modal Visibility 👀" subtitle="setVisible to false 🙈 on Modal close"
75 | import React, { useState } from 'react'
76 | import { Button, Modal } from '../shared/Components'
77 |
78 | export const App = () => {
79 | const [visible, setVisible] = useState(false)
80 |
81 | return (
82 |
83 | setVisible(true)}>Open Modal
84 | setVisible(false)} stateValue={visible} />
85 |
86 | )
87 | }
88 | ```
89 |
90 | ```jsx 5,9:10 title="1️⃣ Toggle Modal Visibility 👀"
91 | import React, { useState } from 'react'
92 | import { Button, Modal } from '../shared/Components'
93 |
94 | export const App = () => {
95 | const [visible, setVisible] = useState(false)
96 |
97 | return (
98 |
99 | setVisible(true)}>Open Modal
100 | setVisible(false)} stateValue={visible} />
101 |
102 | )
103 | }
104 | ```
105 |
106 |
107 |
108 | ---
109 |
110 | # Seems simple 😄
111 |
112 | ---
113 |
114 | ## Toggle Modal Visibility 👀
115 |
116 | ### with State Machine
117 |
118 | ---
119 |
120 |
121 |
122 | ```jsx title="1️⃣ State Machine Definition ⚙️"
123 | import { Machine } from 'xstate'
124 |
125 | export const modalMachine = Machine({
126 | id: 'visibility',
127 | states: {
128 | //
129 | },
130 | })
131 | ```
132 |
133 | ```jsx 5 title="1️⃣ State Machine Definition ⚙️" subtitle="inital 'invisible' 🙈 state"
134 | import { Machine } from 'xstate'
135 |
136 | export const modalMachine = Machine({
137 | id: 'visibility',
138 | initial: 'invisible',
139 | states: {
140 | //
141 | },
142 | })
143 | ```
144 |
145 | ```jsx 7:11 title="1️⃣ State Machine Definition ⚙️" subtitle="'invisible' 🙈 state with 'OPEN' event"
146 | import { Machine } from 'xstate'
147 |
148 | export const modalMachine = Machine({
149 | id: 'visibility',
150 | initial: 'invisible',
151 | states: {
152 | invisible: {
153 | on: {
154 | OPEN: 'visible',
155 | },
156 | },
157 | },
158 | })
159 | ```
160 |
161 | ```jsx 12:16 title="1️⃣ State Machine Definition ⚙️" subtitle="'visible' 👀 state with 'CLOSE' event"
162 | import { Machine } from 'xstate'
163 |
164 | export const modalMachine = Machine({
165 | id: 'visibility',
166 | initial: 'invisible',
167 | states: {
168 | invisible: {
169 | on: {
170 | OPEN: 'visible',
171 | },
172 | },
173 | visible: {
174 | on: {
175 | CLOSE: 'invisible',
176 | },
177 | },
178 | },
179 | })
180 | ```
181 |
182 | ```jsx 5,7:11,12:16 title="1️⃣ State Machine Definition ⚙️"
183 | import { Machine } from 'xstate'
184 |
185 | export const modalMachine = Machine({
186 | id: 'visibility',
187 | initial: 'invisible',
188 | states: {
189 | invisible: {
190 | on: {
191 | OPEN: 'visible',
192 | },
193 | },
194 | visible: {
195 | on: {
196 | CLOSE: 'invisible',
197 | },
198 | },
199 | },
200 | })
201 | ```
202 |
203 |
204 |
205 | ---
206 |
207 | # Visualization
208 |
209 |
217 |
218 | ---
219 |
220 |
221 |
222 | ```jsx title="1️⃣ App with State Machine 🛠"
223 | import React from 'react'
224 | import { Button, Modal } from '../shared/Components'
225 | import { useMachine } from '../shared/useMachine'
226 | import { modalMachine } from './machine'
227 |
228 | export const App = () => {
229 | return (
230 |
231 | Open Modal
232 |
233 |
234 | )
235 | }
236 | ```
237 |
238 | ```jsx 7 title="1️⃣ App with State Machine 🛠" subtitle="useMachine with the modalMachine"
239 | import React from 'react'
240 | import { Button, Modal } from '../shared/Components'
241 | import { useMachine } from '../shared/useMachine'
242 | import { modalMachine } from './machine'
243 |
244 | export const App = () => {
245 | const [current, send] = useMachine(modalMachine, { devTools: true })
246 |
247 | return (
248 |
249 | Open Modal
250 |
251 |
252 | )
253 | }
254 | ```
255 |
256 | ```jsx 11 title="1️⃣ App with State Machine 🛠" subtitle="send 'OPEN' event on Button click"
257 | import React from 'react'
258 | import { Button, Modal } from '../shared/Components'
259 | import { useMachine } from '../shared/useMachine'
260 | import { modalMachine } from './machine'
261 |
262 | export const App = () => {
263 | const [current, send] = useMachine(modalMachine, { devTools: true })
264 |
265 | return (
266 |
267 | send('OPEN')}>Open Modal
268 |
269 |
270 | )
271 | }
272 | ```
273 |
274 | ```jsx 13 title="1️⃣ App with State Machine 🛠" subtitle="send 'CLOSE' event on Modal close"
275 | import React from 'react'
276 | import { Button, Modal } from '../shared/Components'
277 | import { useMachine } from '../shared/useMachine'
278 | import { modalMachine } from './machine'
279 |
280 | export const App = () => {
281 | const [current, send] = useMachine(modalMachine, { devTools: true })
282 |
283 | return (
284 |
285 | send('OPEN')}>Open Modal
286 | send('CLOSE')}
288 | stateValue={}
289 | />
290 |
291 | )
292 | }
293 | ```
294 |
295 | ```jsx 14 title="1️⃣ App with State Machine 🛠" subtitle="Pass down current state value as the visibility props"
296 | import React from 'react'
297 | import { Button, Modal } from '../shared/Components'
298 | import { useMachine } from '../shared/useMachine'
299 | import { modalMachine } from './machine'
300 |
301 | export const App = () => {
302 | const [current, send] = useMachine(modalMachine, { devTools: true })
303 |
304 | return (
305 |
306 | send('OPEN')}>Open Modal
307 | send('CLOSE')}
309 | stateValue={current.value === 'visible'}
310 | />
311 |
312 | )
313 | }
314 | ```
315 |
316 | ```jsx 7,11,13,14 title="1️⃣ App with State Machine 🛠"
317 | import React from 'react'
318 | import { Button, Modal } from '../shared/Components'
319 | import { useMachine } from '../shared/useMachine'
320 | import { modalMachine } from './machine'
321 |
322 | export const App = () => {
323 | const [current, send] = useMachine(modalMachine, { devTools: true })
324 |
325 | return (
326 |
327 | send('OPEN')}>Open Modal
328 | send('CLOSE')}
330 | stateValue={current.value === 'visible'}
331 | />
332 |
333 | )
334 | }
335 | ```
336 |
337 |
338 |
339 | ---
340 |
341 |
342 |
--------------------------------------------------------------------------------
/demo-a/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 |
4 | export const App = () => {
5 | const [visible, setVisible] = useState(false)
6 |
7 | return (
8 |
9 | setVisible(true)}>Open Modal
10 | setVisible(false)} stateValue={visible} />
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/demo-a/AppWithMachine.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 | import { useMachine } from '../shared/useMachine'
4 | import { modalMachine } from './machine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | return (
10 |
11 | send('OPEN')}>Open Modal
12 | send('CLOSE')}
14 | stateValue={current.value === 'visible'}
15 | />
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/demo-a/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | id: 'visibility',
5 | initial: 'invisible',
6 | states: {
7 | invisible: {
8 | on: {
9 | OPEN: 'visible'
10 | }
11 | },
12 | visible: {
13 | on: {
14 | CLOSE: 'invisible'
15 | }
16 | }
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/demo-b.mdx:
--------------------------------------------------------------------------------
1 | import { Image } from '@mdx-deck/components'
2 | import { App } from './demo-b/App.js'
3 | import { App as AppWithMachine } from './demo-b/AppWithMachine.js'
4 | import { CodeSurferLayout } from 'code-surfer'
5 |
6 | # Requirement 2️⃣
7 |
8 | ## Differentiate View & Edit Screens 📝
9 |
10 | ---
11 |
12 |
13 |
14 | ---
15 |
16 |
17 |
18 | ```jsx 5,9:10 title="2️⃣ Differentiate View & Edit Screens 📝"
19 | import React, { useState } from 'react'
20 | import { Button, Modal } from '../shared/Components'
21 |
22 | export const App = () => {
23 | const [visible, setVisible] = useState(false)
24 |
25 | return (
26 |
27 | setVisible(true)}>Open Modal
28 | setVisible(false)} stateValue={visible} />
29 |
30 | )
31 | }
32 | ```
33 |
34 | ```jsx 6 title="2️⃣ Differentiate View & Edit Screens 📝" subtitle="Add 'editing' useState"
35 | import React, { useState } from 'react'
36 | import { Button, Modal } from '../shared/Components'
37 |
38 | export const App = () => {
39 | const [visible, setVisible] = useState(false)
40 | const [editing, setEditing] = useState(false)
41 |
42 | return (
43 |
44 | setVisible(true)}>Open Modal
45 | setVisible(false)} stateValue={visible} />
46 |
47 | )
48 | }
49 | ```
50 |
51 | ```jsx 8:13,24 title="2️⃣ Differentiate View & Edit Screens 📝" subtitle="Derive stateValue based on 'visible' & 'editing' state"
52 | import React, { useState } from 'react'
53 | import { Button, Modal } from '../shared/Components'
54 |
55 | export const App = () => {
56 | const [visible, setVisible] = useState(false)
57 | const [editing, setEditing] = useState(false)
58 |
59 | let stateValue
60 | if (visible && !editing) {
61 | stateValue = 'view'
62 | } else if (visible && editing) {
63 | stateValue = 'edit'
64 | }
65 |
66 | return (
67 |
68 | setVisible(true)}>Open Modal
69 | {
71 | setVisible(false)
72 | }}
73 | onEdit={}
74 | onSubmit={}
75 | stateValue={stateValue}
76 | />
77 |
78 | )
79 | }
80 | ```
81 |
82 | ```jsx 21,23,24:27 title="2️⃣ Differentiate View & Edit Screens 📝" subtitle="Propagate events to the corresponding state changes"
83 | import React, { useState } from 'react'
84 | import { Button, Modal } from '../shared/Components'
85 |
86 | export const App = () => {
87 | const [visible, setVisible] = useState(false)
88 | const [editing, setEditing] = useState(false)
89 |
90 | let stateValue
91 | if (visible && !editing) {
92 | stateValue = 'view'
93 | } else if (visible && editing) {
94 | stateValue = 'edit'
95 | }
96 |
97 | return (
98 |
99 | setVisible(true)}>Open Modal
100 | {
102 | setVisible(false)
103 | setEditing(false)
104 | }}
105 | onEdit={() => setEditing(true)}
106 | onSubmit={() => {
107 | setVisible(false)
108 | setEditing(false)
109 | }}
110 | stateValue={stateValue}
111 | />
112 |
113 | )
114 | }
115 | ```
116 |
117 | ```jsx 6,8:13,21,23,24:27,28 title="2️⃣ Differentiate View & Edit Screens 📝"
118 | import React, { useState } from 'react'
119 | import { Button, Modal } from '../shared/Components'
120 |
121 | export const App = () => {
122 | const [visible, setVisible] = useState(false)
123 | const [editing, setEditing] = useState(false)
124 |
125 | let stateValue
126 | if (visible && !editing) {
127 | stateValue = 'view'
128 | } else if (visible && editing) {
129 | stateValue = 'edit'
130 | }
131 |
132 | return (
133 |
134 | setVisible(true)}>Open Modal
135 | {
137 | setVisible(false)
138 | setEditing(false)
139 | }}
140 | onEdit={() => setEditing(true)}
141 | onSubmit={() => {
142 | setVisible(false)
143 | setEditing(false)
144 | }}
145 | stateValue={stateValue}
146 | />
147 |
148 | )
149 | }
150 | ```
151 |
152 |
153 |
154 | ---
155 |
156 | # Still manageable 👌🏼
157 |
158 | ---
159 |
160 | ## Differentiate View & Edit Screens 📝
161 |
162 | ### with State Machine
163 |
164 | Introducing **Hierarchical State**
165 |
166 | ---
167 |
168 |
169 |
170 | ```jsx 7:11,12:17 title="2️⃣ State Machine Definition ⚙️" subtitle="Introducing Hierarchical State"
171 | import { Machine } from 'xstate'
172 |
173 | export const modalMachine = Machine({
174 | id: 'editView',
175 | initial: 'invisible',
176 | states: {
177 | invisible: {
178 | on: {
179 | OPEN: 'visible',
180 | },
181 | },
182 | visible: {
183 | on: {
184 | CLOSE: 'invisible',
185 | },
186 | // TODO: Add Hierarchical State
187 | },
188 | },
189 | })
190 | ```
191 |
192 | ```jsx 16:28 title="2️⃣ State Machine Definition ⚙️" subtitle="Add states inside 'visible' state"
193 | import { Machine } from 'xstate'
194 |
195 | export const modalMachine = Machine({
196 | id: 'editView',
197 | initial: 'invisible',
198 | states: {
199 | invisible: {
200 | on: {
201 | OPEN: 'visible',
202 | },
203 | },
204 | visible: {
205 | on: {
206 | CLOSE: 'invisible',
207 | },
208 | initial: 'view',
209 | states: {
210 | view: {
211 | on: {
212 | EDIT: 'edit',
213 | },
214 | },
215 | edit: {
216 | on: {
217 | SUBMIT: /* How to access parent state? */,
218 | },
219 | },
220 | },
221 | },
222 | },
223 | })
224 | ```
225 |
226 | ```jsx 8,26 title="2️⃣ State Machine Definition ⚙️" subtitle="Target parent state by using #id"
227 | import { Machine } from 'xstate'
228 |
229 | export const modalMachine = Machine({
230 | id: 'editView',
231 | initial: 'invisible',
232 | states: {
233 | invisible: {
234 | id: 'invisible',
235 | on: {
236 | OPEN: 'visible',
237 | },
238 | },
239 | visible: {
240 | on: {
241 | CLOSE: 'invisible',
242 | },
243 | initial: 'view',
244 | states: {
245 | view: {
246 | on: {
247 | EDIT: 'edit',
248 | },
249 | },
250 | edit: {
251 | on: {
252 | SUBMIT: '#invisible',
253 | },
254 | },
255 | },
256 | },
257 | },
258 | })
259 | ```
260 |
261 | ```jsx 8,17:29 title="2️⃣ State Machine Definition ⚙️"
262 | import { Machine } from 'xstate'
263 |
264 | export const modalMachine = Machine({
265 | id: 'editView',
266 | initial: 'invisible',
267 | states: {
268 | invisible: {
269 | id: 'invisible',
270 | on: {
271 | OPEN: 'visible',
272 | },
273 | },
274 | visible: {
275 | on: {
276 | CLOSE: 'invisible',
277 | },
278 | initial: 'view',
279 | states: {
280 | view: {
281 | on: {
282 | EDIT: 'edit',
283 | },
284 | },
285 | edit: {
286 | on: {
287 | SUBMIT: '#invisible',
288 | },
289 | },
290 | },
291 | },
292 | },
293 | })
294 | ```
295 |
296 |
297 |
298 | ---
299 |
300 | # Visualization
301 |
302 |
310 |
311 | ---
312 |
313 |
314 |
315 | ```jsx title="2️⃣ App with State Machine 🛠"
316 | import React from 'react'
317 | import { Button, Modal } from '../shared/Components'
318 | import { useMachine } from '../shared/useMachine'
319 | import { modalMachine } from './machine'
320 |
321 | export const App = () => {
322 | const [current, send] = useMachine(modalMachine, { devTools: true })
323 |
324 | return (
325 |
326 | send('OPEN')}>Open Modal
327 | send('CLOSE')}
329 | onEdit={}
330 | onSubmit={}
331 | stateValue={current.value === 'visible'}
332 | />
333 |
334 | )
335 | }
336 | ```
337 |
338 | ```jsx 16 title="2️⃣ App with State Machine 🛠" subtitle="We need to change the value that we're passing"
339 | import React from 'react'
340 | import { Button, Modal } from '../shared/Components'
341 | import { useMachine } from '../shared/useMachine'
342 | import { modalMachine } from './machine'
343 |
344 | export const App = () => {
345 | const [current, send] = useMachine(modalMachine, { devTools: true })
346 |
347 | return (
348 |
349 | send('OPEN')}>Open Modal
350 | send('CLOSE')}
352 | onEdit={}
353 | onSubmit={}
354 | stateValue={current.value === 'visible'}
355 | />
356 |
357 | )
358 | }
359 | ```
360 |
361 | ```jsx 16 title="2️⃣ App with State Machine 🛠" subtitle="Pass down 'visible' substates as the stateValue"
362 | import React from 'react'
363 | import { Button, Modal } from '../shared/Components'
364 | import { useMachine } from '../shared/useMachine'
365 | import { modalMachine } from './machine'
366 |
367 | export const App = () => {
368 | const [current, send] = useMachine(modalMachine, { devTools: true })
369 |
370 | return (
371 |
372 | send('OPEN')}>Open Modal
373 | send('CLOSE')}
375 | onEdit={}
376 | onSubmit={}
377 | stateValue={current.value.visible}
378 | />
379 |
380 | )
381 | }
382 | ```
383 |
384 | ```jsx 14:15 title="2️⃣ App with State Machine 🛠" subtitle="Simply send 'EDIT' & 'SUBMIT' events"
385 | import React from 'react'
386 | import { Button, Modal } from '../shared/Components'
387 | import { useMachine } from '../shared/useMachine'
388 | import { modalMachine } from './machine'
389 |
390 | export const App = () => {
391 | const [current, send] = useMachine(modalMachine, { devTools: true })
392 |
393 | return (
394 |
395 | send('OPEN')}>Open Modal
396 | send('CLOSE')}
398 | onEdit={() => send('EDIT')}
399 | onSubmit={() => send('SUBMIT')}
400 | stateValue={current.value.visible}
401 | />
402 |
403 | )
404 | }
405 | ```
406 |
407 | ```jsx 14:15,16 title="2️⃣ App with State Machine 🛠"
408 | import React from 'react'
409 | import { Button, Modal } from '../shared/Components'
410 | import { useMachine } from '../shared/useMachine'
411 | import { modalMachine } from './machine'
412 |
413 | export const App = () => {
414 | const [current, send] = useMachine(modalMachine, { devTools: true })
415 |
416 | return (
417 |
418 | send('OPEN')}>Open Modal
419 | send('CLOSE')}
421 | onEdit={() => send('EDIT')}
422 | onSubmit={() => send('SUBMIT')}
423 | stateValue={current.value.visible}
424 | />
425 |
426 | )
427 | }
428 | ```
429 |
430 |
431 |
432 | ---
433 |
434 |
435 |
--------------------------------------------------------------------------------
/demo-b/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 |
4 | export const App = () => {
5 | const [visible, setVisible] = useState(false)
6 | const [editing, setEditing] = useState(false)
7 |
8 | let stateValue
9 | if (visible && !editing) {
10 | stateValue = 'view'
11 | } else if (visible && editing) {
12 | stateValue = 'edit'
13 | }
14 |
15 | return (
16 |
17 | setVisible(true)}>Open Modal
18 | {
20 | setVisible(false)
21 | setEditing(false)
22 | }}
23 | onEdit={() => setEditing(true)}
24 | onSubmit={() => {
25 | setVisible(false)
26 | setEditing(false)
27 | }}
28 | stateValue={stateValue}
29 | />
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/demo-b/AppWithMachine.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 | import { useMachine } from '../shared/useMachine'
4 | import { modalMachine } from './machine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | return (
10 |
11 | send('OPEN')}>Open Modal
12 | send('CLOSE')}
14 | onEdit={() => send('EDIT')}
15 | onSubmit={() => send('SUBMIT')}
16 | stateValue={current.value.visible}
17 | />
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/demo-b/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | id: 'editView',
5 | initial: 'invisible',
6 | states: {
7 | invisible: {
8 | id: 'invisible',
9 | on: {
10 | OPEN: 'visible'
11 | }
12 | },
13 | visible: {
14 | on: {
15 | CLOSE: 'invisible'
16 | },
17 | initial: 'view',
18 | states: {
19 | view: {
20 | on: {
21 | EDIT: 'edit'
22 | }
23 | },
24 | edit: {
25 | on: {
26 | SUBMIT: '#invisible'
27 | }
28 | }
29 | }
30 | }
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/demo-c.mdx:
--------------------------------------------------------------------------------
1 | import { Image } from '@mdx-deck/components'
2 | import { App } from './demo-c/App.js'
3 | import { App as AppWithMachine } from './demo-c/AppWithMachine.js'
4 | import { CodeSurferLayout } from 'code-surfer'
5 |
6 | # Requirement 3️⃣
7 |
8 | ## Add Create Screen 🆕
9 |
10 | Show `create` screen when opening modal with _empty_ data
11 |
12 | ---
13 |
14 |
15 |
16 | ---
17 |
18 |
19 |
20 | ```jsx 6:31 title="3️⃣ Add Create Screen 🆕"
21 | import React, { useState } from 'react'
22 | import { Button, Modal } from '../shared/Components'
23 |
24 | export const App = () => {
25 | const [visible, setVisible] = useState(false)
26 | const [editing, setEditing] = useState(false)
27 |
28 | let stateValue
29 | if (visible && !editing) {
30 | stateValue = 'view'
31 | } else if (visible && editing) {
32 | stateValue = 'edit'
33 | }
34 |
35 | return (
36 |
37 | setVisible(true)}>Open Modal
38 | {
40 | setVisible(false)
41 | setEditing(false)
42 | }}
43 | onEdit={() => setEditing(true)}
44 | onSubmit={() => {
45 | setVisible(false)
46 | setEditing(false)
47 | }}
48 | stateValue={stateValue}
49 | />
50 |
51 | )
52 | }
53 | ```
54 |
55 | ```jsx 7 title="3️⃣ Add Create Screen 🆕" subtitle="Add 'data' useState"
56 | import React, { useState } from 'react'
57 | import { Button, Modal } from '../shared/Components'
58 |
59 | export const App = () => {
60 | const [visible, setVisible] = useState(false)
61 | const [editing, setEditing] = useState(false)
62 | const [data, setData] = useState([])
63 |
64 | let stateValue
65 | if (visible && !editing) {
66 | stateValue = 'view'
67 | } else if (visible && editing) {
68 | stateValue = 'edit'
69 | }
70 |
71 | return (
72 |
73 | setVisible(true)}>Open Modal
74 | {
76 | setVisible(false)
77 | setEditing(false)
78 | }}
79 | onEdit={() => setEditing(true)}
80 | onSubmit={() => {
81 | setVisible(false)
82 | setEditing(false)
83 | }}
84 | stateValue={stateValue}
85 | />
86 |
87 | )
88 | }
89 | ```
90 |
91 | ```jsx 10,14:16 title="3️⃣ Add Create Screen 🆕" subtitle="Incorporate 'data' state into the stateValue determination"
92 | import React, { useState } from 'react'
93 | import { Button, Modal } from '../shared/Components'
94 |
95 | export const App = () => {
96 | const [visible, setVisible] = useState(false)
97 | const [editing, setEditing] = useState(false)
98 | const [data, setData] = useState([])
99 |
100 | let stateValue
101 | if (visible && !editing && data && data.length > 0) {
102 | stateValue = 'view'
103 | } else if (visible && editing) {
104 | stateValue = 'edit'
105 | } else if (visible && (!data || data.length === 0)) {
106 | stateValue = 'create'
107 | }
108 |
109 | return (
110 |
111 | setVisible(true)}>Open Modal
112 | {
114 | setVisible(false)
115 | setEditing(false)
116 | }}
117 | onEdit={() => setEditing(true)}
118 | onSubmit={() => {
119 | setVisible(false)
120 | setEditing(false)
121 | }}
122 | stateValue={stateValue}
123 | />
124 |
125 | )
126 | }
127 | ```
128 |
129 | ```jsx 20:28 title="3️⃣ Add Create Screen 🆕" subtitle="Update Buttons"
130 | import React, { useState } from 'react'
131 | import { Button, Modal } from '../shared/Components'
132 |
133 | export const App = () => {
134 | const [visible, setVisible] = useState(false)
135 | const [editing, setEditing] = useState(false)
136 | const [data, setData] = useState([])
137 |
138 | let stateValue
139 | if (visible && !editing && data && data.length > 0) {
140 | stateValue = 'view'
141 | } else if (visible && editing) {
142 | stateValue = 'edit'
143 | } else if (visible && (!data || data.length === 0)) {
144 | stateValue = 'create'
145 | }
146 |
147 | return (
148 |
149 | {
151 | setVisible(true)
152 | setData([''])
153 | }}
154 | >
155 | Open Exists
156 |
157 | setVisible(true)}>Open Empty
158 | {
160 | setVisible(false)
161 | setEditing(false)
162 | }}
163 | onEdit={() => setEditing(true)}
164 | onSubmit={() => {
165 | setVisible(false)
166 | setEditing(false)
167 | }}
168 | stateValue={stateValue}
169 | />
170 |
171 | )
172 | }
173 | ```
174 |
175 | ```jsx 33,39 title="3️⃣ Add Create Screen 🆕" subtitle="Update Event Propagations"
176 | import React, { useState } from 'react'
177 | import { Button, Modal } from '../shared/Components'
178 |
179 | export const App = () => {
180 | const [visible, setVisible] = useState(false)
181 | const [editing, setEditing] = useState(false)
182 | const [data, setData] = useState([])
183 |
184 | let stateValue
185 | if (visible && !editing && data && data.length > 0) {
186 | stateValue = 'view'
187 | } else if (visible && editing) {
188 | stateValue = 'edit'
189 | } else if (visible && (!data || data.length === 0)) {
190 | stateValue = 'create'
191 | }
192 |
193 | return (
194 |
195 | {
197 | setVisible(true)
198 | setData([''])
199 | }}
200 | >
201 | Open Exists
202 |
203 | setVisible(true)}>Open Empty
204 | {
206 | setVisible(false)
207 | setEditing(false)
208 | setData([])
209 | }}
210 | onEdit={() => setEditing(true)}
211 | onSubmit={() => {
212 | setVisible(false)
213 | setEditing(false)
214 | setData([])
215 | }}
216 | stateValue={stateValue}
217 | />
218 |
219 | )
220 | }
221 | ```
222 |
223 | ```jsx 7,10,14:16,20:29,33,39 title="3️⃣ Add Create Screen 🆕"
224 | import React, { useState } from 'react'
225 | import { Button, Modal } from '../shared/Components'
226 |
227 | export const App = () => {
228 | const [visible, setVisible] = useState(false)
229 | const [editing, setEditing] = useState(false)
230 | const [data, setData] = useState([])
231 |
232 | let stateValue
233 | if (visible && !editing && data && data.length > 0) {
234 | stateValue = 'view'
235 | } else if (visible && editing) {
236 | stateValue = 'edit'
237 | } else if (visible && (!data || data.length === 0)) {
238 | stateValue = 'create'
239 | }
240 |
241 | return (
242 |
243 | {
245 | setVisible(true)
246 | setData([''])
247 | }}
248 | >
249 | Open Exists
250 |
251 | setVisible(true)}>Open Empty
252 | {
254 | setVisible(false)
255 | setEditing(false)
256 | setData([])
257 | }}
258 | onEdit={() => setEditing(true)}
259 | onSubmit={() => {
260 | setVisible(false)
261 | setEditing(false)
262 | setData([])
263 | }}
264 | stateValue={stateValue}
265 | />
266 |
267 | )
268 | }
269 | ```
270 |
271 |
272 |
273 | ---
274 |
275 | # Oh no 😫
276 |
277 | ---
278 |
279 | ## Add Create Screen 🆕
280 |
281 | ### with State Machine
282 |
283 | Introducing **Guards**
284 |
285 | ---
286 |
287 |
288 |
289 | ```jsx 5:31 title="3️⃣ State Machine Definition ⚙️"
290 | import { Machine } from 'xstate'
291 |
292 | export const modalMachine = Machine({
293 | id: 'createView',
294 | initial: 'invisible',
295 | states: {
296 | invisible: {
297 | id: 'invisible',
298 | on: {
299 | OPEN: 'visible',
300 | },
301 | },
302 | visible: {
303 | on: {
304 | CLOSE: 'invisible',
305 | },
306 | initial: 'view',
307 | states: {
308 | view: {
309 | on: {
310 | EDIT: 'edit',
311 | },
312 | },
313 | edit: {
314 | on: {
315 | SUBMIT: '#invisible',
316 | },
317 | },
318 | },
319 | },
320 | },
321 | })
322 | ```
323 |
324 | ```jsx 10 title="3️⃣ State Machine Definition ⚙️" subtitle="This is a transition"
325 | import { Machine } from 'xstate'
326 |
327 | export const modalMachine = Machine({
328 | id: 'createView',
329 | initial: 'invisible',
330 | states: {
331 | invisible: {
332 | id: 'invisible',
333 | on: {
334 | OPEN: 'visible',
335 | },
336 | },
337 | visible: {
338 | on: {
339 | CLOSE: 'invisible',
340 | },
341 | initial: 'view',
342 | states: {
343 | view: {
344 | on: {
345 | EDIT: 'edit',
346 | },
347 | },
348 | edit: {
349 | on: {
350 | SUBMIT: '#invisible',
351 | },
352 | },
353 | },
354 | },
355 | },
356 | })
357 | ```
358 |
359 | ```jsx 10 title="3️⃣ State Machine Definition ⚙️" subtitle="Transition can be defined as an object"
360 | import { Machine } from 'xstate'
361 |
362 | export const modalMachine = Machine({
363 | id: 'createView',
364 | initial: 'invisible',
365 | states: {
366 | invisible: {
367 | id: 'invisible',
368 | on: {
369 | OPEN: { target: 'visible' },
370 | },
371 | },
372 | visible: {
373 | on: {
374 | CLOSE: 'invisible',
375 | },
376 | initial: 'view',
377 | states: {
378 | view: {
379 | on: {
380 | EDIT: 'edit',
381 | },
382 | },
383 | edit: {
384 | on: {
385 | SUBMIT: '#invisible',
386 | },
387 | },
388 | },
389 | },
390 | },
391 | })
392 | ```
393 |
394 | ```jsx 10:13 title="3️⃣ State Machine Definition ⚙️" subtitle="as well as an array"
395 | import { Machine } from 'xstate'
396 |
397 | export const modalMachine = Machine({
398 | id: 'createView',
399 | initial: 'invisible',
400 | states: {
401 | invisible: {
402 | id: 'invisible',
403 | on: {
404 | OPEN: [
405 | // ⚠️ an array here
406 | { target: 'visible' },
407 | ],
408 | },
409 | },
410 | visible: {
411 | on: {
412 | CLOSE: 'invisible',
413 | },
414 | initial: 'view',
415 | states: {
416 | view: {
417 | on: {
418 | EDIT: 'edit',
419 | },
420 | },
421 | edit: {
422 | on: {
423 | SUBMIT: '#invisible',
424 | },
425 | },
426 | },
427 | },
428 | },
429 | })
430 | ```
431 |
432 | ```jsx 11:13 title="3️⃣ State Machine Definition ⚙️" subtitle="How do we target different states from a same event?"
433 | import { Machine } from 'xstate'
434 |
435 | export const modalMachine = Machine({
436 | id: 'createView',
437 | initial: 'invisible',
438 | states: {
439 | invisible: {
440 | id: 'invisible',
441 | on: {
442 | OPEN: [
443 | {
444 | target: 'visible.create',
445 | },
446 | { target: 'visible' },
447 | ],
448 | },
449 | },
450 | visible: {
451 | on: {
452 | CLOSE: 'invisible',
453 | },
454 | initial: 'view',
455 | states: {
456 | view: {
457 | on: {
458 | EDIT: 'edit',
459 | },
460 | },
461 | edit: {
462 | on: {
463 | SUBMIT: '#invisible',
464 | },
465 | },
466 | },
467 | },
468 | },
469 | })
470 | ```
471 |
472 | ```jsx 12:13 title="3️⃣ State Machine Definition ⚙️" subtitle="By providing a condition, it's called a Guard"
473 | import { Machine } from 'xstate'
474 |
475 | export const modalMachine = Machine({
476 | id: 'createView',
477 | initial: 'invisible',
478 | states: {
479 | invisible: {
480 | id: 'invisible',
481 | on: {
482 | OPEN: [
483 | {
484 | cond: (context, event) =>
485 | !event.data || (event.data && event.data.length === 0),
486 | target: 'visible.create',
487 | },
488 | { target: 'visible' },
489 | ],
490 | },
491 | },
492 | visible: {
493 | on: {
494 | CLOSE: 'invisible',
495 | },
496 | initial: 'view',
497 | states: {
498 | view: {
499 | on: {
500 | EDIT: 'edit',
501 | },
502 | },
503 | edit: {
504 | on: {
505 | SUBMIT: '#invisible',
506 | },
507 | },
508 | },
509 | },
510 | },
511 | })
512 | ```
513 |
514 | ```jsx 36:40 title="3️⃣ State Machine Definition ⚙️" subtitle="Define another 'create' state"
515 | import { Machine } from 'xstate'
516 |
517 | export const modalMachine = Machine({
518 | id: 'createView',
519 | initial: 'invisible',
520 | states: {
521 | invisible: {
522 | id: 'invisible',
523 | on: {
524 | OPEN: [
525 | {
526 | cond: (context, event) =>
527 | !event.data || (event.data && event.data.length === 0),
528 | target: 'visible.create',
529 | },
530 | { target: 'visible' },
531 | ],
532 | },
533 | },
534 | visible: {
535 | on: {
536 | CLOSE: 'invisible',
537 | },
538 | initial: 'view',
539 | states: {
540 | view: {
541 | on: {
542 | EDIT: 'edit',
543 | },
544 | },
545 | edit: {
546 | on: {
547 | SUBMIT: '#invisible',
548 | },
549 | },
550 | create: {
551 | on: {
552 | SUBMIT: '#invisible',
553 | },
554 | },
555 | },
556 | },
557 | },
558 | })
559 | ```
560 |
561 | ```jsx 11:16,36:40 title="3️⃣ State Machine Definition ⚙️"
562 | import { Machine } from 'xstate'
563 |
564 | export const modalMachine = Machine({
565 | id: 'createView',
566 | initial: 'invisible',
567 | states: {
568 | invisible: {
569 | id: 'invisible',
570 | on: {
571 | OPEN: [
572 | {
573 | cond: (context, event) =>
574 | !event.data || (event.data && event.data.length === 0),
575 | target: 'visible.create',
576 | },
577 | { target: 'visible' },
578 | ],
579 | },
580 | },
581 | visible: {
582 | on: {
583 | CLOSE: 'invisible',
584 | },
585 | initial: 'view',
586 | states: {
587 | view: {
588 | on: {
589 | EDIT: 'edit',
590 | },
591 | },
592 | edit: {
593 | on: {
594 | SUBMIT: '#invisible',
595 | },
596 | },
597 | create: {
598 | on: {
599 | SUBMIT: '#invisible',
600 | },
601 | },
602 | },
603 | },
604 | },
605 | })
606 | ```
607 |
608 |
609 |
610 | ---
611 |
612 | # Visualization
613 |
614 |
622 |
623 | ---
624 |
625 |
626 |
627 | ```jsx title="3️⃣ App with State Machine 🛠"
628 | import React from 'react'
629 | import { Button, Modal } from '../shared/Components'
630 | import { useMachine } from '../shared/useMachine'
631 | import { modalMachine } from './machine'
632 |
633 | export const App = () => {
634 | const [current, send] = useMachine(modalMachine, { devTools: true })
635 |
636 | return (
637 |
638 | send('OPEN')}>Open Modal
639 | send('CLOSE')}
641 | onEdit={() => send('EDIT')}
642 | onSubmit={() => send('SUBMIT')}
643 | stateValue={current.value.visible}
644 | />
645 |
646 | )
647 | }
648 | ```
649 |
650 | ```jsx 11 title="3️⃣ App with State Machine 🛠" subtitle="Let's rename the current button"
651 | import React from 'react'
652 | import { Button, Modal } from '../shared/Components'
653 | import { useMachine } from '../shared/useMachine'
654 | import { modalMachine } from './machine'
655 |
656 | export const App = () => {
657 | const [current, send] = useMachine(modalMachine, { devTools: true })
658 |
659 | return (
660 |
661 | send('OPEN')}>Open Modal
662 | send('CLOSE')}
664 | onEdit={() => send('EDIT')}
665 | onSubmit={() => send('SUBMIT')}
666 | stateValue={current.value.visible}
667 | />
668 |
669 | )
670 | }
671 | ```
672 |
673 | ```jsx 11 title="3️⃣ App with State Machine 🛠" subtitle="Name it as an Empty Button"
674 | import React from 'react'
675 | import { Button, Modal } from '../shared/Components'
676 | import { useMachine } from '../shared/useMachine'
677 | import { modalMachine } from './machine'
678 |
679 | export const App = () => {
680 | const [current, send] = useMachine(modalMachine, { devTools: true })
681 |
682 | return (
683 |
684 | send('OPEN')}>Open Empty
685 | send('CLOSE')}
687 | onEdit={() => send('EDIT')}
688 | onSubmit={() => send('SUBMIT')}
689 | stateValue={current.value.visible}
690 | />
691 |
692 | )
693 | }
694 | ```
695 |
696 | ```jsx 11:13 title="3️⃣ App with State Machine 🛠" subtitle="Event can be defined as an object with a type and payloads"
697 | import React from 'react'
698 | import { Button, Modal } from '../shared/Components'
699 | import { useMachine } from '../shared/useMachine'
700 | import { modalMachine } from './machine'
701 |
702 | export const App = () => {
703 | const [current, send] = useMachine(modalMachine, { devTools: true })
704 |
705 | return (
706 |
707 | send({ type: 'OPEN', data: [] })}>
708 | Open Empty
709 |
710 | send('CLOSE')}
712 | onEdit={() => send('EDIT')}
713 | onSubmit={() => send('SUBMIT')}
714 | stateValue={current.value.visible}
715 | />
716 |
717 | )
718 | }
719 | ```
720 |
721 | ```jsx 11:13 title="3️⃣ App with State Machine 🛠" subtitle="Add another button which sends the same event but different payloads"
722 | import React from 'react'
723 | import { Button, Modal } from '../shared/Components'
724 | import { useMachine } from '../shared/useMachine'
725 | import { modalMachine } from './machine'
726 |
727 | export const App = () => {
728 | const [current, send] = useMachine(modalMachine, { devTools: true })
729 |
730 | return (
731 |
732 | send({ type: 'OPEN', data: [''] })}>
733 | Open Exists
734 |
735 | send({ type: 'OPEN', data: [] })}>
736 | Open Empty
737 |
738 | send('CLOSE')}
740 | onEdit={() => send('EDIT')}
741 | onSubmit={() => send('SUBMIT')}
742 | stateValue={current.value.visible}
743 | />
744 |
745 | )
746 | }
747 | ```
748 |
749 | ```jsx 11:16 title="3️⃣ App with State Machine 🛠" subtitle="That's it!"
750 | import React from 'react'
751 | import { Button, Modal } from '../shared/Components'
752 | import { useMachine } from '../shared/useMachine'
753 | import { modalMachine } from './machine'
754 |
755 | export const App = () => {
756 | const [current, send] = useMachine(modalMachine, { devTools: true })
757 |
758 | return (
759 |
760 | send({ type: 'OPEN', data: [''] })}>
761 | Open Exists
762 |
763 | send({ type: 'OPEN', data: [] })}>
764 | Open Empty
765 |
766 | send('CLOSE')}
768 | onEdit={() => send('EDIT')}
769 | onSubmit={() => send('SUBMIT')}
770 | stateValue={current.value.visible}
771 | />
772 |
773 | )
774 | }
775 | ```
776 |
777 |
778 |
779 | ---
780 |
781 |
782 |
--------------------------------------------------------------------------------
/demo-c/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 |
4 | export const App = () => {
5 | const [visible, setVisible] = useState(false)
6 | const [editing, setEditing] = useState(false)
7 | const [data, setData] = useState([])
8 |
9 | let stateValue
10 | if (visible && !editing && data && data.length > 0) {
11 | stateValue = 'view'
12 | } else if (visible && editing) {
13 | stateValue = 'edit'
14 | } else if (visible && (!data || data.length === 0)) {
15 | stateValue = 'create'
16 | }
17 |
18 | return (
19 |
20 | {
22 | setVisible(true)
23 | setData([''])
24 | }}
25 | >
26 | Open Exists
27 |
28 |
29 | setVisible(true)}>Open Empty
30 | {
32 | setVisible(false)
33 | setEditing(false)
34 | setData([])
35 | }}
36 | onEdit={() => setEditing(true)}
37 | onSubmit={() => {
38 | setVisible(false)
39 | setEditing(false)
40 | setData([])
41 | }}
42 | stateValue={stateValue}
43 | />
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/demo-c/AppWithMachine.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 | import { useMachine } from '../shared/useMachine'
4 | import { modalMachine } from './machine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | return (
10 |
11 | send({ type: 'OPEN', data: [''] })}>
12 | Open Exists
13 |
14 |
15 | send({ type: 'OPEN', data: [] })}>
16 | Open Empty
17 |
18 | send('CLOSE')}
20 | onEdit={() => send('EDIT')}
21 | onSubmit={() => send('SUBMIT')}
22 | stateValue={current.value.visible}
23 | />
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/demo-c/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine({
4 | id: 'createView',
5 | initial: 'invisible',
6 | states: {
7 | invisible: {
8 | id: 'invisible',
9 | on: {
10 | OPEN: [
11 | {
12 | cond: (context, event) =>
13 | !event.data || (event.data && event.data.length === 0),
14 | target: 'visible.create'
15 | },
16 | { target: 'visible' }
17 | ]
18 | }
19 | },
20 | visible: {
21 | on: {
22 | CLOSE: 'invisible'
23 | },
24 | initial: 'view',
25 | states: {
26 | view: {
27 | on: {
28 | EDIT: 'edit'
29 | }
30 | },
31 | edit: {
32 | on: {
33 | SUBMIT: '#invisible'
34 | }
35 | },
36 | create: {
37 | on: {
38 | SUBMIT: '#invisible'
39 | }
40 | }
41 | }
42 | }
43 | }
44 | })
45 |
--------------------------------------------------------------------------------
/demo-d.mdx:
--------------------------------------------------------------------------------
1 | import { Image } from '@mdx-deck/components'
2 | import { App } from './demo-d/App.js'
3 | import { App as AppWithMachine } from './demo-d/AppWithMachine.js'
4 | import { CodeSurferLayout } from 'code-surfer'
5 |
6 | # Requirement 4️⃣
7 |
8 | ## Add List Screen 🗒
9 |
10 | Show `list` screen when opening modal with _multiple_ data
11 |
12 | ---
13 |
14 |
15 |
16 | ---
17 |
18 |
19 |
20 | ```jsx 7:44 title="4️⃣ Add List Screen 🗒"
21 | import React, { useState } from 'react'
22 | import { Button, Modal } from '../shared/Components'
23 |
24 | export const App = () => {
25 | const [visible, setVisible] = useState(false)
26 | const [editing, setEditing] = useState(false)
27 | const [data, setData] = useState([])
28 |
29 | let stateValue
30 | if (visible && !editing && data && data.length > 0) {
31 | stateValue = 'view'
32 | } else if (visible && editing) {
33 | stateValue = 'edit'
34 | } else if (visible && (!data || data.length === 0)) {
35 | stateValue = 'create'
36 | }
37 |
38 | return (
39 |
40 | {
42 | setVisible(true)
43 | setData([''])
44 | }}
45 | >
46 | Open Exists
47 |
48 | setVisible(true)}>Open Empty
49 | {
51 | setVisible(false)
52 | setEditing(false)
53 | setData([])
54 | }}
55 | onEdit={() => setEditing(true)}
56 | onSubmit={() => {
57 | setVisible(false)
58 | setEditing(false)
59 | setData([])
60 | }}
61 | stateValue={stateValue}
62 | />
63 |
64 | )
65 | }
66 | ```
67 |
68 | ```jsx 8 title="4️⃣ Add List Screen 🗒" subtitle="Add 'index' useState"
69 | import React, { useState } from 'react'
70 | import { Button, Modal } from '../shared/Components'
71 |
72 | export const App = () => {
73 | const [visible, setVisible] = useState(false)
74 | const [editing, setEditing] = useState(false)
75 | const [data, setData] = useState([])
76 | const [index, setIndex] = useState(-1)
77 |
78 | let stateValue
79 | if (visible && !editing && data && data.length > 0) {
80 | stateValue = 'view'
81 | } else if (visible && editing) {
82 | stateValue = 'edit'
83 | } else if (visible && (!data || data.length === 0)) {
84 | stateValue = 'create'
85 | }
86 |
87 | return (
88 |
89 | {
91 | setVisible(true)
92 | setData([''])
93 | }}
94 | >
95 | Open Exists
96 |
97 | setVisible(true)}>Open Empty
98 | {
100 | setVisible(false)
101 | setEditing(false)
102 | setData([])
103 | }}
104 | onEdit={() => setEditing(true)}
105 | onSubmit={() => {
106 | setVisible(false)
107 | setEditing(false)
108 | setData([])
109 | }}
110 | stateValue={stateValue}
111 | />
112 |
113 | )
114 | }
115 | ```
116 |
117 | ```jsx 11:18 title="4️⃣ Add List Screen 🗒" subtitle="Incorporate 'index' state into the stateValue determination"
118 | import React, { useState } from 'react'
119 | import { Button, Modal } from '../shared/Components'
120 |
121 | export const App = () => {
122 | const [visible, setVisible] = useState(false)
123 | const [editing, setEditing] = useState(false)
124 | const [data, setData] = useState([])
125 | const [index, setIndex] = useState(-1)
126 |
127 | let stateValue
128 | if (visible && !editing && data && data.length > 1 && index < 0) {
129 | stateValue = 'list'
130 | } else if (
131 | visible &&
132 | !editing &&
133 | data &&
134 | ((data.length > 0 && index >= 0) || data.length === 1)
135 | ) {
136 | stateValue = 'view'
137 | } else if (visible && editing) {
138 | stateValue = 'edit'
139 | } else if (visible && (!data || data.length === 0)) {
140 | stateValue = 'create'
141 | }
142 |
143 | return (
144 |
145 | {
147 | setVisible(true)
148 | setData([''])
149 | }}
150 | >
151 | Open Exists
152 |
153 | setVisible(true)}>Open Empty
154 | {
156 | setVisible(false)
157 | setEditing(false)
158 | setData([])
159 | }}
160 | onEdit={() => setEditing(true)}
161 | onSubmit={() => {
162 | setVisible(false)
163 | setEditing(false)
164 | setData([])
165 | }}
166 | stateValue={stateValue}
167 | />
168 |
169 | )
170 | }
171 | ```
172 |
173 | ```jsx 28:35 title="4️⃣ Add List Screen 🗒" subtitle="Update Buttons"
174 | import React, { useState } from 'react'
175 | import { Button, Modal } from '../shared/Components'
176 |
177 | export const App = () => {
178 | const [visible, setVisible] = useState(false)
179 | const [editing, setEditing] = useState(false)
180 | const [data, setData] = useState([])
181 | const [index, setIndex] = useState(-1)
182 |
183 | let stateValue
184 | if (visible && !editing && data && data.length > 1 && index < 0) {
185 | stateValue = 'list'
186 | } else if (
187 | visible &&
188 | !editing &&
189 | data &&
190 | ((data.length > 0 && index >= 0) || data.length === 1)
191 | ) {
192 | stateValue = 'view'
193 | } else if (visible && editing) {
194 | stateValue = 'edit'
195 | } else if (visible && (!data || data.length === 0)) {
196 | stateValue = 'create'
197 | }
198 |
199 | return (
200 |
201 | {
203 | setVisible(true)
204 | setData(['', ''])
205 | }}
206 | >
207 | Open Multiple
208 |
209 | {
211 | setVisible(true)
212 | setData([''])
213 | }}
214 | >
215 | Open Exists
216 |
217 | setVisible(true)}>Open Empty
218 | {
220 | setVisible(false)
221 | setEditing(false)
222 | setData([])
223 | }}
224 | onEdit={() => setEditing(true)}
225 | onSubmit={() => {
226 | setVisible(false)
227 | setEditing(false)
228 | setData([])
229 | }}
230 | stateValue={stateValue}
231 | />
232 |
233 | )
234 | }
235 | ```
236 |
237 | ```jsx 50,57,59:60 title="4️⃣ Add List Screen 🗒" subtitle="Update Event Propagations"
238 | import React, { useState } from 'react'
239 | import { Button, Modal } from '../shared/Components'
240 |
241 | export const App = () => {
242 | const [visible, setVisible] = useState(false)
243 | const [editing, setEditing] = useState(false)
244 | const [data, setData] = useState([])
245 | const [index, setIndex] = useState(-1)
246 |
247 | let stateValue
248 | if (visible && !editing && data && data.length > 1 && index < 0) {
249 | stateValue = 'list'
250 | } else if (
251 | visible &&
252 | !editing &&
253 | data &&
254 | ((data.length > 0 && index >= 0) || data.length === 1)
255 | ) {
256 | stateValue = 'view'
257 | } else if (visible && editing) {
258 | stateValue = 'edit'
259 | } else if (visible && (!data || data.length === 0)) {
260 | stateValue = 'create'
261 | }
262 |
263 | return (
264 |
265 | {
267 | setVisible(true)
268 | setData(['', ''])
269 | }}
270 | >
271 | Open Multiple
272 |
273 | {
275 | setVisible(true)
276 | setData([''])
277 | }}
278 | >
279 | Open Exists
280 |
281 | setVisible(true)}>Open Empty
282 | {
284 | setVisible(false)
285 | setEditing(false)
286 | setData([])
287 | setIndex(-1)
288 | }}
289 | onEdit={() => setEditing(true)}
290 | onSubmit={() => {
291 | setVisible(false)
292 | setEditing(false)
293 | setData([])
294 | setIndex(-1)
295 | }}
296 | onBack={() => setIndex(-1)}
297 | onView={() => setIndex(0)}
298 | stateValue={stateValue}
299 | />
300 |
301 | )
302 | }
303 | ```
304 |
305 | ```jsx 8,11:18,29:36,50,57,59:60 title="4️⃣ Add List Screen 🗒"
306 | import React, { useState } from 'react'
307 | import { Button, Modal } from '../shared/Components'
308 |
309 | export const App = () => {
310 | const [visible, setVisible] = useState(false)
311 | const [editing, setEditing] = useState(false)
312 | const [data, setData] = useState([])
313 | const [index, setIndex] = useState(-1)
314 |
315 | let stateValue
316 | if (visible && !editing && data && data.length > 1 && index < 0) {
317 | stateValue = 'list'
318 | } else if (
319 | visible &&
320 | !editing &&
321 | data &&
322 | ((data.length > 0 && index >= 0) || data.length === 1)
323 | ) {
324 | stateValue = 'view'
325 | } else if (visible && editing) {
326 | stateValue = 'edit'
327 | } else if (visible && (!data || data.length === 0)) {
328 | stateValue = 'create'
329 | }
330 |
331 | return (
332 |
333 | {
335 | setVisible(true)
336 | setData(['', ''])
337 | }}
338 | >
339 | Open Multiple
340 |
341 | {
343 | setVisible(true)
344 | setData([''])
345 | }}
346 | >
347 | Open Exists
348 |
349 | setVisible(true)}>Open Empty
350 | {
352 | setVisible(false)
353 | setEditing(false)
354 | setData([])
355 | setIndex(-1)
356 | }}
357 | onEdit={() => setEditing(true)}
358 | onSubmit={() => {
359 | setVisible(false)
360 | setEditing(false)
361 | setData([])
362 | setIndex(-1)
363 | }}
364 | onBack={() => setIndex(-1)}
365 | onView={() => setIndex(0)}
366 | stateValue={stateValue}
367 | />
368 |
369 | )
370 | }
371 | ```
372 |
373 |
374 |
375 | ---
376 |
377 | # Mind blown 🤯
378 |
379 | ---
380 |
381 | ## Add List Screen 🗒
382 |
383 | ### with State Machine
384 |
385 | Introducing **Guards Aliasing**
386 |
387 | ---
388 |
389 |
390 |
391 | ```jsx 4:45 title="4️⃣ State Machine Definition ⚙️"
392 | import { Machine } from 'xstate'
393 |
394 | export const modalMachine = Machine(
395 | {
396 | id: 'listView',
397 | initial: 'invisible',
398 | states: {
399 | invisible: {
400 | id: 'invisible',
401 | on: {
402 | OPEN: [
403 | {
404 | cond: (context, event) =>
405 | !event.data || (event.data && event.data.length === 0),
406 | target: 'visible.create',
407 | },
408 | { target: 'visible' },
409 | ],
410 | },
411 | },
412 | visible: {
413 | on: {
414 | CLOSE: 'invisible',
415 | },
416 | initial: 'view',
417 | states: {
418 | view: {
419 | on: {
420 | EDIT: 'edit',
421 | },
422 | },
423 | edit: {
424 | on: {
425 | SUBMIT: '#invisible',
426 | },
427 | },
428 | create: {
429 | on: {
430 | SUBMIT: '#invisible',
431 | },
432 | },
433 | },
434 | },
435 | },
436 | }
437 | //
438 | )
439 | ```
440 |
441 | ```jsx 12:17 title="4️⃣ State Machine Definition ⚙️" subtitle="We have 2 transitions here"
442 | import { Machine } from 'xstate'
443 |
444 | export const modalMachine = Machine(
445 | {
446 | id: 'listView',
447 | initial: 'invisible',
448 | states: {
449 | invisible: {
450 | id: 'invisible',
451 | on: {
452 | OPEN: [
453 | {
454 | cond: (context, event) =>
455 | !event.data || (event.data && event.data.length === 0),
456 | target: 'visible.create',
457 | },
458 | { target: 'visible' },
459 | ],
460 | },
461 | },
462 | visible: {
463 | on: {
464 | CLOSE: 'invisible',
465 | },
466 | initial: 'view',
467 | states: {
468 | view: {
469 | on: {
470 | EDIT: 'edit',
471 | },
472 | },
473 | edit: {
474 | on: {
475 | SUBMIT: '#invisible',
476 | },
477 | },
478 | create: {
479 | on: {
480 | SUBMIT: '#invisible',
481 | },
482 | },
483 | },
484 | },
485 | },
486 | }
487 | //
488 | )
489 | ```
490 |
491 | ```jsx 17:20 title="4️⃣ State Machine Definition ⚙️" subtitle="Add a new guard for single data"
492 | import { Machine } from 'xstate'
493 |
494 | export const modalMachine = Machine(
495 | {
496 | id: 'listView',
497 | initial: 'invisible',
498 | states: {
499 | invisible: {
500 | id: 'invisible',
501 | on: {
502 | OPEN: [
503 | {
504 | cond: (context, event) =>
505 | !event.data || (event.data && event.data.length === 0),
506 | target: 'visible.create',
507 | },
508 | {
509 | cond: (context, event) => event.data && event.data.length === 1
510 | target: 'visible.view',
511 | },
512 | { target: 'visible' },
513 | ],
514 | },
515 | },
516 | visible: {
517 | on: {
518 | CLOSE: 'invisible',
519 | },
520 | initial: 'view',
521 | states: {
522 | view: {
523 | on: {
524 | EDIT: 'edit',
525 | },
526 | },
527 | edit: {
528 | on: {
529 | SUBMIT: '#invisible',
530 | },
531 | },
532 | create: {
533 | on: {
534 | SUBMIT: '#invisible',
535 | },
536 | },
537 | },
538 | },
539 | },
540 | }
541 | //
542 | )
543 | ```
544 |
545 | ```jsx 13:14,18 title="4️⃣ State Machine Definition ⚙️" subtitle="Now it gets harder to read"
546 | import { Machine } from 'xstate'
547 |
548 | export const modalMachine = Machine(
549 | {
550 | id: 'listView',
551 | initial: 'invisible',
552 | states: {
553 | invisible: {
554 | id: 'invisible',
555 | on: {
556 | OPEN: [
557 | {
558 | cond: (context, event) =>
559 | !event.data || (event.data && event.data.length === 0),
560 | target: 'visible.create',
561 | },
562 | {
563 | cond: (context, event) => event.data && event.data.length === 1
564 | target: 'visible.view',
565 | },
566 | { target: 'visible' },
567 | ],
568 | },
569 | },
570 | visible: {
571 | on: {
572 | CLOSE: 'invisible',
573 | },
574 | initial: 'view',
575 | states: {
576 | view: {
577 | on: {
578 | EDIT: 'edit',
579 | },
580 | },
581 | edit: {
582 | on: {
583 | SUBMIT: '#invisible',
584 | },
585 | },
586 | create: {
587 | on: {
588 | SUBMIT: '#invisible',
589 | },
590 | },
591 | },
592 | },
593 | },
594 | }
595 | //
596 | )
597 | ```
598 |
599 | ```jsx 13,17 title="4️⃣ State Machine Definition ⚙️" subtitle="Alias the conditions to increase readability"
600 | import { Machine } from 'xstate'
601 |
602 | export const modalMachine = Machine(
603 | {
604 | id: 'listView',
605 | initial: 'invisible',
606 | states: {
607 | invisible: {
608 | id: 'invisible',
609 | on: {
610 | OPEN: [
611 | {
612 | cond: 'empty',
613 | target: 'visible.create',
614 | },
615 | {
616 | cond: 'single',
617 | target: 'visible.view',
618 | },
619 | { target: 'visible' },
620 | ],
621 | },
622 | },
623 | visible: {
624 | on: {
625 | CLOSE: 'invisible',
626 | },
627 | initial: 'view',
628 | states: {
629 | view: {
630 | on: {
631 | EDIT: 'edit',
632 | },
633 | },
634 | edit: {
635 | on: {
636 | SUBMIT: '#invisible',
637 | },
638 | },
639 | create: {
640 | on: {
641 | SUBMIT: '#invisible',
642 | },
643 | },
644 | },
645 | },
646 | },
647 | }
648 | //
649 | )
650 | ```
651 |
652 | ```jsx 49:55 title="4️⃣ State Machine Definition ⚙️" subtitle="Define the guards at the second argument"
653 | import { Machine } from 'xstate'
654 |
655 | export const modalMachine = Machine(
656 | {
657 | id: 'listView',
658 | initial: 'invisible',
659 | states: {
660 | invisible: {
661 | id: 'invisible',
662 | on: {
663 | OPEN: [
664 | {
665 | cond: 'empty',
666 | target: 'visible.create',
667 | },
668 | {
669 | cond: 'single',
670 | target: 'visible.view',
671 | },
672 | { target: 'visible' },
673 | ],
674 | },
675 | },
676 | visible: {
677 | on: {
678 | CLOSE: 'invisible',
679 | },
680 | initial: 'view',
681 | states: {
682 | view: {
683 | on: {
684 | EDIT: 'edit',
685 | },
686 | },
687 | edit: {
688 | on: {
689 | SUBMIT: '#invisible',
690 | },
691 | },
692 | create: {
693 | on: {
694 | SUBMIT: '#invisible',
695 | },
696 | },
697 | },
698 | },
699 | },
700 | },
701 | {
702 | guards: {
703 | empty: (context, event) =>
704 | !event.data || (event.data && event.data.length === 0),
705 | single: (context, event) => event.data && event.data.length === 1,
706 | },
707 | }
708 | )
709 | ```
710 |
711 | ```jsx 28,30:34,38 title="4️⃣ State Machine Definition ⚙️" subtitle="Define another 'list' state"
712 | import { Machine } from 'xstate'
713 |
714 | export const modalMachine = Machine(
715 | {
716 | id: 'listView',
717 | initial: 'invisible',
718 | states: {
719 | invisible: {
720 | id: 'invisible',
721 | on: {
722 | OPEN: [
723 | {
724 | cond: 'empty',
725 | target: 'visible.create',
726 | },
727 | {
728 | cond: 'single',
729 | target: 'visible.view',
730 | },
731 | { target: 'visible' },
732 | ],
733 | },
734 | },
735 | visible: {
736 | on: {
737 | CLOSE: 'invisible',
738 | },
739 | initial: 'list',
740 | states: {
741 | list: {
742 | on: {
743 | VIEW: 'view',
744 | },
745 | },
746 | view: {
747 | on: {
748 | EDIT: 'edit',
749 | BACK: 'list',
750 | },
751 | },
752 | edit: {
753 | on: {
754 | SUBMIT: '#invisible',
755 | },
756 | },
757 | create: {
758 | on: {
759 | SUBMIT: '#invisible',
760 | },
761 | },
762 | },
763 | },
764 | },
765 | },
766 | {
767 | guards: {
768 | empty: (context, event) =>
769 | !event.data || (event.data && event.data.length === 0),
770 | single: (context, event) => event.data && event.data.length === 1,
771 | },
772 | }
773 | )
774 | ```
775 |
776 | ```jsx 13,16:19,30:34,38,55:61 title="4️⃣ State Machine Definition ⚙️"
777 | import { Machine } from 'xstate'
778 |
779 | export const modalMachine = Machine(
780 | {
781 | id: 'listView',
782 | initial: 'invisible',
783 | states: {
784 | invisible: {
785 | id: 'invisible',
786 | on: {
787 | OPEN: [
788 | {
789 | cond: 'empty',
790 | target: 'visible.create',
791 | },
792 | {
793 | cond: 'single',
794 | target: 'visible.view',
795 | },
796 | { target: 'visible' },
797 | ],
798 | },
799 | },
800 | visible: {
801 | on: {
802 | CLOSE: 'invisible',
803 | },
804 | initial: 'list',
805 | states: {
806 | list: {
807 | on: {
808 | VIEW: 'view',
809 | },
810 | },
811 | view: {
812 | on: {
813 | EDIT: 'edit',
814 | BACK: 'list',
815 | },
816 | },
817 | edit: {
818 | on: {
819 | SUBMIT: '#invisible',
820 | },
821 | },
822 | create: {
823 | on: {
824 | SUBMIT: '#invisible',
825 | },
826 | },
827 | },
828 | },
829 | },
830 | },
831 | {
832 | guards: {
833 | empty: (context, event) =>
834 | !event.data || (event.data && event.data.length === 0),
835 | single: (context, event) => event.data && event.data.length === 1,
836 | },
837 | }
838 | )
839 | ```
840 |
841 |
842 |
843 | ---
844 |
845 | # Visualization
846 |
847 |
855 |
856 | ---
857 |
858 |
859 |
860 | ```jsx title="4️⃣ App with State Machine 🛠"
861 | import React from 'react'
862 | import { Button, Modal } from '../shared/Components'
863 | import { useMachine } from '../shared/useMachine'
864 | import { modalMachine } from './machine'
865 |
866 | export const App = () => {
867 | const [current, send] = useMachine(modalMachine, { devTools: true })
868 |
869 | return (
870 |
871 | send({ type: 'OPEN', data: [''] })}>
872 | Open Exists
873 |
874 | send({ type: 'OPEN', data: [] })}>
875 | Open Empty
876 |
877 | send('CLOSE')}
879 | onEdit={() => send('EDIT')}
880 | onSubmit={() => send('SUBMIT')}
881 | stateValue={current.value.visible}
882 | />
883 |
884 | )
885 | }
886 | ```
887 |
888 | ```jsx 11:13 title="4️⃣ App with State Machine 🛠" subtitle="Add another button for multiple data"
889 | import React from 'react'
890 | import { Button, Modal } from '../shared/Components'
891 | import { useMachine } from '../shared/useMachine'
892 | import { modalMachine } from './machine'
893 |
894 | export const App = () => {
895 | const [current, send] = useMachine(modalMachine, { devTools: true })
896 |
897 | return (
898 |
899 | send({ type: 'OPEN', data: ['', ''] })}>
900 | Open Multiple
901 |
902 | send({ type: 'OPEN', data: [''] })}>
903 | Open Exists
904 |
905 | send({ type: 'OPEN', data: [] })}>
906 | Open Empty
907 |
908 | send('CLOSE')}
910 | onEdit={() => send('EDIT')}
911 | onSubmit={() => send('SUBMIT')}
912 | stateValue={current.value.visible}
913 | />
914 |
915 | )
916 | }
917 | ```
918 |
919 | ```jsx 24:25 title="4️⃣ App with State Machine 🛠" subtitle="Simply send 'BACK' & 'VIEW' events"
920 | import React from 'react'
921 | import { Button, Modal } from '../shared/Components'
922 | import { useMachine } from '../shared/useMachine'
923 | import { modalMachine } from './machine'
924 |
925 | export const App = () => {
926 | const [current, send] = useMachine(modalMachine, { devTools: true })
927 |
928 | return (
929 |
930 | send({ type: 'OPEN', data: ['', ''] })}>
931 | Open Multiple
932 |
933 | send({ type: 'OPEN', data: [''] })}>
934 | Open Exists
935 |
936 | send({ type: 'OPEN', data: [] })}>
937 | Open Empty
938 |
939 | send('CLOSE')}
941 | onEdit={() => send('EDIT')}
942 | onSubmit={() => send('SUBMIT')}
943 | onBack={() => send('BACK')}
944 | onView={() => send('VIEW')}
945 | stateValue={current.value.visible}
946 | />
947 |
948 | )
949 | }
950 | ```
951 |
952 | ```jsx 11:13,24:25 title="4️⃣ App with State Machine 🛠" subtitle="That's it!"
953 | import React from 'react'
954 | import { Button, Modal } from '../shared/Components'
955 | import { useMachine } from '../shared/useMachine'
956 | import { modalMachine } from './machine'
957 |
958 | export const App = () => {
959 | const [current, send] = useMachine(modalMachine, { devTools: true })
960 |
961 | return (
962 |
963 | send({ type: 'OPEN', data: ['', ''] })}>
964 | Open Multiple
965 |
966 | send({ type: 'OPEN', data: [''] })}>
967 | Open Exists
968 |
969 | send({ type: 'OPEN', data: [] })}>
970 | Open Empty
971 |
972 | send('CLOSE')}
974 | onEdit={() => send('EDIT')}
975 | onSubmit={() => send('SUBMIT')}
976 | onBack={() => send('BACK')}
977 | onView={() => send('VIEW')}
978 | stateValue={current.value.visible}
979 | />
980 |
981 | )
982 | }
983 | ```
984 |
985 |
986 |
987 | ---
988 |
989 |
990 |
--------------------------------------------------------------------------------
/demo-d/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 |
4 | export const App = () => {
5 | const [visible, setVisible] = useState(false)
6 | const [editing, setEditing] = useState(false)
7 | const [data, setData] = useState([])
8 | const [index, setIndex] = useState(-1)
9 |
10 | let stateValue
11 | if (visible && !editing && data && data.length > 1 && index < 0) {
12 | stateValue = 'list'
13 | } else if (
14 | visible &&
15 | !editing &&
16 | data &&
17 | ((data.length > 0 && index >= 0) || data.length === 1)
18 | ) {
19 | stateValue = 'view'
20 | } else if (visible && editing) {
21 | stateValue = 'edit'
22 | } else if (visible && (!data || data.length === 0)) {
23 | stateValue = 'create'
24 | }
25 |
26 | return (
27 |
28 | {
30 | setVisible(true)
31 | setData(['', ''])
32 | }}
33 | >
34 | Open Multiple
35 |
36 |
37 | {
39 | setVisible(true)
40 | setData([''])
41 | }}
42 | >
43 | Open Exists
44 |
45 |
46 | setVisible(true)}>Open Empty
47 | {
49 | setVisible(false)
50 | setEditing(false)
51 | setData([])
52 | setIndex(-1)
53 | }}
54 | onEdit={() => setEditing(true)}
55 | onSubmit={() => {
56 | setVisible(false)
57 | setEditing(false)
58 | setData([])
59 | setIndex(-1)
60 | }}
61 | onBack={() => setIndex(-1)}
62 | onView={() => setIndex(0)}
63 | stateValue={stateValue}
64 | />
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/demo-d/AppWithMachine.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Modal } from '../shared/Components'
3 | import { useMachine } from '../shared/useMachine'
4 | import { modalMachine } from './machine'
5 |
6 | export const App = () => {
7 | const [current, send] = useMachine(modalMachine, { devTools: true })
8 |
9 | return (
10 |
11 | send({ type: 'OPEN', data: ['', ''] })}>
12 | Open Multiple
13 |
14 |
15 | send({ type: 'OPEN', data: [''] })}>
16 | Open Single
17 |
18 |
19 | send({ type: 'OPEN', data: [] })}>
20 | Open Empty
21 |
22 | send('CLOSE')}
24 | onEdit={() => send('EDIT')}
25 | onSubmit={() => send('SUBMIT')}
26 | onBack={() => send('BACK')}
27 | onView={() => send('VIEW')}
28 | stateValue={current.value.visible}
29 | />
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/demo-d/machine.js:
--------------------------------------------------------------------------------
1 | import { Machine } from 'xstate'
2 |
3 | export const modalMachine = Machine(
4 | {
5 | id: 'listView',
6 | initial: 'invisible',
7 | states: {
8 | invisible: {
9 | id: 'invisible',
10 | on: {
11 | OPEN: [
12 | {
13 | cond: 'empty',
14 | target: 'visible.create'
15 | },
16 | {
17 | cond: 'single',
18 | target: 'visible.view'
19 | },
20 | { target: 'visible' }
21 | ]
22 | }
23 | },
24 | visible: {
25 | on: {
26 | CLOSE: 'invisible'
27 | },
28 | initial: 'list',
29 | states: {
30 | list: {
31 | on: {
32 | VIEW: 'view'
33 | }
34 | },
35 | view: {
36 | on: {
37 | EDIT: 'edit',
38 | BACK: 'list'
39 | }
40 | },
41 | edit: {
42 | on: {
43 | SUBMIT: '#invisible'
44 | }
45 | },
46 | create: {
47 | on: {
48 | SUBMIT: '#invisible'
49 | }
50 | }
51 | }
52 | }
53 | }
54 | },
55 | {
56 | guards: {
57 | empty: (context, event) =>
58 | !event.data || (event.data && event.data.length === 0),
59 | single: (context, event) => event.data && event.data.length === 1
60 | }
61 | }
62 | )
63 |
--------------------------------------------------------------------------------
/guards-2.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 |
3 | # Add another requirement
4 |
5 | ```js
6 | {type: "OPEN", data: ['']}
7 | ```
8 |
9 | That event should automatically show a single view instead of list
10 |
11 | ---
12 |
13 |
14 |
15 | ```js 17 title="Guards" subtitle="Add another target"
16 | import { Machine } from 'xstate'
17 |
18 | const modalMachine = Machine({
19 | initial: 'invisible',
20 | states: {
21 | invisible: {
22 | id: 'invisible',
23 | on: {
24 | OPEN: [
25 | {
26 | cond: (context, event) =>
27 | !event.data || (event.data && event.data.length === 0),
28 | target: 'visible.create',
29 | },
30 | {
31 | // TODO: Guard
32 | target: 'visible.view',
33 | },
34 | { target: 'visible' },
35 | ],
36 | },
37 | },
38 | visible: {
39 | on: {
40 | CLOSE: 'invisible',
41 | },
42 | initial: 'list',
43 | states: {
44 | list: {
45 | on: {
46 | VIEW: 'view',
47 | },
48 | },
49 | view: {
50 | on: {
51 | EDIT: 'edit',
52 | BACK: 'list',
53 | },
54 | },
55 | edit: {
56 | on: {
57 | SUBMIT: '#invisible',
58 | },
59 | },
60 | create: {
61 | on: {
62 | SUBMIT: '#invisible',
63 | },
64 | },
65 | },
66 | },
67 | },
68 | })
69 | ```
70 |
71 | ```js 16 title="Guards" subtitle="Add another guard"
72 | import { Machine } from 'xstate'
73 |
74 | const modalMachine = Machine({
75 | initial: 'invisible',
76 | states: {
77 | invisible: {
78 | id: 'invisible',
79 | on: {
80 | OPEN: [
81 | {
82 | cond: (context, event) =>
83 | !event.data || (event.data && event.data.length === 0),
84 | target: 'visible.create',
85 | },
86 | {
87 | cond: (context, event) => event.data && event.data.length === 1,
88 | target: 'visible.view',
89 | },
90 | { target: 'visible' },
91 | ],
92 | },
93 | },
94 | visible: {
95 | on: {
96 | CLOSE: 'invisible',
97 | },
98 | initial: 'list',
99 | states: {
100 | list: {
101 | on: {
102 | VIEW: 'view',
103 | },
104 | },
105 | view: {
106 | on: {
107 | EDIT: 'edit',
108 | BACK: 'list',
109 | },
110 | },
111 | edit: {
112 | on: {
113 | SUBMIT: '#invisible',
114 | },
115 | },
116 | create: {
117 | on: {
118 | SUBMIT: '#invisible',
119 | },
120 | },
121 | },
122 | },
123 | },
124 | })
125 | ```
126 |
127 |
128 |
129 | ---
130 |
131 |
132 |
133 | ```js 11:20 title="Guards" subtitle="We need to increase the machine definition readability"
134 | import { Machine } from 'xstate'
135 |
136 | const modalMachine = Machine(
137 | {
138 | initial: 'invisible',
139 | states: {
140 | invisible: {
141 | id: 'invisible',
142 | on: {
143 | OPEN: [
144 | {
145 | cond: (context, event) =>
146 | !event.data || (event.data && event.data.length === 0),
147 | target: 'visible.create',
148 | },
149 | {
150 | cond: (context, event) => event.data && event.data.length === 1,
151 | target: 'visible.view',
152 | },
153 | { target: 'visible' },
154 | ],
155 | },
156 | },
157 | visible: {
158 | on: {
159 | CLOSE: 'invisible',
160 | },
161 | initial: 'list',
162 | states: {
163 | list: {
164 | on: {
165 | VIEW: 'view',
166 | },
167 | },
168 | view: {
169 | on: {
170 | EDIT: 'edit',
171 | BACK: 'list',
172 | },
173 | },
174 | edit: {
175 | on: {
176 | SUBMIT: '#invisible',
177 | },
178 | },
179 | create: {
180 | on: {
181 | SUBMIT: '#invisible',
182 | },
183 | },
184 | },
185 | },
186 | },
187 | },
188 | {
189 | // TODO: Add options here
190 | }
191 | )
192 | ```
193 |
194 | ```js 55:61 title="Guards" subtitle="Guards can be aliased by passing them into the second argument"
195 | import { Machine } from 'xstate'
196 |
197 | const modalMachine = Machine(
198 | {
199 | initial: 'invisible',
200 | states: {
201 | invisible: {
202 | id: 'invisible',
203 | on: {
204 | OPEN: [
205 | {
206 | cond: (context, event) =>
207 | !event.data || (event.data && event.data.length === 0),
208 | target: 'visible.create',
209 | },
210 | {
211 | cond: (context, event) => event.data && event.data.length === 1,
212 | target: 'visible.view',
213 | },
214 | { target: 'visible' },
215 | ],
216 | },
217 | },
218 | visible: {
219 | on: {
220 | CLOSE: 'invisible',
221 | },
222 | initial: 'list',
223 | states: {
224 | list: {
225 | on: {
226 | VIEW: 'view',
227 | },
228 | },
229 | view: {
230 | on: {
231 | EDIT: 'edit',
232 | BACK: 'list',
233 | },
234 | },
235 | edit: {
236 | on: {
237 | SUBMIT: '#invisible',
238 | },
239 | },
240 | create: {
241 | on: {
242 | SUBMIT: '#invisible',
243 | },
244 | },
245 | },
246 | },
247 | },
248 | },
249 | {
250 | guards: {
251 | empty: (context, event) =>
252 | !event.data || (event.data && event.data.length === 0),
253 | single: (context, event) => event.data && event.data.length === 1,
254 | },
255 | }
256 | )
257 | ```
258 |
259 | ```js 57,59 title="Guards" subtitle="We have empty & single guards here"
260 | import { Machine } from 'xstate'
261 |
262 | const modalMachine = Machine(
263 | {
264 | initial: 'invisible',
265 | states: {
266 | invisible: {
267 | id: 'invisible',
268 | on: {
269 | OPEN: [
270 | {
271 | cond: (context, event) =>
272 | !event.data || (event.data && event.data.length === 0),
273 | target: 'visible.create',
274 | },
275 | {
276 | cond: (context, event) => event.data && event.data.length === 1,
277 | target: 'visible.view',
278 | },
279 | { target: 'visible' },
280 | ],
281 | },
282 | },
283 | visible: {
284 | on: {
285 | CLOSE: 'invisible',
286 | },
287 | initial: 'list',
288 | states: {
289 | list: {
290 | on: {
291 | VIEW: 'view',
292 | },
293 | },
294 | view: {
295 | on: {
296 | EDIT: 'edit',
297 | BACK: 'list',
298 | },
299 | },
300 | edit: {
301 | on: {
302 | SUBMIT: '#invisible',
303 | },
304 | },
305 | create: {
306 | on: {
307 | SUBMIT: '#invisible',
308 | },
309 | },
310 | },
311 | },
312 | },
313 | },
314 | {
315 | guards: {
316 | empty: (context, event) =>
317 | !event.data || (event.data && event.data.length === 0),
318 | single: (context, event) => event.data && event.data.length === 1,
319 | },
320 | }
321 | )
322 | ```
323 |
324 | ```js 12,16 title="Guards" subtitle="Now the guards are semantically aliased"
325 | import { Machine } from 'xstate'
326 |
327 | const modalMachine = Machine(
328 | {
329 | initial: 'invisible',
330 | states: {
331 | invisible: {
332 | id: 'invisible',
333 | on: {
334 | OPEN: [
335 | {
336 | cond: 'empty',
337 | target: 'visible.create',
338 | },
339 | {
340 | cond: 'single',
341 | target: 'visible.view',
342 | },
343 | { target: 'visible' },
344 | ],
345 | },
346 | },
347 | visible: {
348 | on: {
349 | CLOSE: 'invisible',
350 | },
351 | initial: 'list',
352 | states: {
353 | list: {
354 | on: {
355 | VIEW: 'view',
356 | },
357 | },
358 | view: {
359 | on: {
360 | EDIT: 'edit',
361 | BACK: 'list',
362 | },
363 | },
364 | edit: {
365 | on: {
366 | SUBMIT: '#invisible',
367 | },
368 | },
369 | create: {
370 | on: {
371 | SUBMIT: '#invisible',
372 | },
373 | },
374 | },
375 | },
376 | },
377 | },
378 | {
379 | guards: {
380 | empty: (context, event) =>
381 | !event.data || (event.data && event.data.length === 0),
382 | single: (context, event) => event.data && event.data.length === 1,
383 | },
384 | }
385 | )
386 | ```
387 |
388 | ```js 3:61 title="Guards" subtitle="Visualize again"
389 | import { Machine } from 'xstate'
390 |
391 | const modalMachine = Machine(
392 | {
393 | initial: 'invisible',
394 | states: {
395 | invisible: {
396 | id: 'invisible',
397 | on: {
398 | OPEN: [
399 | {
400 | cond: 'empty',
401 | target: 'visible.create',
402 | },
403 | {
404 | cond: 'single',
405 | target: 'visible.view',
406 | },
407 | { target: 'visible' },
408 | ],
409 | },
410 | },
411 | visible: {
412 | on: {
413 | CLOSE: 'invisible',
414 | },
415 | initial: 'list',
416 | states: {
417 | list: {
418 | on: {
419 | VIEW: 'view',
420 | },
421 | },
422 | view: {
423 | on: {
424 | EDIT: 'edit',
425 | BACK: 'list',
426 | },
427 | },
428 | edit: {
429 | on: {
430 | SUBMIT: '#invisible',
431 | },
432 | },
433 | create: {
434 | on: {
435 | SUBMIT: '#invisible',
436 | },
437 | },
438 | },
439 | },
440 | },
441 | },
442 | {
443 | guards: {
444 | empty: (context, event) =>
445 | !event.data || (event.data && event.data.length === 0),
446 | single: (context, event) => event.data && event.data.length === 1,
447 | },
448 | }
449 | )
450 | ```
451 |
452 |
453 |
454 | ---
455 |
456 | # Visualize
457 |
458 | Verify again in the XState visualizer
459 |
460 | This event now should lead to the `view` state instead:
461 |
462 | ```js
463 | {type: "OPEN", data: ['']}
464 | ```
465 |
--------------------------------------------------------------------------------
/guards.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 | import { Image } from '@mdx-deck/components'
3 |
4 | # Any missing transition?
5 |
6 |
15 |
16 | How do we reach the `create` state?
17 |
18 | ---
19 |
20 |
21 |
22 | ```js 9 title="Guards" subtitle="as a conditional state transition"
23 | import { Machine } from 'xstate'
24 |
25 | const modalMachine = Machine({
26 | initial: 'invisible',
27 | states: {
28 | invisible: {
29 | id: 'invisible',
30 | on: {
31 | OPEN: 'visible',
32 | },
33 | },
34 | visible: {
35 | on: {
36 | CLOSE: 'invisible',
37 | },
38 | initial: 'list',
39 | states: {
40 | list: {
41 | on: {
42 | VIEW: 'view',
43 | },
44 | },
45 | view: {
46 | on: {
47 | EDIT: 'edit',
48 | BACK: 'list',
49 | },
50 | },
51 | edit: {
52 | on: {
53 | SUBMIT: '#invisible',
54 | },
55 | },
56 | create: {
57 | on: {
58 | SUBMIT: '#invisible',
59 | },
60 | },
61 | },
62 | },
63 | },
64 | })
65 | ```
66 |
67 | ```js 9 title="Guards" subtitle="this is identical to the previous one"
68 | import { Machine } from 'xstate'
69 |
70 | const modalMachine = Machine({
71 | initial: 'invisible',
72 | states: {
73 | invisible: {
74 | id: 'invisible',
75 | on: {
76 | OPEN: { target: 'visible' },
77 | },
78 | },
79 | visible: {
80 | on: {
81 | CLOSE: 'invisible',
82 | },
83 | initial: 'list',
84 | states: {
85 | list: {
86 | on: {
87 | VIEW: 'view',
88 | },
89 | },
90 | view: {
91 | on: {
92 | EDIT: 'edit',
93 | BACK: 'list',
94 | },
95 | },
96 | edit: {
97 | on: {
98 | SUBMIT: '#invisible',
99 | },
100 | },
101 | create: {
102 | on: {
103 | SUBMIT: '#invisible',
104 | },
105 | },
106 | },
107 | },
108 | },
109 | })
110 | ```
111 |
112 | ```js 9:12 title="Guards" subtitle="this is also identical"
113 | import { Machine } from 'xstate'
114 |
115 | const modalMachine = Machine({
116 | initial: 'invisible',
117 | states: {
118 | invisible: {
119 | id: 'invisible',
120 | on: {
121 | OPEN: [
122 | // ⚠️ an array here
123 | { target: 'visible' },
124 | ],
125 | },
126 | },
127 | visible: {
128 | on: {
129 | CLOSE: 'invisible',
130 | },
131 | initial: 'list',
132 | states: {
133 | list: {
134 | on: {
135 | VIEW: 'view',
136 | },
137 | },
138 | view: {
139 | on: {
140 | EDIT: 'edit',
141 | BACK: 'list',
142 | },
143 | },
144 | edit: {
145 | on: {
146 | SUBMIT: '#invisible',
147 | },
148 | },
149 | create: {
150 | on: {
151 | SUBMIT: '#invisible',
152 | },
153 | },
154 | },
155 | },
156 | },
157 | })
158 | ```
159 |
160 | ```js 11,13 title="Guards" subtitle="The same event needs to target different states"
161 | import { Machine } from 'xstate'
162 |
163 | const modalMachine = Machine({
164 | initial: 'invisible',
165 | states: {
166 | invisible: {
167 | id: 'invisible',
168 | on: {
169 | OPEN: [
170 | {
171 | target: 'visible.create',
172 | },
173 | { target: 'visible' },
174 | ],
175 | },
176 | },
177 | visible: {
178 | on: {
179 | CLOSE: 'invisible',
180 | },
181 | initial: 'list',
182 | states: {
183 | list: {
184 | on: {
185 | VIEW: 'view',
186 | },
187 | },
188 | view: {
189 | on: {
190 | EDIT: 'edit',
191 | BACK: 'list',
192 | },
193 | },
194 | edit: {
195 | on: {
196 | SUBMIT: '#invisible',
197 | },
198 | },
199 | create: {
200 | on: {
201 | SUBMIT: '#invisible',
202 | },
203 | },
204 | },
205 | },
206 | },
207 | })
208 | ```
209 |
210 | ```js 11:12 title="Guards" subtitle="Target different states conditionally"
211 | import { Machine } from 'xstate'
212 |
213 | const modalMachine = Machine({
214 | initial: 'invisible',
215 | states: {
216 | invisible: {
217 | id: 'invisible',
218 | on: {
219 | OPEN: [
220 | {
221 | cond: (context, event) =>
222 | !event.data || (event.data && event.data.length === 0),
223 | target: 'visible.create',
224 | },
225 | { target: 'visible' },
226 | ],
227 | },
228 | },
229 | visible: {
230 | on: {
231 | CLOSE: 'invisible',
232 | },
233 | initial: 'list',
234 | states: {
235 | list: {
236 | on: {
237 | VIEW: 'view',
238 | },
239 | },
240 | view: {
241 | on: {
242 | EDIT: 'edit',
243 | BACK: 'list',
244 | },
245 | },
246 | edit: {
247 | on: {
248 | SUBMIT: '#invisible',
249 | },
250 | },
251 | create: {
252 | on: {
253 | SUBMIT: '#invisible',
254 | },
255 | },
256 | },
257 | },
258 | },
259 | })
260 | ```
261 |
262 | ```js 3:49 title="Guards" subtitle="Visualize again"
263 | import { Machine } from 'xstate'
264 |
265 | const modalMachine = Machine({
266 | initial: 'invisible',
267 | states: {
268 | invisible: {
269 | id: 'invisible',
270 | on: {
271 | OPEN: [
272 | {
273 | cond: (context, event) =>
274 | !event.data || (event.data && event.data.length === 0),
275 | target: 'visible.create',
276 | },
277 | { target: 'visible' },
278 | ],
279 | },
280 | },
281 | visible: {
282 | on: {
283 | CLOSE: 'invisible',
284 | },
285 | initial: 'list',
286 | states: {
287 | list: {
288 | on: {
289 | VIEW: 'view',
290 | },
291 | },
292 | view: {
293 | on: {
294 | EDIT: 'edit',
295 | BACK: 'list',
296 | },
297 | },
298 | edit: {
299 | on: {
300 | SUBMIT: '#invisible',
301 | },
302 | },
303 | create: {
304 | on: {
305 | SUBMIT: '#invisible',
306 | },
307 | },
308 | },
309 | },
310 | },
311 | })
312 | ```
313 |
314 |
315 |
316 | ---
317 |
318 | # Visualize
319 |
320 | Verify again in the XState visualizer
321 |
322 | Try sending these events:
323 |
324 | ```js
325 | {type: "OPEN", data: []}
326 | ```
327 |
328 | ```js
329 | {type: "OPEN", data: ['', '']}
330 | ```
331 |
--------------------------------------------------------------------------------
/hierarchy.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 |
3 |
4 |
5 | ```js title="Hierarchical State"
6 | import { Machine } from 'xstate'
7 |
8 | const modalMachine = Machine({
9 | initial: 'invisible',
10 | states: {
11 | invisible: {
12 | on: {
13 | OPEN: 'visible',
14 | },
15 | },
16 | visible: {
17 | on: {
18 | CLOSE: 'invisible',
19 | },
20 | },
21 | },
22 | })
23 | ```
24 |
25 | ```js 15 title="Hierarchical State" subtitle="Define Inner States of the 'visible' state"
26 | import { Machine } from 'xstate'
27 |
28 | const modalMachine = Machine({
29 | initial: 'invisible',
30 | states: {
31 | invisible: {
32 | on: {
33 | OPEN: 'visible',
34 | },
35 | },
36 | visible: {
37 | on: {
38 | CLOSE: 'invisible',
39 | },
40 | // TODO: Inner States of the 'visible' state
41 | },
42 | },
43 | })
44 | ```
45 |
46 | ```js 15,17,20,23,26 title="Hierarchical State" subtitle="Inner states of the 'visible' state"
47 | import { Machine } from 'xstate'
48 |
49 | const modalMachine = Machine({
50 | initial: 'invisible',
51 | states: {
52 | invisible: {
53 | on: {
54 | OPEN: 'visible',
55 | },
56 | },
57 | visible: {
58 | on: {
59 | CLOSE: 'invisible',
60 | },
61 | initial: 'list',
62 | states: {
63 | list: {
64 | //
65 | },
66 | view: {
67 | //
68 | },
69 | edit: {
70 | //
71 | },
72 | create: {
73 | //
74 | },
75 | },
76 | },
77 | },
78 | })
79 | ```
80 |
81 | ```js 18:20,23:26 title="Hierarchical State" subtitle="Inner events of the 'visible' state"
82 | import { Machine } from 'xstate'
83 |
84 | const modalMachine = Machine({
85 | initial: 'invisible',
86 | states: {
87 | invisible: {
88 | on: {
89 | OPEN: 'visible',
90 | },
91 | },
92 | visible: {
93 | on: {
94 | CLOSE: 'invisible',
95 | },
96 | initial: 'list',
97 | states: {
98 | list: {
99 | on: {
100 | VIEW: 'view',
101 | },
102 | },
103 | view: {
104 | on: {
105 | EDIT: 'edit',
106 | BACK: 'list',
107 | },
108 | },
109 | edit: {
110 | //
111 | },
112 | create: {
113 | //
114 | },
115 | },
116 | },
117 | },
118 | })
119 | ```
120 |
121 | ```js 7,30:32,35:37 title="Hierarchical State" subtitle="Referencing external state by #id"
122 | import { Machine } from 'xstate'
123 |
124 | const modalMachine = Machine({
125 | initial: 'invisible',
126 | states: {
127 | invisible: {
128 | id: 'invisible',
129 | on: {
130 | OPEN: 'visible',
131 | },
132 | },
133 | visible: {
134 | on: {
135 | CLOSE: 'invisible',
136 | },
137 | initial: 'list',
138 | states: {
139 | list: {
140 | on: {
141 | VIEW: 'view',
142 | },
143 | },
144 | view: {
145 | on: {
146 | EDIT: 'edit',
147 | BACK: 'list',
148 | },
149 | },
150 | edit: {
151 | on: {
152 | SUBMIT: '#invisible',
153 | },
154 | },
155 | create: {
156 | on: {
157 | SUBMIT: '#invisible',
158 | },
159 | },
160 | },
161 | },
162 | },
163 | })
164 | ```
165 |
166 | ```js 3:42 title="Hierarchical State" subtitle="How does it look like on the state diagram?"
167 | import { Machine } from 'xstate'
168 |
169 | const modalMachine = Machine({
170 | initial: 'invisible',
171 | states: {
172 | invisible: {
173 | id: 'invisible',
174 | on: {
175 | OPEN: 'visible',
176 | },
177 | },
178 | visible: {
179 | on: {
180 | CLOSE: 'invisible',
181 | },
182 | initial: 'list',
183 | states: {
184 | list: {
185 | on: {
186 | VIEW: 'view',
187 | },
188 | },
189 | view: {
190 | on: {
191 | EDIT: 'edit',
192 | BACK: 'list',
193 | },
194 | },
195 | edit: {
196 | on: {
197 | SUBMIT: '#invisible',
198 | },
199 | },
200 | create: {
201 | on: {
202 | SUBMIT: '#invisible',
203 | },
204 | },
205 | },
206 | },
207 | },
208 | })
209 | ```
210 |
211 |
212 |
213 | ---
214 |
215 | # Visualize
216 |
217 | XState visualizer
218 |
--------------------------------------------------------------------------------
/images/createView-state-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/createView-state-machine.png
--------------------------------------------------------------------------------
/images/designers-flow-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/designers-flow-diagram.png
--------------------------------------------------------------------------------
/images/editView-state-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/editView-state-machine.png
--------------------------------------------------------------------------------
/images/how-do-we-reach-the-create-state.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/how-do-we-reach-the-create-state.jpg
--------------------------------------------------------------------------------
/images/listView-state-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/listView-state-machine.png
--------------------------------------------------------------------------------
/images/user-flow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/user-flow.gif
--------------------------------------------------------------------------------
/images/visibility-state-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zainfathoni/state-machines-meet-react-hooks/e7c96bf15de659003e6d0cd448d4afae9059bc48/images/visibility-state-machine.png
--------------------------------------------------------------------------------
/interpreter.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 |
3 | # State Machine [Interpreter](https://xstate.js.org/docs/guides/interpretation.html#interpreter)
4 |
5 | - State machines are just pure functions (stateless)
6 | - Interpreter is where the `state` is managed
7 |
8 | ---
9 |
10 |
11 |
12 | ```js title="XState Interpreter API"
13 | import { Machine, interpret } from 'xstate'
14 |
15 | const machine = Machine(/* machine config */)
16 | ```
17 |
18 | ```js 5:7 title="XState Interpreter API" subtitle="Interpret the machine, and add a listener for whenever a transition occurs."
19 | import { Machine, interpret } from 'xstate'
20 |
21 | const machine = Machine(/* machine config */)
22 |
23 | const service = interpret(machine).onTransition(state => {
24 | console.log(state.value)
25 | })
26 | ```
27 |
28 | ```js 9 title="XState Interpreter API" subtitle="Start the service"
29 | import { Machine, interpret } from 'xstate'
30 |
31 | const machine = Machine(/* machine config */)
32 |
33 | const service = interpret(machine).onTransition(state => {
34 | console.log(state.value)
35 | })
36 |
37 | service.start()
38 | ```
39 |
40 | ```js 11 title="XState Interpreter API" subtitle="Send events"
41 | import { Machine, interpret } from 'xstate'
42 |
43 | const machine = Machine(/* machine config */)
44 |
45 | const service = interpret(machine).onTransition(state => {
46 | console.log(state.value)
47 | })
48 |
49 | service.start()
50 |
51 | service.send('SOME_EVENT')
52 | ```
53 |
54 | ```js 13 title="XState Interpreter API" subtitle="Stop the service"
55 | import { Machine, interpret } from 'xstate'
56 |
57 | const machine = Machine(/* machine config */)
58 |
59 | const service = interpret(machine).onTransition(state => {
60 | console.log(state.value)
61 | })
62 |
63 | service.start()
64 |
65 | service.send('SOME_EVENT')
66 |
67 | service.stop()
68 | ```
69 |
70 |
71 |
--------------------------------------------------------------------------------
/introduction.mdx:
--------------------------------------------------------------------------------
1 | import { Appear, Image, Notes } from 'mdx-deck'
2 |
3 | # [State Machines](https://xstate.js.org/) Meet [React Hooks](https://reactjs.org/docs/hooks-intro.html)
4 |
5 | ## JSConf Asia 2019
6 |
7 | [@zainfathoni](https://twitter.com/zainfathoni)
8 |
9 | ---
10 |
11 | # I am [Zain Fathoni](https://github.com/zainfathoni) 🤝
12 |
13 |
14 |
15 | Indonesian 🇮🇩 family with 2 kids 👨👩👧👦
16 |
17 | Working @ Ninja Van Singapore 🇸🇬
18 |
19 | Backend turned Frontend 👨🏻💻
20 | ❤️ React & its Hooks ⚛️
21 |
22 |
23 |
24 | ---
25 |
26 | # What this talk is 😄
27 |
28 | - A _very_ brief introduction to State Machines
29 | - A small replica of my personal journey with:
30 | - [React Hooks](https://reactjs.org/docs/hooks-intro.html)
31 | - State Machines, especially [Xstate](https://xstate.js.org/) in particular
32 | - How to combine them together
33 | - There will be **A LOT** of code, buckle up!
34 |
35 | ---
36 |
37 | # What this talk is _not_ 😅
38 |
39 | - An introduction to React Hooks
40 | - A deep dig down of State Machines concepts
41 |
42 |
43 |
44 |
45 | Let's assume that everybody here is familiar enough with React Hooks
46 |
47 | Shawn Wang will also talk about it later today
48 |
49 |
50 | State Machines concepts are huge and it won't fit in this 30
51 | minutes talk
52 |
53 |
54 |
55 |
56 | ---
57 |
58 | # What is State Machines?
59 |
60 |
87 |
88 | ---
89 |
90 | ## I got a lot of these from my designer 🤯
91 |
92 |
97 |
98 | ---
99 |
100 | #### State Machines provide a common language for designers & developers 😍
101 |
102 |
110 |
111 |
112 | Source:{' '}
113 |
114 | https://dribbble.com/shots/1871910-User-Flow-Part-1
115 |
116 |
117 |
118 | ---
119 |
120 | # Problem Statement 🤯
121 |
122 |
123 |
124 | Complex UI flows require multiple states ⚛️
125 | Multiple states are generally too implicit 🧐
126 | Implicit codes are harder to reason about 🤔
127 | This causes hidden bugs everywhere due to "invisible states" 🕶
128 |
129 |
130 |
131 | ---
132 |
133 | # State Machines to the Rescue!
134 |
135 | ## 🦸♂️ 🦸♀️
136 |
137 | ---
138 |
139 | # Hypothesis 🤔
140 |
141 |
142 |
143 | State machines make all states are visible 👀
144 | No invisible states means no hidden bugs 🤞
145 |
146 |
147 |
148 | ---
149 |
150 | # Enough talk 🗣
151 |
152 | ## Show me the code! 💻
153 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "version": "1.0.0",
4 | "scripts": {
5 | "start": "mdx-deck deck.js",
6 | "build": "mdx-deck build deck.js",
7 | "test": "echo \"there are no tests\""
8 | },
9 | "devDependencies": {
10 | "code-surfer": "2.0.0-alpha.5",
11 | "file-loader": "4.0.0",
12 | "mdx-deck": "2.3.2"
13 | },
14 | "name": "state-machines-meet-react-hooks",
15 | "dependencies": {
16 | "antd": "3.19.3",
17 | "xstate": "4.6.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/shared/Components.js:
--------------------------------------------------------------------------------
1 | import { Button as AntButton, List, Modal as AntModal } from 'antd'
2 | import React from 'react'
3 |
4 | export const Button = props =>
5 |
6 | const PrimaryButton = props =>
7 |
8 | const ListView = ({ onView }) => (
9 |
10 |
11 | Item 1
12 |
13 |
14 | Item 2
15 |
16 |
17 | Item 3
18 |
19 |
20 | )
21 |
22 | const DetailView = ({ onBack, onEdit }) => (
23 | <>
24 | Back
25 | Edit
26 | >
27 | )
28 |
29 | const DetailEdit = ({ onSubmit }) => (
30 | Submit
31 | )
32 |
33 | export const Modal = ({
34 | onBack,
35 | onClose,
36 | onEdit,
37 | onSubmit,
38 | onView,
39 | stateValue
40 | }) => (
41 |
48 | {stateValue === 'list' ? (
49 |
50 | ) : stateValue === 'view' ? (
51 |
52 | ) : stateValue === 'edit' ? (
53 |
54 | ) : stateValue === 'create' ? (
55 |
56 | ) : (
57 | 'No Content'
58 | )}
59 |
60 | )
61 |
--------------------------------------------------------------------------------
/shared/useMachine.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react'
2 | import { interpret } from 'xstate'
3 |
4 | export function useMachine(machine, options) {
5 | const [current, setCurrent] = useState(machine.initialState)
6 |
7 | const serviceRef = useRef(null)
8 |
9 | if (serviceRef.current === null) {
10 | serviceRef.current = interpret(machine, options).onTransition(state => {
11 | if (state.changed) {
12 | setCurrent(state)
13 | }
14 | })
15 | }
16 |
17 | const service = serviceRef.current
18 |
19 | useEffect(() => {
20 | service.start()
21 |
22 | return () => {
23 | service.stop()
24 | }
25 | }, [service])
26 |
27 | return [current, service.send]
28 | }
29 |
--------------------------------------------------------------------------------
/solution.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 |
3 |
4 |
5 | ```js 3 title="Let's build the Modal Machine"
6 | import { Machine } from 'xstate'
7 |
8 | const modalMachine = Machine({
9 | //
10 | })
11 | ```
12 |
13 | ```js 4:6,8:9,11 subtitle="We have 2 states: invisible & visible"
14 | import { Machine } from 'xstate'
15 |
16 | const modalMachine = Machine({
17 | initial: 'invisible',
18 | states: {
19 | invisible: {
20 | //
21 | },
22 | visible: {
23 | //
24 | },
25 | },
26 | })
27 | ```
28 |
29 | ```js 7:9,12:14 subtitle="We have 2 events: OPEN & CLOSE"
30 | import { Machine } from 'xstate'
31 |
32 | const modalMachine = Machine({
33 | initial: 'invisible',
34 | states: {
35 | invisible: {
36 | on: {
37 | OPEN: 'visible',
38 | },
39 | },
40 | visible: {
41 | on: {
42 | CLOSE: 'invisible',
43 | },
44 | },
45 | },
46 | })
47 | ```
48 |
49 | ```js 3:17 subtitle="Copy these lines"
50 | import { Machine } from 'xstate'
51 |
52 | const modalMachine = Machine({
53 | initial: 'invisible',
54 | states: {
55 | invisible: {
56 | on: {
57 | OPEN: 'visible',
58 | },
59 | },
60 | visible: {
61 | on: {
62 | CLOSE: 'invisible',
63 | },
64 | },
65 | },
66 | })
67 | ```
68 |
69 |
70 |
71 | ---
72 |
73 | # Visualize
74 |
75 | Verify again in the XState visualizer
76 |
--------------------------------------------------------------------------------
/toggle.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 |
3 | # State Machine Introduction
4 |
5 | ---
6 |
7 |
8 |
9 | ```js 1:3,5 title="A simple toggle machine example"
10 | import { Machine } from 'xstate'
11 |
12 | const toggleMachine = Machine({
13 | //
14 | })
15 | ```
16 |
17 | ```js 4:4 title="A simple toggle machine example" subtitle="Initial state"
18 | import { Machine } from 'xstate'
19 |
20 | const toggleMachine = Machine({
21 | initial: 'inactive',
22 | })
23 | ```
24 |
25 | ```js 5:6,8:9,11:12 title="A simple toggle machine example" subtitle="States"
26 | import { Machine } from 'xstate'
27 |
28 | const toggleMachine = Machine({
29 | initial: 'inactive',
30 | states: {
31 | inactive: {
32 | //
33 | },
34 | active: {
35 | //
36 | },
37 | },
38 | })
39 | ```
40 |
41 | ```js 7:9,12:14 title="A simple toggle machine example" subtitle="State Transitions"
42 | import { Machine } from 'xstate'
43 |
44 | const toggleMachine = Machine({
45 | initial: 'inactive',
46 | states: {
47 | inactive: {
48 | on: {
49 | TOGGLE: 'active',
50 | },
51 | },
52 | active: {
53 | on: {
54 | TOGGLE: 'inactive',
55 | },
56 | },
57 | },
58 | })
59 | ```
60 |
61 | ```js 3:17 title="A simple toggle machine example" subtitle="Copy these lines"
62 | import { Machine } from 'xstate'
63 |
64 | const toggleMachine = Machine({
65 | initial: 'inactive',
66 | states: {
67 | inactive: {
68 | on: {
69 | TOGGLE: 'active',
70 | },
71 | },
72 | active: {
73 | on: {
74 | TOGGLE: 'inactive',
75 | },
76 | },
77 | },
78 | })
79 | ```
80 |
81 |
82 |
83 | ---
84 |
85 | # Visualize
86 |
87 | Paste to the XState visualizer
88 |
--------------------------------------------------------------------------------
/use-machine.mdx:
--------------------------------------------------------------------------------
1 | import { CodeSurferLayout } from 'code-surfer'
2 |
3 | # Extracting into a Custom Hook
4 |
5 | Let's call it a `useMachine` hook
6 |
7 | ---
8 |
9 |
10 |
11 | ```jsx title="Extracting into useMachine" subtitle="This is our simple app so far"
12 | import React, { useState, useRef, useEffect } from 'react'
13 | import { Button, Modal } from './Components'
14 | import { modalMachine } from './machine'
15 | import { interpret } from 'xstate'
16 |
17 | export const App = () => {
18 | const [current, setCurrent] = useState(modalMachine.initialState)
19 |
20 | const serviceRef = useRef(null)
21 |
22 | if (serviceRef.current === null) {
23 | serviceRef.current = interpret(modalMachine, {
24 | devTools: true,
25 | }).onTransition(state => {
26 | if (state.changed) {
27 | setCurrent(state)
28 | }
29 | })
30 | }
31 |
32 | const service = serviceRef.current
33 |
34 | useEffect(() => {
35 | service.start()
36 |
37 | return () => {
38 | service.stop()
39 | }
40 | }, [])
41 |
42 | const handleOpen = () => service.send('OPEN')
43 | const handleClose = () => service.send('CLOSE')
44 |
45 | return (
46 |
47 | Open Modal
48 |
49 |
50 | )
51 | }
52 | ```
53 |
54 | ```jsx 7:29 title="Extracting into useMachine" subtitle="This chunk of code can be extracted out"
55 | import React, { useState, useRef, useEffect } from 'react'
56 | import { Button, Modal } from './Components'
57 | import { modalMachine } from './machine'
58 | import { interpret } from 'xstate'
59 |
60 | export const App = () => {
61 | const [current, setCurrent] = useState(modalMachine.initialState)
62 |
63 | const serviceRef = useRef(null)
64 |
65 | if (serviceRef.current === null) {
66 | serviceRef.current = interpret(modalMachine, {
67 | devTools: true,
68 | }).onTransition(state => {
69 | if (state.changed) {
70 | setCurrent(state)
71 | }
72 | })
73 | }
74 |
75 | const service = serviceRef.current
76 |
77 | useEffect(() => {
78 | service.start()
79 |
80 | return () => {
81 | service.stop()
82 | }
83 | }, [])
84 |
85 | const handleOpen = () => service.send('OPEN')
86 | const handleClose = () => service.send('CLOSE')
87 |
88 | return (
89 |
90 | Open Modal
91 |
92 |
93 | )
94 | }
95 | ```
96 |
97 | ```jsx 1:4 title="Extracting into useMachine" subtitle="Accept machine & options params"
98 | import { useState, useRef, useEffect } from 'react'
99 | import { interpret } from 'xstate'
100 |
101 | export function useMachine(machine, options) {
102 | const [current, setCurrent] = useState(modalMachine.initialState)
103 |
104 | const serviceRef = useRef(null)
105 |
106 | if (serviceRef.current === null) {
107 | serviceRef.current = interpret(modalMachine, {
108 | devTools: true,
109 | }).onTransition(state => {
110 | if (state.changed) {
111 | setCurrent(state)
112 | }
113 | })
114 | }
115 |
116 | const service = serviceRef.current
117 |
118 | useEffect(() => {
119 | service.start()
120 |
121 | return () => {
122 | service.stop()
123 | }
124 | }, [])
125 |
126 | return [current, service.send]
127 | }
128 | ```
129 |
130 | ```jsx 5,10 title="Extracting into useMachine" subtitle="Rename arguments accordingly"
131 | import { useState, useRef, useEffect } from 'react'
132 | import { interpret } from 'xstate'
133 |
134 | export function useMachine(machine, options) {
135 | const [current, setCurrent] = useState(machine.initialState)
136 |
137 | const serviceRef = useRef(null)
138 |
139 | if (serviceRef.current === null) {
140 | serviceRef.current = interpret(machine, options).onTransition(state => {
141 | if (state.changed) {
142 | setCurrent(state)
143 | }
144 | })
145 | }
146 |
147 | const service = serviceRef.current
148 |
149 | useEffect(() => {
150 | service.start()
151 |
152 | return () => {
153 | service.stop()
154 | }
155 | }, [])
156 |
157 | return [current, service.send]
158 | }
159 | ```
160 |
161 | ```jsx 27 title="Extracting into useMachine" subtitle="Return current machine state & the send function"
162 | import { useState, useRef, useEffect } from 'react'
163 | import { interpret } from 'xstate'
164 |
165 | export function useMachine(machine, options) {
166 | const [current, setCurrent] = useState(machine.initialState)
167 |
168 | const serviceRef = useRef(null)
169 |
170 | if (serviceRef.current === null) {
171 | serviceRef.current = interpret(machine, options).onTransition(state => {
172 | if (state.changed) {
173 | setCurrent(state)
174 | }
175 | })
176 | }
177 |
178 | const service = serviceRef.current
179 |
180 | useEffect(() => {
181 | service.start()
182 |
183 | return () => {
184 | service.stop()
185 | }
186 | }, [])
187 |
188 | return [current, service.send]
189 | }
190 | ```
191 |
192 |
193 |
--------------------------------------------------------------------------------