Although making synchronisation easy to use is one of the primary goal of this library, there are a few things that are important to understand to avoid common pitfalls.
In this guide we'll implement a data synchronisation saga that supports common scenarios such as:
We'll iterate over several implementations to get to a complete solution.
This is the simplest way to synchronize data:
function * rootSaga () {
yield fork(
rsf.database.sync,
'todos',
{ successActionCreator: setTodos }
)
}
Or, with firestore:
function * rootSaga () {
yield fork(
rsf.firestore.syncCollection,
'todos',
{ successActionCreator: setTodos }
)
}
In the above, when rootSaga
starts, it starts an attached fork which runs rsf.database.sync
"in the background".
This means that rootSaga
isn't blocked by the yield fork(...)
line and, when it stops, so does rsf.database.sync
.
Since rootSaga
is redux-saga's root saga it will never stop and so, in the above example, the synchronization process will never stop.
But what if I need to pause the sync process and restart it later?
Now let's assume we have two actions being dispatched by another part of the system (ie. the user pressing buttons): RESUME_SYNC
and PAUSE_SYNC
.
First, once we have a sync saga running, how can we take PAUSE_SYNC
into account to pause the sync process?
Let's list what needs to happen in the root saga:
PAUSE_SYNC
actionTo implement this we can use tasks. Task objects represent running sagas and we can use them to stop a saga running the background.
Using the take
effect creator to wait for the pause action and the cancel
effect creator to cancel the running task we get:
function * rootSaga () {
// Start the sync saga
const task = yield fork(
rsf.database.sync,
'todos',
{ successActionCreator: setTodos }
)
// Wait for the pause action
yield take('PAUSE_SYNC')
// Stop the sync saga
yield cancel(task)
}
Now what about restarting? We need to:
PAUSE_SYNC
actionRESUME_SYNC
actionOr, to be able to pause/resume sync as many times as we want:
infinite loop:
PAUSE_SYNC
actionRESUME_SYNC
actionSo we end up with:
function * rootSaga () {
while (true) {
// Start the sync saga
let task = yield fork(
rsf.database.sync,
'todos',
{ successActionCreator: setTodos }
)
// Wait for the pause action, then stop sync
yield take('PAUSE_SYNC')
yield cancel(task)
// Wait for the resume action
yield take('RESUME_SYNC')
}
}
That's a perfectly fine saga, but what if we want to sync user-specific data (ie. not always at todos
)? 🤔
Let's now synchronize our user's notifications stored in notifications/:userId
.
The actions we're going to listen for are slightly different: LOGIN
and LOGOUT
instead of SYNC_RESUME
and SYNC_PAUSE
, but they essentially serve the same purpose.
We also won't start the sync process automatically when the application starts anymore.
We only need a couple changes to get this to work:
function * rootSaga () {
while (true) {
// Wait for a user to login
const loginAction = yield take('LOGIN')
// Start the sync saga
let task = yield fork(
rsf.database.sync,
`notifications/${loginAction.userId}`,
{ successActionCreator: syncNotifications }
)
// Wait for the logout action, then stop sync
yield take('LOGOUT')
yield cancel(task)
}
}
So what changed?
RESUME_SYNC
/LOGIN
was moved to the start of loop to make sure we don't automatically start syncing when the app startssync
ed path (notifications/${loginAction.userId}
) now depends on the content of the action that's returned by yield take('LOGIN')
And that's it!
This should work as expected when users log in and out, however we've kinda been re-inventing the wheel here, let's see how we can refactor this.
Let's start by extracting our code to a separate saga:
function * rootSaga () {
+ yield fork(syncSaga)
+ // other stuff
+ }
+
+ function * syncSaga () {
while (true) {
// Wait for a user to login
const loginAction = yield take('LOGIN')
// Start the sync saga
let task = yield fork(
rsf.database.sync,
`notifications/${loginAction.userId}`,
{ successActionCreator: syncNotifications }
)
// Wait for the logout action, then stop sync
yield take('LOGOUT')
yield cancel(task)
}
}
It's a small change but it allows us to start several sagas from rootSaga
and to move syncSaga
to another file if necessary.
Then let's rewrite our code using takeLatest
.
Essentially, takeLatest
starts a saga whenever an action is dispatched, so we could use it to start syncSaga
whenever LOGIN
is dispatched and get rid of this distasteful while(true)
loop:
function * rootSaga () {
yield takeLatest('LOGIN', syncSaga)
// other stuff
}
function * syncSaga (action) {
// Start the sync saga
let task = yield fork(
rsf.database.sync,
`notifications/${action.userId}`,
{ successActionCreator: syncNotifications }
)
// Wait for the logout action, then stop sync
yield take('LOGOUT')
yield cancel(task)
}
No need to wait for LOGIN
manually anymore, and no need to think about this infinite loop either!
Learnings:
fork
ed sagas run "in the background" and aren't blockingtakeEvery
, takeLatest
) save time and prevent headaches, use them!For more details, check out issue #92.