August 11, 2020Handling promises in Sagas
August 11, 2020Modern Frontend Architecture Part 1: API
May 5, 2020Frontend Pattern: Route Object
May 1, 2020Frontend skills for the Web Designer
April 10, 2020Learning Frontend Path
January 26, 2020Podcasts I listen to (Jan 2020)
January 23, 2020Are you too old to start programming
January 22, 2020Use tabs not spaces
October 18, 2019Refactoring Overgrown React Components slides
October 2, 2019Book Review: Factfulness
October 1, 2019Yes, it's a good time to add TypeScript to your project
September 24, 2019Code Review – Best Practices, Guidelines & Process Insights
June 10, 2019Why I read on the iPad
March 18, 2019Software & Hardware I use
March 16, 2019Why I dont use React Router Link component
November 15, 2018Why public money software is not open source?
July 5, 2018How learning Angular made me better React developer
May 23, 2018My 12 tips how to increase your frontend coding productivity

Handling promises in Sagas

I'm a huge fan of sagas, but I'm aware for some cases they are too strict - similar to CQRS, they can't respond to an action about success/failure. I think redux-thunk is not a scalable choice, but it has a major benefit - for some cases you really need to know about the result in the component. With sagas only way is to set an extra store entry to inform component about the change... Or is it?

In general I think it's good that components just inform (dispatch) about some domain event (FORM_SUBMITTED, DELETE_REQUESTED etc), and Sagas work as 'business threads' responsible for any business logic.

However this approach has flaws - especially forms, which can be submitted and fail, because of e.g. backend side validation. In this scenario we can't redirect to another page (success scenario), but we need to stay at the same page, informing component about the status. Component has to somehow get informed about the errors and display them.

Initial approaches

Set errors to Redux

The most obvious one is to set errors in Redux store. Saga will dispatch some FORM_SUBMIT_FAILED event with errors and payload, reducers will save them, selectors retrieve them and component connected to the store will get re-rendered.

I don't like this approach, because it bloats the store, adds ton of boilerplate, adds complexity because we need to clear the errors etc. Let's move on

Inform by router

I use connected-react-router and I believe router should be definitely handled in Sagas.

Technically we can put some router action and pass errors visible way (/form?errors=[email, Email+Invalid])) or something like that or more elegant way via location state. However I don't like this approach too - it adds extra state layer to the app.

Context

This gets interesting. Technically it's tempting approach to create some withFormErrorsProvider HOC (or whatever you like to implement context). But how it will get erros from saga, without saving them to the store? We can experiment with saga context (kind of dependency injection), here are some hints about it. You can set context dynamically, calling saga from HOC. This is an interesting concept, but I have concerns about strong typing it.

Mixing with thunk

Another interesting concept is to use thunk as a command handler and sagas as event handler. So thunk will react on FORM_SUBMITTED and asynchronously emit FORM_SUBMIT_FAILED and FORM_SUBMIT_SUCCEEDED, they will also return the promise resolve or reject to the component. At the same time saga can react on each of these actions. This is very interesting approach and I will experiment with it in the future, expecially with Redux-Toolkit. The concerns I have is to somehow strong define what is an command and what is an event - could be problematic for people not familiar with Event Driven Architecture.

Current solution - the Async Meta

The standard Redux action can contain payload and meta, implementing {type: string, payload?: any, meta?: any}.

I use typesafe-actions at the moment, so I will use them as an example.

type AsyncMeta<Error extends any> = {
	onSuccess?: () => void;
	onError?: (error: Error) => void;
};

const formSubmitted = createAction('FORM_SUBMITTED')<
	[FormPayload, AsyncMeta]
>();

// Inside component
const onSubmit = (formValues: FormPayload) =>
	dispatch(
		formSubmitted(formValues, {
			onError: (errors) => {
				setErrors(errors);
				setSubmitting(false);
			}
		})
	);

// Saga
function* handleFormSubmit(action: ReturnType<typeof formSubmitted>) {
	const { onError, onSuccess } = action.meta;

	try {
		// do some api calls
		onSuccess && onSuccess();
	} catch (e) {
		onError && onError(e);
	}
}

First, I declare the AsyncMeta type with 2 possible callbacks, while onError can get emails (should?) and onSuccess is almost never used (saga is handling success scenario on its own).

Then, I create an action which can get payload and meta with these callbacks

In the component I can dispatch an action and pass callbacks, which - if needed - will update local state (errors, submitting)

In the end, saga is listening on the event and - when saga is finished - it calls error or success callbacks (along with actions and other things).

Summary

This is how I handle it at the moment. I think it's a sweet spot between saga-based architecture and elegant, boilerplate-less solution.