Speaking side effects fluently with redux saga
Imagine a frontend that runs smooth as butter and speaks logic like poetry. This is possible when we define our side effects in a concurrent expressive framework like Redux-Saga. It can help you surf through unpredictable side effects on frontend with ease.
I’ve noticed many frontend developers don’t understand what redux saga is for, what it unlocks. When it is more than just redux middleware or as an replacement for redux thunk, while in fact these two have nothing in common and you can use both in synergy.
So in this story I’ll talk about concept of saga, importance of expressiveness in a framework, magic of function generators, concurrency, first class decoupling UI and domain.
What Are Sagas, Really?
Saga is an old concept that takes us back to time to the database design and transactions. A transaction in an database means that any database action should be easy to verify, cancel or rollback. This is very important need in an database because we don’t want our database in a bricked non operational state.
Redux saga brings the same concept into frontend side effects with the help of function generators and tells us think of every side effect like an transaction. A concept can suddenly make spaghetti of side effects in an frontend in to very manageable pure functional frontend.
Regular functions in javascript are not transactions, once a function is called you cannot pause or cancel its execution, but in the realm of function generators you can create such kind of functionality.
Magic of function generators in Spaghetti of messy side effects
A user creates a side effect when he uploads his picture and upon successful completion we need to redirect him to a new page. In an ideal world the user is patient and waits for the uploads process to finish before he takes another action.
But in real world, his picture could be 20mb in size, he’ll get bored of waiting and move on to a different page while picture uploads in the background in an event loop. And in the middle of user doing something important like payment the picture finishes upload and redirects to photo success page.
An side effect like this very easy to program if the side effect is easy to define in an expressive way.
1 User starts upload starts upload side effect
2 Let user go off the upload module
3 If user is in critical workflow wait for this transaction to finish
4 When user is not waiting in critical transaction redirect to success pageWhile this just a simple pseudo code, implementing a flow similar to this in a sane way is impossible if the side effects effects are not transactions.
But this is something that is very easy to implement with just few lines in an expressive event driven framework like redux saga.
function* smartUploadFlow() {
yield takeEvery('UPLOAD_REQUESTED', function* (action) {
try {
// Start the upload process in background
const uploadTask = yield fork(performUpload, action.payload.file);
// Track user state while upload happens
let userInCriticalFlow = false;
while (true) {
const { uploadSuccess, enterCritical, exitCritical, uploadError } = yield race({
uploadSuccess: take('UPLOAD_SUCCESS'),
enterCritical: take('ENTER_CRITICAL_FLOW'),
exitCritical: take('EXIT_CRITICAL_FLOW'),
uploadError: take('UPLOAD_FAILURE')
});
if (enterCritical) {
userInCriticalFlow = true;
continue;
}
if (exitCritical) {
userInCriticalFlow = false;
continue;
}
if (uploadError) {
yield put({ type: 'SHOW_UPLOAD_ERROR', payload: uploadError.error });
break;
}
if (uploadSuccess && !userInCriticalFlow) {
// Safe to redirect - user not in critical flow
yield put({ type: 'NAVIGATE_TO_SUCCESS' });
break;
} else if (uploadSuccess && userInCriticalFlow) {
// Upload done but user busy - wait for them to finish
yield take('EXIT_CRITICAL_FLOW');
yield put({ type: 'NAVIGATE_TO_SUCCESS' });
break;
}
}
} catch (error) {
yield put({ type: 'UPLOAD_SAGA_ERROR', payload: error });
}
});
}
function* performUpload(file) {
try {
const result = yield call(uploadAPI, file);
yield put({ type: 'UPLOAD_SUCCESS', payload: result });
} catch (error) {
yield put({ type: 'UPLOAD_FAILURE', payload: { error } });
}
}Notice how this reads almost exactly like our requirements! The code becomes self-documenting, and complex async coordination becomes as readable as a recipe.
Understanding the Concurrency and Coordinator Pattern
One of Redux-Saga's most underappreciated features is how it handles concurrency through what's essentially a coordinator pattern. While JavaScript runs on a single thread, modern browsers are incredibly sophisticated at managing multiple concurrent operations - network requests, timers, user interactions, and background tasks all happen "simultaneously" from the user's perspective.
Redux-Saga leverages this by acting as an intelligent coordinator that can:
Fork multiple sagas concurrently - Your upload saga can run alongside a user navigation saga and a payment validation saga
Race conditions with grace - Use
race()to handle "first wins" scenarios, like user cancellation vs. upload completionBackground task management - Tasks can continue running while users navigate, but be intelligently paused or cancelled when needed
Here's how your upload example might look:
function* uploadWithSmartRedirect() {
const uploadTask = yield fork(uploadImage);
while (true) {
const { userNavigation, uploadComplete, userInCriticalFlow } = yield race({
userNavigation: take('USER_NAVIGATED'),
uploadComplete: take('UPLOAD_SUCCESS'),
userInCriticalFlow: take('ENTERING_CRITICAL_FLOW')
});
if (userInCriticalFlow) {
yield take('EXITING_CRITICAL_FLOW');
continue;
}
if (uploadComplete && !userInCriticalFlow) {
yield put(navigateTo('/upload-success'));
break;
}
}
}The browser's event loop handles the actual concurrency, but Saga gives you the tools to orchestrate these concurrent operations in a readable, maintainable way.
Expressive Poetry of Declarative Side Effects
What makes Redux-Saga feel like "logic poetry" is its declarative and expressive nature. You describe what should happen and when, rather than imperatively managing how it happens. Effects like call(), put(), take(), race(), and fork() become the vocabulary of a domain-specific language for side effect management.
Your pseudo-code example translates almost directly:
function* smartUploadSaga() {
// 1. User starts upload
yield takeEvery('UPLOAD_REQUESTED', function* (action) {
// 2. Let user go off the upload module (fork allows this)
const uploadTask = yield fork(performUpload, action.payload);
// 3. If user is in critical workflow, wait for transaction to finish
const { complete, criticalFlow } = yield race({
complete: take('UPLOAD_SUCCESS'),
criticalFlow: take('ENTERING_CRITICAL_WORKFLOW')
});
if (criticalFlow) {
yield take('EXITING_CRITICAL_WORKFLOW');
yield take('UPLOAD_SUCCESS');
}
// 4. When user is not in critical transaction, redirect
yield put(navigateTo('/upload-success'));
});
}This reads like a specification document, yet it's executable code. The expressiveness comes from how closely the code maps to your mental model of the problem.
Decoupling UI from Domain in its finest form
Perhaps the most transformative aspect of Redux-Saga is how it enables true separation of concerns. Your React components become pure presentational layers that simply dispatch actions and render state - they don't need to know about API calls, complex async flows, or business logic timing.
Consider a typical component without Saga:
// Tightly coupled, hard to test, business logic mixed with UI
function UploadComponent() {
const [uploading, setUploading] = useState(false);
const [error, setError] = useState(null);
const handleUpload = async (file) => {
setUploading(true);
try {
await uploadAPI(file);
if (userNotInCriticalFlow()) {
navigate('/success');
} else {
// Wait for critical flow to complete...
// This gets messy quickly
}
} catch (err) {
setError(err);
} finally {
setUploading(false);
}
};
return (
<div>
<FileUpload onUpload={handleUpload} />
{uploading && <Spinner />}
{error && <ErrorMessage error={error} />}
</div>
);
}With Redux-Saga, the same component becomes:
// Pure, testable, focused only on presentation
function UploadComponent() {
const { uploading, error } = useSelector(state => state.upload);
const dispatch = useDispatch();
const handleUpload = (file) => {
dispatch({ type: 'UPLOAD_REQUESTED', payload: file });
};
return (
<div>
<FileUpload onUpload={handleUpload} />
{uploading && <Spinner />}
{error && <ErrorMessage error={error} />}
</div>
);
}All the complex business logic lives in your sagas, which are:
Easily testable - You can step through generator functions and test each yield
Reusable - The same upload saga can be triggered from multiple components
Maintainable - Changes to business logic don't require touching UI components
All of this without a single dependency of domain function.
So now you know why what redux-sage actually unlocks for you in your frontend. expressive transactional side effects, intelligent concurrency coordination, and clean UI/logic separation - work together to create applications that feel both robust and fluid. Your frontend flows with the natural rhythms of user behavior and system capabilities.
Once you experience the smooth butter of Redux-Saga side effects, you never go back to traditional traditional async handling

