Reuse saga logic - redux

We have a general Form component, whith an accompanying saga to handle validation and submission
function* validateAndSubmit(action) {
const errors = clientSideValidate(action.values);
if (errors) {
return yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: errors});
}
try {
const response = yield call(submitToTargetUrl(action.values, action.url));
if (response.errors) {
return yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: response.errors});
}
yield put({type: SUBMIT_SUCCESS, formKey: action.formKey});
} catch (e) {
yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: [e.message]});
}
}
function* form() {
yield takeEvery(SUBMITTED, validateAndSubmit);
}
Now, we have another component, say UserForm that wraps the general Form component. On submission, we want to Submit the form to the backend and fetch some data from an external API concurrently, wait for both to complete, and then dispatch some action. This logic will live in another Saga in some other file. What would be the right pattern for reusing the validateAndSubmit logic? Is there some way to do this:
function* handleUserFormSubmit(action) {
const [submitResult, fetchResult] = yield all([
call(validateAndSubmitSaga),
call(fetchOtherData),
]);
// ...test for successful results for both
if (allIsGood) {
yield put({type: ALL_IS_GOOD});
}
}
function* userForm() {
yield takeEvery(USER_FORM_SUBMITTED, handleUserFormSubmit);
}
Thanks!

I would suggest to create a reusable validateAndSubmit function that would handle the validation and submission, then would return an error if any. And then, have a form submit saga effect that uses this function.
async function reusableValidateAndSubmit(formValues, submitUrl) {
try {
const errors = clientSideValidate(formValues);
if (errors) {
return errors;
}
const response = await submitToTargetUrl(formValues, submitUrl);
if (response.errors) {
return response.errors;
}
return null;
} catch (e) {
console.error('#reusableValidateAndSubmit: ', e);
return [e.message];
}
}
function* handleFormSubmitSaga(action) {
try {
const { values, url, formKey } = action;
const errors = yield call(reusableValidateAndSubmit, values, url);
if (errors) {
return yield put({type: SUBMIT_FAILED, formKey: formKey, errors: errors});
}
return yield put({type: SUBMIT_SUCCESS, formKey: formKey});
} catch (e) {
return yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: [e.message]});
}
}
function* form() {
yield takeEvery(SUBMITTED, handleFormSubmitSaga);
}
For the handleUserFormSubmit, I'm not just quite sure if in your use case, you want for the validateAndSubmitSaga to fail if fetchOtherData fails, and vice versa. Using redux-saga's all() would bring this effect as it behaves like Promise.all().
A snippet on the return value of Promise.all() from MDN:
This returned promise is then resolved/rejected asynchronously (as soon as the stack is empty) when all the promises in the given iterable have resolved, or if any of the promises reject.
Supposedly, it is your expected behavior, and having implemented the code above. You could just reuse reusableValidateAndSubmit function
function* handleUserFormSubmit(action) {
const [submitError, fetchResult] = yield all([
call(reusableValidateAndSubmit, action.values, action.url),
call(fetchOtherData),
]);
// ...test for successful results for both
// submitError is null if submit was a success
// fetchResult must have a value or return true if was a success
if (!submitError && fetchResult) {
yield put({type: ALL_IS_GOOD});
}
}
May I also suggest having a look at some form frameworks that you could partner with redux (i.e. redux-form) as they could also help in some use cases.

We ended up with a slightly different solution. Instead of call-ing we're take-ing:
function* handleUserFormSubmit(action) {
const [submitResult, fetchResult] = yield all([
yield take(SUBMIT_SUCCESS),
yield take(FETCH_OTHER_DATA_SUCCESS),
]);
// ...test for successful results for both
if (allIsGood) {
yield put({type: ALL_IS_GOOD});
}
}
function* userForm() {
yield takeEvery(USER_FORM_SUBMITTED, handleUserFormSubmit);
}
This way the other Saga can do its thing undisturbed, an this saga can React according to its own logic

Related

Redux Saga socket.io

