<!-- Marcus: Intro, Controlling a Saga, Fazit Moritz: Generators, Testing Leo: Example + Demo, Advanced Use Cases --> <style> img { border: none !important; width: 30vh; object-fit: contain; background: none !important; box-shadow: none !important; } p code:not(.hljs), li code:not(.hljs) { color: #dc8c8c; } .reveal blockquote { padding: 0 25px; } .profilepics > div { display: flex; justify-content: space-around; } .profilepics img { border-radius: 50%; height: 25vh; width: 25vh; object-fit: cover; max-width: 100%; } .profilepics h2 { font-size: 3vh; margin-bottom: 0; } .profilepics h3 { font-size: 2vh; } .hljs { font-size: 80%; } </style> # Redux Saga #### Transparent Asynchronous State Changes *React Meetup DΓΌsseldorf* --- <!-- .slide: class="profilepics" --> <div class="image"> <img src="https://i.imgur.com/m7xBGhd.jpg" width="300" height="300"> <h2>Leo Bernard</h2> <h3>github.com/leolabs</h3> </div> <div class="image"> <img src="https://i.imgur.com/WIwJk9U.jpg" width="300" height="300"> <h2>Marcus Weiner</h2> <h3>github.com/mraerino</h3> </div> <div class="image"> <img src="https://i.imgur.com/vE8DKi4.jpg" width="300" height="300"> <h2>Moritz Gunz</h2> <h3>github.com/NeoLegends</h3> </div> --- ### Nice to have - ES2015 - Redux knowledge - Test Assertions Note: Marcus --- # πŸ˜… ## Finding a way to do async things with Redux Note: - Reducers are synchronous - many async actions in a bigger webapp --- ### Cases - Data fetching - Data subscriptions - Retry, Cancel, Debounce - Rollbacks / Optimistic updates - Integrating async actions into the redux state machine --- # πŸŽ‰ ## Saga --- > [...] a Saga is like a separate thread in your application that's solely responsible for side effects. --- # πŸ‘Ύ ## Actor model --- # ☸️ ## What Can a Saga Do? --- - Await events - Interoperate with Redux - Manage other sagas --- # 🏭 ## Generators Note: Moritz - Wer kennt das? --- ```javascript function* generator() { /* ... */ } const iterator = generator() // => Object ``` ```javascript iterator.next() => { value: T | undefined, done: bool } ``` <!-- .element: class="fragment" --> ```javascript for (const item of iterator) { // calls iterator.next() console.log(item) } ``` <!-- .element: class="fragment" --> Note: - Generator returns iterator - Object to traverse a collection - works with for...of - function only runs on call to .next() --- ```javascript function* generator() { const num = 2 // Pass value to callee and pause execution yield 1 } ``` ```javascript const iterator = generator() ``` Note: - Yield -> resumable function with context - Try-catch / if-else, etc. - Push values into generator --- ```javascript function* generator() { const num = 2 // Pass value to callee and pause execution yield 1 } ``` ```javascript const iterator = generator() // Generator progress controlled from outside iterator.next().value == 1 ``` Note: - Yield -> resumable function with context - Try-catch / if-else, etc. - Push values into generator --- ```javascript function* generator() { const num = 2 // Pass value to callee and pause execution yield 1 // Callee can pass values into generator let a = yield 2 } ``` ```javascript const iterator = generator() // Generator progress controlled from outside iterator.next().value == 1 iterator.next().value == 2 ``` Note: - Yield -> resumable function with context - Try-catch / if-else, etc. - Push values into generator --- ```javascript function* generator() { const num = 2 // Pass value to callee and pause execution yield 1 // Callee can pass values into generator let a = yield 2 // `num` maintains value across yield points yield a + num } ``` ```javascript const iterator = generator() // Generator progress controlled from outside iterator.next().value == 1 iterator.next().value == 2 iterator.next(10).value == 12 ``` Note: - Yield -> resumable function with context - Try-catch / if-else, etc. - Push values into generator --- ```javascript function* generator() { const num = 2 // Pass value to callee and pause execution yield 1 // Callee can pass values into generator let a = yield 2 // `num` maintains value across yield points yield a + num } ``` ```javascript const iterator = generator() // Generator progress controlled from outside iterator.next().value == 1 iterator.next().value == 2 iterator.next(10).value == 12 iterator.next().done == true ``` Note: - Yield -> resumable function with context - Try-catch / if-else, etc. - Push values into generator --- # ✨ ## Effects Note: Leo - "Glue" between generators and saga library - Way to control sagas --- ```javascript function* saga() { yield put({ type: "SET_JOKE", payload: "What do you call a fake noodle? An impasta!", }) } ``` ```javascript saga().next() == { PUT: { action: { type: "SET_JOKE", payload: "What do you call a fake noodle? An impasta!", } } } ``` <!-- .element: class="fragment" --> Note: - Functions don't do anything themselves - Return an object containing instructions for saga - Saga receives instructions and acts on them --- # πŸ’» ## Example --- ```javascript function* fetchJoke() { const { joke } = yield call( fetchJson, "https://icanhazdadjoke.com/", ) yield put({ type: 'SET_JOKE', payload: joke }) } ``` ```javascript function* jokeThread() { while (true) { const action = yield take('GET_JOKE') yield fork(fetchJoke) } } ``` <!-- .element: class="fragment" --> Note: - Why infinite loop? - Alright, because function is non-blocking --- ## DEMO https://codesandbox.io/s/1q5856pww4 --- # 😍 ## Why is Saga so great? Note: Marcus --- ### Model the Whole Story of Your App **App Lifecycle** Login β†’ Interaction β†’ Logout β†’ Login ... ```javascript function* app() { while (true) { yield waitForlogin() const pid = yield fork(runDataFetching) yield waitForLogout() yield cancel(pid) } } ``` <!-- .element: class="fragment" --> --- # βœ… ## Testing our Saga Note: Moritz --- ```javascript it("should fetch a joke and store it in the state", () => { const jokeMock = { joke: "Chuck Norris knows the last digit of pi." } const it = fetchJoke() }) ``` --- ```javascript it("should fetch a joke and store it in the state", () => { const jokeMock = { joke: "Chuck Norris knows the last digit of pi." } const it = fetchJoke() it.next() // returns { CALL: { ... } } effect object }) ``` --- ```javascript it("should fetch a joke and store it in the state", () => { const jokeMock = { joke: "Chuck Norris knows the last digit of pi." } const it = fetchJoke() it.next() // returns { CALL: { ... } } effect object expect(it.next(jokeMock).value).toEqual(put({ type: "SET_JOKE", payload: jokeMock.joke, })) }) ``` --- ```javascript it("should fetch a joke and store it in the state", () => { const jokeMock = { joke: "Chuck Norris knows the last digit of pi." } const it = fetchJoke() it.next() // returns { CALL: { ... } } effect object expect(it.next(jokeMock).value).toEqual(put({ type: "SET_JOKE", payload: jokeMock.joke, })) expect(it.next()) .toEqual({ done: true, value: undefined }) }) ``` --- # πŸ’‘ ## Advanced use cases Note: Leo --- #### Debounce ```javascript function* handleInput({ payload }) { yield delay(500) const res = yield call( fetch, "https://myAPI", { body: payload }, ) } function* watchInput() { yield takeLatest('INPUT_CHANGED', handleInput) } ``` Note: - Great for input fields that trigger searches - Limit the amount of requests --- #### Retries ```javascript function* request() { while (true) { const res = yield call(fetch, "https://myAPI") if (res) { put(...) return } yield delay(5000) } } ``` Note: - Good for flaky connections --- # 🏁 ## Conclusion --- - Actor Model for the Frontend<!-- .element: class="fragment" --> - Declarative 😍<!-- .element: class="fragment" --> - Sagas are pure (Runtime)<!-- .element: class="fragment" --> - Enables sequential running of effects<!-- .element: class="fragment" --> - Easy error handling<!-- .element: class="fragment" --> --- ![Festify Logo](https://d33wubrfki0l68.cloudfront.net/1967c0923ec2039f845c917dae55fa32f0b73e74/ca447/img/festify-logo.svg) ## [github.com/Festify/app](https://github.com/Festify/app) Note: Leo --- # ❀️ ## Thank you! --- ## Saga "Without" Redux ```javascript const io = { dispatch(action) => /* saga dispatches an action */, getState() => /* saga requests app state */, subscribe(callback) => /* on action: callback(action) */, } runSaga(io, saga) ```
{"metaMigratedAt":"2023-06-14T18:42:25.653Z","metaMigratedFrom":"YAML","title":"Redux Saga","breaks":true,"slideOptions":"{\"transition\":\"fade\",\"controls\":false}","contributors":"[{\"id\":\"53c655a0-3881-4259-afc8-68b5fcfce041\",\"add\":9093,\"del\":57}]"}
    613 views