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))
}
}
Related
Lets say we have
function* saga1() {
yield call(saga2)
}
function* saga2() {
debugger;
}
Whats the best way to visualize how sagas are setup/structured.
Let's say I have a modal with a button that dispatches an action if you click on it. And I'd like to know the result of the action dispatched: e.g. if it was successful I'll close the modal and to something different otherwise.
With redux-thunk my action would look something like:
export const deleteObjects = () => {
return (dispatch, getState) => {
try {
...
dispatch(setObjects([]));
return true
} catch (e) {
return false
}
}
};
so I could use the result in my component. But how to do the same with redux-sagas? As far as I know, you can use sagas with watchers.
One solution I could think of is to pass a callback function to the action creator and call it inside of saga. Like this:
export const deleteObjects = (callback) => ({
type: DELETE_OBJECTS,
callback
});
export function* deleteObjectsAsync({callback}) {
try {
...
put(setObjects([]))
yield call(callback, true)
} catch (err) {
yield call(callback, false)
}
}
function* watchGetPlaces() {
yield takeEvery(DELETE_OBJECTS, deleteObjectsAsync)
}
Is this a valid solution or there is a more adequate way to do so?
I do not recommend your proposed solution. Passing callback functions is one of the precise things redux-saga tries to prevent the developer to have to deal with.
One clean solution is to wrap your modal closing functionality into its own saga that can be invoked by an action. I'm not sure how you open your modals, but on our apps we dispatch actions to open and close modals. Doing this enables connected components and sagas can manipulate any modal. Sagas are designed to handle side effects. Closing a modal is a side effect. Therefore, a saga is a perfect place to put closing modal logic.
Check out this boilerplate:
export const closeModal = () => ({
type: CLOSE_MODAL,
});
function* onCloseModal() {
// Your logic for closing modals goes here
}
function* closeModalSaga() {
yield takeEvery(CLOSE_MODAL, onCloseModal)
}
export const deleteObjects = () => ({
type: DELETE_OBJECTS,
});
export function* deleteObjectsAsync() {
try {
...
yield put(setObjects([]))
yield put(closeModal());
} catch (err) {
// Your "do-something-else" logic here
// I'd recommend dispatching another action
}
}
function* watchGetPlaces() {
yield takeEvery(DELETE_OBJECTS, deleteObjectsAsync)
}
How to handle firebase auth state observer in redux saga?
firebase.auth().onAuthStateChanged((user) => {
});
I want to run APP_START saga when my app starts which will run firebase.auth().onAuthStateChanged observer and will run other sagas depending on the callback.
As I understand eventChannel is right way to do it. But I don't understand how to make it work with firebase.auth().onAuthStateChanged.
Can someone show how to put firebase.auth().onAuthStateChanged in to eventChannel?
You can use eventChannel. Here is an example code:
function getAuthChannel() {
if (!this.authChannel) {
this.authChannel = eventChannel(emit => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => emit({ user }));
return unsubscribe;
});
}
return this.authChannel;
}
function* watchForFirebaseAuth() {
...
// This is where you wait for a callback from firebase
const channel = yield call(getAuthChannel);
const result = yield take(channel);
// result is what you pass to the emit function. In this case, it's an object like { user: { name: 'xyz' } }
...
}
When you are done, you can close the channel using this.authChannel.close().
Create your own function onAuthStateChanged() that will return a Promise
function onAuthStateChanged() {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
resolve(user);
} else {
reject(new Error('Ops!'));
}
});
});
}
Then use call method to get the user synchronously
const user = yield call(onAuthStateChanged);
This could be handled in the Saga such as the following for Redux Saga Firebase:
// Redux Saga: Firebase Auth Channel
export function* firebaseAuthChannelSaga() {
try {
// Auth Channel (Events Emit On Login And Logout)
const authChannel = yield call(reduxSagaFirebase.auth.channel);
while (true) {
const { user } = yield take(authChannel);
// Check If User Exists
if (user) {
// Redux: Login Success
yield put(loginSuccess(user));
}
else {
// Redux: Logout Success
yield put(logoutSuccess());
}
}
}
catch (error) {
console.log(error);
}
};
here is how you would run the onAuthStateChanged observable using redux-saga features (mainly eventChannel)
import { eventChannel } from "redux-saga";
import { take, call } from "redux-saga/effects";
const authStateChannel = function () {
return eventChannel((emit) => {
const unsubscribe = firebase.auth().onAuthStateChanged(
(doc) => emit({ doc }),
(error) => emit({ error })
);
return unsubscribe;
});
};
export const onAuthStateChanged = function* () {
const channel = yield call(authStateChannel);
while (true) {
const { doc, error } = yield take(channel);
if (error) {
// handle error
} else {
if (doc) {
// user has signed in, use `doc.toJSON()` to check
} else {
// user has signed out
}
}
}
};
please note that other solutions that don't utilize channel sagas are not optimal for redux-saga, because turning an observable into a promise is not a valid solution in this case since you would need to call the promise each time you anticipate a change in authentication state (like for example: taking every USER_SIGNED_IN action and calling the "promisified" observable), which will negate the whole purpose of an observable
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.
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.