I'm currently developing a chat client which have to be able to receive messages and to send messages. The only problem I'm facing to is that I really don't know how to send messages inside the given component from the Saga example.
I found the example on their API documentation https://redux-saga.js.org/docs/advanced/Channels.html.
Am I be able to reuse the socket const I created inside the watchSocketChannel function? Or do I need to just create the connection twice? What would you advise to do?
import {all, apply, call, fork, put, take} from 'redux-saga/effects'
import {eventChannel} from 'redux-saga';
import * as actions from "../actions";
import io from 'socket.io-client';
function createSocketConnection(url, namespace) {
return io(url + '/' + namespace);
}
function createSocketChannel(socket) {
return eventChannel(emit => {
const eventHandler = (event) => {
emit(event.payload);
};
const errorHandler = (errorEvent) => {
emit(new Error(errorEvent.reason));
};
socket.on('message', eventHandler);
socket.on('error', errorHandler);
const unsubscribe = () => {
socket.off('message', eventHandler);
};
return unsubscribe;
});
}
function* emitResponse(socket) {
yield apply(socket, socket.emit, ['message received']);
}
function* writeSocket(socket) {
while (true) {
const { eventName, payload } = yield take(actions.WEBSOCKET_SEND);
socket.emit(eventName, payload);
}
}
function* watchSocketChannel() {
const socket = yield call(createSocketConnection, 'http://localhost:3000', 'terminal');
const socketChannel = yield call(createSocketChannel, socket);
console.log(socket);
while (true) {
try {
const payload = yield take(socketChannel);
yield put({type: actions.WEBSOCKET_MESSAGE, payload});
yield fork(emitResponse, socket);
} catch (err) {
console.log('socket error: ', err);
}
}
}
export default function* root() {
yield all([
fork(watchSocketChannel),
])
I know that the fork function is attaching the watchSocketChannel function to saga and constantly listening.
I'm not sure I have understood correctly your question... If you're asking how/where to fork the writeSocket saga to allow you to dispatch the actions.WEBSOCKET_SEND) action and have your message sent to the socket:
isn't sufficient to do add a fork in the middle of the socket channel creation?
const socket = yield call(createSocketConnection, 'http://localhost:3000', 'terminal');
fork(writeSocket, socket); // I've added this line
const socketChannel = yield call(createSocketChannel, socket);

redux saga yield call throw error TypeError: Cannot read property 'ready' of null

