I have been using Redux-Toolkit more than the React-Redux. I came across situations where I had to make GET requests, So I recently started using Redux-Thunk (before this I used useEffect but, as it's not a standard way to handle async functions, when using redux. I learned about middleware).
Here is the code of my Thunk function nad extraReducer which handles the GET request
export const fetchData = createAsyncThunk("type/getData", async () => {
try {
const response = await axios({url});
return response.data;
} catch (error) {
console.log(error.response);
}
});
export const extraReducers = {
[fetchData.pending]: (state) => {
state.loading = true;
},
[fetchData.fulfilled]: (state, action) => {
state.loading = false;
state.products = action.payload;
},
[fetchData.rejected]: (state) => {
state.loading = false;
state.error = true;
},
};
In fetchData function my returned response.data is being used in extraReducers as payload so I can set the state easily. But, now the scenario is I have make a post request and I don't know how will I send the data to my Thunk function.
First you create the action of posting data and send the data:
export const postData = createAsyncThunk(
"type/postData",
async (data) => {
try {
const response = await axios.post("https://reqres.in/api/users", data);
// If you want to get something back
return response.data;
} catch (err) {
console.error(err)
}
}
);
Then in a place where you want to send that data you just dispatch this action with the data argument that you want to send:
const handlePost = () => {
// whatever you want to send
const data = .........
dispatch(postData(data));
}
If you want, you can also modify your extraReducers for this action.
Related
I need function in redux-toolkit to fetch all data from others slices.
I have this code:
export const getAllData = createAsyncThunk(
'fetchRoot/getAllData',
async (_, { dispatch, rejectWithValue }) => {
const promises = [dispatch(getUsers()), dispatch(getSettings()), dispatch(getClients())];
Promise.all(promises)
.then((res: any) => {
// for (const promise of res) {
// console.log('SSS', promise);
// if (promise.meta.rejectedWithValue) {
// return rejectWithValue(promise.payload);
// }
}
})
.catch((err) => {
console.log(err);
});
}
);
My question: if one of slice fetch function (example: getUsers()) is rejected, how to reject promise.all?
getUsers() function and extraReducers:
export const getUsers = createAsyncThunk('users/getUsers', async (_, { rejectWithValue }) => {
try {
const res = await agent.Users.getAll();
return await res.data;
} catch (err) {
return rejectWithValue(err);
}
});
extraReducers: (builder) => {
builder
// GetUsers lifecycle ===================================
.addCase(getUsers.pending, (state) => {
state.apiState.loading = true;
state.apiState.error = null;
})
.addCase(getUsers.fulfilled, (state, { payload }) => {
state.apiState.loading = false;
state.data = payload;
})
.addCase(getUsers.rejected, (state, { payload }) => {
state.apiState.loading = false;
state.apiState.error = payload;
})
You have it basically right. Once the Promise.all(promises) has resolved you will have an array containing the resolved value of each of your individual thunks.
The individual promises will always resolve and will never reject. They will resolve to either a fulfilled action or a rejected action. In some cases, it will make sense to use the unwrap() property which causes rejected actions to throw errors. But looking at the .meta property will work too.
You can check your action with the isRejected or isRejectedWithValue functions which serve as type guards, that way you won't have any TypeScript errors when accessing properties like action.meta.rejectedWithValue.
The hard part here is trying to return rejectWithValue() from inside a loop. I would recommend unwrapping to throw an error instead.
import { createAsyncThunk, unwrapResult } from "#reduxjs/toolkit";
export const getAllData = createAsyncThunk(
"fetchRoot/getAllData",
async (_, { dispatch }) => {
const promises = [dispatch(getUsers()), dispatch(getSettings()), dispatch(getClients())];
const actions = await Promise.all(promises);
return actions.map(unwrapResult);
}
);
Note that there is no reason to try/catch in your getUsers thunk if you are going to rejectWithValue with the entire caught error object. Just let the error be thrown.
export const getUsers = createAsyncThunk('users/getUsers', async () => {
const res = await agent.Users.getAll();
return res.data;
});
I have a react-redux app. I need to call API and used it in my component. The app is called with fetch in function in utills.
All functions are group and export like this:
export const sportTeam = {
getBasketballTeam,
getBasketballTeamById,
}
function getBasketballTeam() {
let token = store.getState().UserReducer.token;
fetch(
actions.GET_BASKETBALLTEAM,
{
method: "GET",
headers: { Authorization: `Bearer ${token}` },
}
)
.then((res) => {
if (res.status == 200 ) {
return res.json();
}
})
.then((response) => {
console.log(response);
})
.catch((err) => {
console.log(err);
});
}
getBasketballTeam contains an array of objects.
How can I get getBasketballTeam and used it in the component in the view to returning the list with this data?
You don't want your getBasketballTeam function to access the store directly through store.getState().
What you want is a "thunk" action creator that gets the store instance as an argument when you dispatch it.
The flow that you want is this:
Component continuously listens to the basketball team state with useSelector (or connect).
Component mounts.
Component dispatches a getBasketballTeam action.
Action fetches data from the API.
Reducer saves data from the action to the state.
State updates.
Component re-renders with the new data from state.
The easiest way to do this is with the createAsyncThunk function from Redux Toolkit. This helper handles all errors by dispatching a separate error action. Try something like this:
Action:
export const fetchBasketballTeam = createAsyncThunk(
"team/fetchBasketballTeam",
async (_, thunkAPI) => {
const token = thunkAPI.getState().user.token;
if ( ! token ) {
throw new Error("Missing access token.");
}
const res = await fetch(actions.GET_BASKETBALLTEAM, {
method: "GET",
headers: { Authorization: `Bearer ${token}` }
});
if (res.status !== 200) {
throw new Error("Invalid response");
}
// what you return is the payload of the fulfilled action
return res.json();
}
);
Reducer:
const initialState = {
status: "idle",
data: null
};
export const teamReducer = createReducer(initialState, (builder) =>
builder
.addCase(fetchBasketballTeam.pending, (state) => {
state.status = "pending";
})
.addCase(fetchBasketballTeam.fulfilled, (state, action) => {
state.status = "fulfilled";
delete state.error;
state.data = action.payload;
})
.addCase(fetchBasketballTeam.rejected, (state, action) => {
state.status = "rejected";
state.error = action.error;
})
);
Store:
export const store = configureStore({
reducer: {
team: teamReducer,
user: userReducer,
}
});
Component:
export const BasketballTeam = () => {
const { data, error, status } = useSelector((state) => state.team);
const dispatch = useDispatch();
useEffect(
() => {
dispatch(fetchBasketballTeam());
},
// run once on mount
// or better: take the token as an argument and re-run if token changes
[dispatch]
);
if (status === "pending") {
return <SomeLoadingComponent />;
}
if (!data) {
return <SomeErrorComponent />;
}
// if we are here then we definitely have data
return <div>{/* do something with data */}</div>;
};
After you get response you need to do the following things
call dispatch function to store the data received in REDUX state.
Now when you have data in redux state, you can use useSelector() to get that state and make use of it in your jsx file.
I have used a similar Thunk and slice in other areas of my project and all work as expected but this particular one does not work, the difference with this Thunk is it contains an axios call that waits for the result so that it can be chained the next axios call.
After logging i think the issue is that the Thunk is firing 'fulfilled' after the first axios call and not waiting for the full function to complete, this one has me stumped on how to fix this issue.
export const getPlanAPI = createAsyncThunk('dataStorePlans/plan', async () => {
const response = await axios.get('/api/routes')
let promises = [];
const routeData = [];
// I think the Thunk is firing 'fulfilled' at this point.
try {
for (let i = 0; i < response.data.length; i++) {
promises.push(axios.get('/api/routedetail?planid=' + response.data[i].id + '&jobcount=' + response.data[i].jobs))
}
} catch (error) {
console.log(error);
}
Promise.all(promises).then(function (results) {
results.forEach(function (response) {
routeData.push(response.data[0]);
})
return routeData
});
});
export const planSlice = createSlice({
name: 'dataStorePlans/plan',
initialState: {
planList: [],
status: ''
},
reducers: {
getPlanState: (state) => {
return state
}
},
extraReducers: {
[getPlanAPI.pending]: (state, action) => {
state.status = 'Loading';
},
[getPlanAPI.fulfilled]: (state, action) => {
state.status = 'Success';
state.planList = action.payload
},
[getPlanAPI.rejected]: (state, action) => {
state.status = 'failed';
action.error = action.error.message;
}
}
});
The main problem is that your Function will not wait for the Promises to be finished. It also did never return the routeData.
You also clutter your function with lots of iterations and pushing stuff around. It is easy to forget where you have to return things. I know async is the new hot stuff, but a promise chain works better here. Even if I keep your structure the function becomes way easier to read if you use map at the right places.
export const getPlanAPI = createAsyncThunk('dataStorePlans/plan', async () => {
const response = await axios.get('/api/routes')
let promises = [];
try {
promises = response.data.map(({id, jobs}) =>
axios.get(`/api/routedetail?planid=${ id }&jobcount=${ jobs }`)
)
} catch (error) {
console.log(error);
}
return Promise
.all(promises)
.then(results =>
results.map(response => response.data[0])
)
});
I am currently trying to load my product data into redux, but so far I cant seem to pass the product information returned from firestore into the reducer.
Index.js -> load first 10 products from firestore soon after store was created.
store.dispatch(getAllProducts)
action/index.js
import shop from '../api/shop'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS
products
})
const getAllProducts = () => dispatch => {
shop.getProducts(products => {
dispatch(receiveProducts)
})
}
shop.js
import fetchProducts from './firebase/fetchProducts'
export default {
getProducts: (cb) => cb(fetchProducts())
}
fetchProducts.js
const fetchProducts = async() => {
const ProductList = await firebase_product.firestore()
.collection('store_products').limit(10)
ProductList.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
}).catch(function (error) {
console.log('Error getting Documents: ', error)
})
}
In product reducers
const byId = (state={}, action) => {
case RECEIVE_PRODUCTS:
console.log(action); <- this should be products, but it is now promise due to aysnc function return?
}
I can get the documents with no issues (tempDocs gets the first 10 documents without any issue.) but I am not able to pass the data back into my redux. If I were creating normal react app, I would add a loading state when retrieving the documents from firestore, do I need to do something similar in redux as well ?
Sorry if the code seems messy at the moment.
fetchProducts is an async function so you need to wait for its result before calling dispatch. There are a few ways you could do this, you could give fetchProducts access to dispatch via a hook or passing dispatch to fetchProducts directly.
I don't quite understand the purpose of shop.js but you also could await fetchProducts and then pass the result of that into dispatch.
A generalized routine I use to accomplish exactly this:
const ListenGenerator = (sliceName, tableName, filterArray) => {
return () => {
//returns a listener function
try {
const unsubscribe = ListenCollectionGroupQuery(
tableName,
filterArray,
(listenResults) => {
store.dispatch(
genericReduxAction(sliceName, tableName, listenResults)
);
},
(err) => {
console.log(
err + ` ListenGenerator listener ${sliceName} ${tableName} err`
);
}
);
//The unsubscribe function to be returned includes clearing
// Redux entry
const unsubscriber = () => {
//effectively a closure
unsubscribe();
store.dispatch(genericReduxAction(sliceName, tableName, null));
};
return unsubscriber;
} catch (err) {
console.log(
`failed:ListenGenerator ${sliceName} ${tableName} err: ${err}`
);
}
};
};
The ListenCollectionGroupQuery does what it sounds like; it takes a tableName, an array of filter/.where() conditions, and data/err callbacks.
The genericReduxAction pretty much just concatenates the sliceName and TableName to create an action type (my reducers de-construct action types similarly). The point is you can put the dispatch into the datacallback.
Beyond this, you simply treat Redux as Redux - subscribe, get, etc just as if the data were completely local.
I am trying to get user data from api using axios with createAsyncThunk, and want the user data to be stored in state by the fulfilled action dispatched by the createAsyncThunk.
As mentioned in the docs
if the promise resolved successfully, dispatch the fulfilled action with the promise value as action.payload.
But the action.payload in undefined in the fulfilled action creator.
Here is my code.
/// Create Async Thunk
export const fetchUserData = createAsyncThunk(
'user/fetchUserData',
(payload, { dispatch }) => {
axios
.get('/user')
.then(res => {
console.log(res.data);
//Used this as a work around for storing data
dispatch(setUser(res.data));
return res.data;
})
.catch(err => {
console.error(err);
return err;
});
}
);
/// On Fulfilled
const userSlice = createSlice({
...
extraReducers:{
...
[fetchUserData.fulfilled]: (state, action) => {
// Payload is undefined
state.data = action.payload
},
}
}
createAsyncThunk accepts two parameters:
type
payloadCreator
Where payloadCreator is a callback function that should return a promise (containing the result of some asynchronous logic) or a value (synchronously).
So, you can either write:
export const fetchUserData = createAsyncThunk(
'user/fetchUserData',
(payload, { dispatch }) => {
return axios.get('/user'); // Return a promise
}
);
or
export const fetchUserData = createAsyncThunk(
'user/fetchUserData',
async (payload, { dispatch, rejectWithValue }) => {
try {
const response = await axios.get('/user')
return response // Return a value synchronously using Async-await
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response)
}
}
);
An addition to #Ajeet Shah's answer:
According to the documentation a rejected promise must return either
an Error-instance, as in new Error(<your message>),
a plain value, such as a descriptive String,
or a RejectWithValue return by thunkAPI.rejectWithValue()
With the first two options, and I haven't tested the last option, the payload will also by undefined, but an error parameter will be given containing your rejected message.
See this example:
const loginAction = createAsyncThunk(
"user/login",
(payload, { getState }) => {
const { logged_in, currentRequestId, lastRequestId } = getState().login;
// Do not login if user is already logged in
if (logged_in) {
return Promise.reject(new Error(Cause.LoggedIn));
}
// Do not login if there is a pending login request
else if (lastRequestId != null && lastRequestId !== currentRequestId) {
return Promise.reject(new Error(Cause.Concurrent));
}
// May as well try logging in now...
return AccountManager.login(payload.email, payload.password);
}
);