I created thunk by redux toolkit "createAsyncThunk" and i do some async operations here
export let Login: any = createAsyncThunk("authPage/Login",
async ({values, action}: any, {dispatch, rejectWithValue}) => {
try {
let response = await AuthAPI.Login(values)
if (response.data.resultCode === 0) {
let responseFromAuthMe = await HeaderAPI.AuthMe()
if (responseFromAuthMe.data.resultCode === 0) {
- - - - - - dispatch(HeaderLogin()) - - - - - - - - -
}
} else {
if (response.data.resultCode === 10) {
let response = AuthAPI.GetCaptcha()
dispatch(getCaptcha(response))
}
action.setStatus({error: response.data.messages})
}
} catch (err: any) {
return rejectWithValue(err.response.data)
}
})
but one moment i need use another thunk like "HeaderLogin" :
export let HeaderLogin: any = createAsyncThunk("authPage/HeaderLogin",
async ({}, {dispatch}) => {
let responseFromAuthMe = await HeaderAPI.AuthMe()
if (responseFromAuthMe.data.resultCode === 0) {
let {id, email, login} = responseFromAuthMe.data.data;
//TODO check 4 arg - isLogin
dispatch(setAuthUserData({id, email, login}))
let responseLogin = await HeaderAPI.Login(login)
return responseLogin.data.items.filter((u: any) => {
if (id === u.id) {
return u
}
})
}
})
but thunk is not called.
maybe have any way that calls thunk?
How i can use thunk inside another thunk in Redux?
his problem is solved, you need to throw an empty object, if you don't even have any arguments, then he is called
Related
How do I retry this fetch x times if it fails?
The code is based on this article: https://dmitripavlutin.com/javascript-fetch-async-await/
async function fetchData() {
const [firstResponse, secondResponse] = await Promise.all([
fetch(firstUrl),
fetch(secondUrl),
]);
const first = await firstResponse.json();
const second = await secondResponse.json();
return [first, second];
}
fetchData()
.then(([first, second]) => {
console.log("success");
})
.catch((error) => {
console.log("error");
});
Since the requests are independent of each other, I'd have a utility function that will retry X times and then use that in the Promise.all. I'd also have a utility function for fetching JSON that handles the fetch API footgun where it doesn't check HTTP success (see my blog post here). So something along these lines:
// Fetch JSON
function fetchJSON(...args) {
const response = await fetch(...args);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
}
// Fetch JSON with up to `retries` retries
async fetchJSONWithRetry(retries, ...args) {
while (retries > 0) {
try {
const result = await fetchJSON(...args);
return result;
} catch (e) {
if (--retries === 0) {
throw e;
}
}
}
}
// Your `fetchData`
async function fetchData(retries = 5) {
const [first, second] = await Promise.all([
fetchJSONWithRetry(retries, firstUrl),
fetchJSONWithRetry(retries, secondUrl),
]);
return [first, second];
}
The information about the error in my case sits deeply in the response, and I'm trying to move my project to redux-toolkit. This is how it used to be:
catch(e) {
let warning
switch (e.response.data.error.message) {
...
}
}
The problem is that redux-toolkit doesn't put that data in the rejected action creator and I have no access to the error message, it puts his message instead of the initial one:
While the original response looks like this:
So how can I retrieve that data?
Per the docs, RTK's createAsyncThunk has default handling for errors - it dispatches a serialized version of the Error instance as action.error.
If you need to customize what goes into the rejected action, it's up to you to catch the initial error yourself, and use rejectWithValue() to decide what goes into the action:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
}
)
We use thunkAPI, the second argument in the payloadCreator; containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: For our example async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) is our payloadCreator with the required arguments;
This is an example using fetch api
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getExampleThunk = createAsyncThunk(
'auth/getExampleThunk',
async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
try{
const response = await fetch('https://reqrefs.in/api/users/yu');
if (!response.ok) {
return rejectWithValue(response.status)
}
const data = await response.json();
return fulfillWithValue(data)
}catch(error){
throw rejectWithValue(error.message)
}
}
)
Simple example in slice:
const exampleSlice = createSlice({
name: 'example',
initialState: {
httpErr: false,
},
reducers: {
//set your reducers
},
extraReducers: {
[getExampleThunk.pending]: (state, action) => {
//some action here
},
[getExampleThunk.fulfilled]: (state, action) => {
state.httpErr = action.payload;
},
[getExampleThunk.rejected]: (state, action) => {
state.httpErr = action.payload;
}
}
})
Handling Error
Take note:
rejectWithValue - utility (additional option from thunkAPI) that you can return/throw in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action.
For those that use apisauce (wrapper that uses axios with standardized errors + request/response transforms)
Since apisauce always resolves Promises, you can check !response.ok and handle it with rejectWithValue. (Notice the ! since we want to check if the request is not ok)
export const login = createAsyncThunk(
"auth/login",
async (credentials, { rejectWithValue }) => {
const response = await authAPI.signin(credentials);
if (!response.ok) {
return rejectWithValue(response.data.message);
}
return response.data;
}
);
I'm playing around with nextjs
and I've trouble to debug a function like
this:
export const authInitialProps = isProtectedRoute => {
console.log("mylog");// this works
return ({ req, res }) => {
console.log("inner my log", req); // this doesn't work
};
};
Using in a page
ProfilePage.getInitialProps = async () => {
const auth = authInitialProps(true);
if (!typeof auth === "function") {
const user = await getUserProfile();
return { user };
}
return { user: null };
};
I never see "inner my log" both in chrome console
and in my console terminal.
What's the problem please?
Try this It may work:
ProfilePage.getInitialProps = async () => {
const auth = await authInitialProps(true); // await added
if (!typeof auth === "function") {
const user = await getUserProfile();
return { user };
}
return { user: null };
};
I think it should be async to not pass over it!
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
Implementing the pattern Dan (#gaearon) demonstrated here, I was just wondering the best way to have the common action work standalone in the following scenario:
const commonAction = () => {
return async (dispatch, getState, api) => {
try {
const response = await api.get('/resource');
dispatch(success('SUCCESS', response.data));
} catch(error) {
dispatch(error('ERROR', error));
throw error; //this is the problem
}
}
const combinedAction = () => {
return async (dispatch, getState, api) => {
try {
await dispatch(commonAction());
const otherResponse = await api.get('/otherResource');
dispatch(success('COMBINED_SUCCESS', otherResponse.data));
} catch(error) {
dispatch(error('COMBINED_ERROR', error));
}
}
The above works in that if either fails, the COMBINED_ERROR will be dispatched, due to the commonAction re-throwing it's error. However, if I wanted to use commonAction by itself, I would get an Unhandled promise rejection error due to the re-throw.
One thought I had was an optional bool isStandAlone = true to determine whether to re-throw or not, i.e.
const commonAction = (isStandAlone = true) => {
return async (dispatch, getState, api) => {
try {
const response = await api.get('/resource');
dispatch(success('SUCCESS', response.data));
} catch(error) {
dispatch(error('ERROR', error));
if(!isStandAlone) {
throw error;
}
}
}
And then in the combinedAction I would just:
await dispatch(commonAction(false));
Is this a code smell? Is there a better and/or different way to approach this?