I have app which loads 'favorites' each time user logs in. Data are loaded from IndexedDB using localforage. It works perfectly when app is starting fresh (after window refresh). When I logout and login (root saga task running) call that loads 'favorites' data throws error:
TypeError: Cannot read property 'ready' of null
at step:
export function* handleRecoverFavorites() {
try {
const resp = yield call(localforage.getItem, 'favorites')
saga code extract:
export function* handleRecoverFavorites() {
try {
const resp = yield call(localforage.getItem, 'favorites')
if(resp) {
yield put(recoverFavorites(resp))
yield all(resp.map(symbol => put(getTickPricesSubscribe(symbol))));
}
} catch(err) {
let response={"errorDescr":"Error whilst recovering favorites: "+err}
yield put({ type: types.RESPONSE_ERR, response })
console.log(response.errorDescr)
}
}
function* runAtStartupSubscirptions(socket, streamSessionId) {
yield fork(send, {"command": "getBalance", "streamSessionId": streamSessionId }, socket );
yield fork(handleRecoverFavorites)
yield fork(handleRecoverCharts)
while(true) {
yield call(delay, STREAMING_PING_TIME)
yield call(send, {"command": "ping", "streamSessionId": streamSessionId }, socket );
}
}
function* handleRequests(socket) {
let streamSessionId = yield select(state => state.get("auth").get("streamSessionId"))
while(true) {
yield take(types.STREAMING_SOCKET_STATUS)
if(socket.readyState === 1)
yield fork(runAtStartupSubscirptions, socket, streamSessionId)
}
}
export function* handleStreamingConnection() {
let server = yield select(state => state.get("auth").get("server"))
const socket = yield call(createWebSocketConnection, server+"Stream" )
const socketChannel = yield call(createSocketChannel, socket, null)
const task = yield fork(handleRequests, socket)
let channelMsg;
do {
channelMsg = yield take(socketChannel)
if(channelMsg.socketResponse) {
const response = channelMsg.socketResponse;
switch (response.command) {
case "candle":
yield put({ type: types.GET_CANDLES_STREAMING, response })
break;
(...)
default:
console.log("unrequested data: "+response)
}
}
if(channelMsg.socketStatus) {
console.log(channelMsg)
yield put({ type: types.STREAMING_SOCKET_STATUS, channelMsg })
}
} while (channelMsg.socketStatus!=="Disconnected")
yield cancel(task)
}
export default function* rootSaga() {
while(true) {
// Wait for log-in
yield take(types.LOGIN_SUCCESS);
const handleStreamingtask = yield fork(handleStreamingConnection)
yield take([types.LOGOUT_REQUEST, types.RECONNECTING ])
yield cancel(handleStreamingtask)
const channelMsg={"socketStatus" : "Disconnected"}
yield put({ type: types.STREAMING_SOCKET_STATUS, channelMsg })
}
}
I will be appreciated for any suggestions.
I have added console.log line to see result without saga call:
export function* handleRecoverFavorites() {
try {
console.log(localforage.getItem('favorites'))
const resp = yield call(localforage.getItem, 'favorites')
it returns every time Promise with correct value:
Promise {<pending>}
__proto__:Promise
[[PromiseStatus]]:"resolved"
[[PromiseValue]]:Array(1)
0:"CHFPLN"
length:1
This is how I solved it, instead of direct call of localforage method inside "yield call", I have wraped it into separate class:
export class LocalDataService {
getData = ( param ) => {
return localforage.getItem(param)
.then( result => result)
.catch( reason => reason)
}
}
export function* handleRecoverFavorites() {
try {
const api = new LocalDataService()
const resp = yield call( api.getData, 'favorites')
(...)
As per Sylwek reply, and investigating a bit further, the issue relies on the internal setItem context not binding as we expected.
By calling localforage with the following form, we can avoid creating a class:
yield call([localforage, localforage.getItem], 'favorites')
This fixed it for me :)
Check the this context section in saga docs: https://redux-saga.js.org/docs/basics/DeclarativeEffects/

Redux Saga - dispatching action from call function

Is there a way in redux saga to dispatch an action from a function that is being called by call effect to the global store?
for example:
export function* login({ payload }) {
try {
// from API.post I want to be able to dispatch an action that I will handle in a different saga.
const resp = yield call(API.post, 'api/1/login', {
email: payload.email,
password: payload.password,
});
yield put(AuthActions.loginSuccess(resp.token));
} catch (error) {
yield put(AuthActions.loginError(error));
}
}
I know that I can use saga's channel, but if I pass channel to the API.post I must use take(channel) and I want to handle this action in a different file so I dont have access to this channel. And I also not sure I want to export the channel and import it in a different file cause I want each saga to be independent.
What can I do?
One possibility is to create a wrapper function for API calls. Like this:
// `apiRequest` dispatches `anotherAction` and calls given `apiCall`
function* apiRequest(apiCall, ...args) {
yield put(anotherAction())
yield call(apiCall, ...arg)
}
export function* login({ payload }) {
try {
const resp = yield apiRequest(API.post, 'api/1/login', {
email: payload.email,
password: payload.password,
})
yield put(AuthActions.loginSuccess(resp.token))
} catch (error) {
yield put(AuthActions.loginError(error))
}
}

How to chain async actions redux saga (fetch of XHR requests)

Is there some usual pattern to chain redux-saga async requests synchronously? Eg. 1 function loads user ID and the second call some API request using that ID. I will try to demonstrate (this code isn't solution, just demonstration)
function* laodUserSaga(action) {
try {
const res = yield apiGet('/user')
const onboardingData = yield res.json()
yield put.resolve(loadUserSuccess(camelizeKeys(onboardingData)))
} catch (error) {
yield put.resolve(loadUserError(error))
}
}
function* loadProfileDataByUserID(action) {
const state = yield select();
try {
const res = yield apiGet(`/user/${state.userID}user-profile`)
const onboardingData = yield res.json()
yield put.resolve(loadUserSuccess(camelizeKeys(onboardingData)))
} catch (error) {
yield put.resolve(loadUserError(error))
}
}
function* loadProfileWithDataSaga(aciton){
yield put(laodUserSaga)
yield put(loadProfileDataByUserID)
}
function* sagaConnect() {
yield all([
takeLatest(LOAD_USER, laodUserSaga),
takeLatest(LOAD_USER_DATA_BY_PROFILE_ID, loadProfileDataByUserID),
takeLatest(LOAD_USER_WITH_PROFILE, loadProfileWithDataSaga),
])
}
you see, such examples would be really useful in docs of every library, 1 real world example is often what I understand in seconds, am I the only one?
You don't have to start every saga using takeEvery/Latest helper.
You can just call sagas as any other function, e.g.:
function* loadProfileWithDataSaga() {
const user = yield call(loadUser)
const profileData = yield call(loadProfileDataByUserID, user.id)
}
You just need to write loadUser and loadProfileDataByUserID sagas so that they receive and return the right values. If you want to be still able to call them in other scenarios by dispatching actions I would create another sagas that would just wrap the loadUser/Profile functionality.

On a saga context, how do I handle reducer errors?

I have a saga:
function* mySaga() {
try {
yield put({type: 'ACTION_TYPE'})
} catch (e) {
handleError(e)
}
}
If the reducer that handles ACTION_TYPE throws an error, the catch block won't run. At least not within my tests.
I have set up a Redux middleware to catch all errors and then dispatch an action that can update the store accordingly.
const errorHandlerMiddleware = store => next => action => {
try {
return next(action)
} catch (err) {
return next({
type: 'START_ERROR_STATE',
err: err
})
}
}
But if I have another saga that puts two sequential actions:
function* anotherSaga() {
try {
yield put({type: 'ACTION_TYPE'})
yield put({type: 'ANOTHER_ACTION_TYPE'})
} catch (e) {
handleError(e)
}
}
Even if the reducer that handles the action of type ACTION_TYPE throws an error, anotherSaga will still dispatch ANOTHER_ACTION_TYPE. I don't want this to happen.
I have implemented a race:
function* raceSaga() {
const { error} = yield race({
main: call(anotherSaga),
error: take('START_ERROR_STATE')
})
if (main) {
yield put({type: 'SUCCESS_ACTION'})
} else if (error) {
yield put({type: 'ERROR_ACTION'})
}
}
I'm not sure if it's the best approach.
There is something wrong with your redux setup. Citing redux documentation:
The reducer is a pure function that takes the previous state and an action, and returns the next state
Which means that you can't throw errors inside reducers. It should take state, action object and return you next state. I suggest that you revisit redux documantation or some tutorial.

Resources