├── .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 | 49 | 57 | 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 | 119 | 127 | 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 | 189 | 197 | 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 | 240 | 248 | 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 | 291 | 299 | 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 | 337 | 340 | 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 | 370 | 373 | 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 | 13 | 14 | --- 15 | 16 | # Caution! ⚠️ 17 | 18 | 30 | 31 | --- 32 | 33 | # Lessons Learned 📝 34 | 35 | 68 | 69 | --- 70 | 71 | # Key Takeaways 🔑 72 | 73 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 125 | 126 | 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 |   21 |   22 |   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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 157 | 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 | 203 | 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 | 251 | 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 | 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 | 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 | 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 | 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 | 735 | 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 | 763 | 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 | 28 |   29 | 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 | 14 |   15 | 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 | 48 | 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 | 97 | 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 | 153 | 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 | 209 | 217 | 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 | 273 | 281 | 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 | 341 | 349 | 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 | 874 | 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 | 902 | 905 | 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 | 933 | 936 | 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 | 966 | 969 | 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 | 36 |   37 | 45 |   46 | 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 | 14 |   15 | 18 |   19 | 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 | How do we reach the create state? 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 | Flow Diagram example 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 | 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 | 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 | --------------------------------------------------------------------------------