I am trying to figure out how to use useReducer with asynchronous CRUD operations (using the fetch API).
In my mind the reducer function would look like this:
async function reducer(action, state) {
const newState = {...state};
switch (action.type) {
case types.ADD_ITEM:
try {
const {item} = await addItem(action.payload.item);
newState.items.push(item);
}
catch (e) {
newState.error = e.message;
}
break;
case types.REMOVE_ITEM:
try {
await removeItem(action.payload.itemId);
newState.items = newState.items.filter(value.id !== action.payload);
}
catch (e) {
newState.error = e.message;
}
break;
case types.EDIT_ITEM:
try {
const {item} = await editItem(action.payload.itemId, action.payload.item);
newState.items[newState.items.findIndex(value => value.id === action.payload.itemId)] = item;
}
catch (e) {
newState.error = e.message;
}
break;
}
return newState;
}
These would be the fetch functions:
async function addItem(item) {
const response = await fetch('addItemRoute', {
method: "POST",
body: JSON.stringify({
item
})
});
return response.json();
}
async function removeItem(itemId) {
const response = await fetch('removeItemRoute/' + itemId, {
method: "DELETE"
});
return response.json();
}
async function editItem(itemId, item) {
const response = await fetch('editItemRoute/'+ itemId, {
method: "PUT",
body: JSON.stringify({
item
})
});
return response.json();
}
But the reducer function cannot be an async function.
What would be the standard way to handle concepts like this?
Any help/reference is truly appreciated.
I think you misunderstood the role of reducer. In React world, there is a thing call global state (a way to pass values down to children without having to pass as props), which traditionally being handled by another package called Redux. The reducer only handle taking whatever you dispatch, decide what action to take to update the global state based on the type of action which is not asynchronous. The action is what you use to decide what to dispatch and also the way for you to get the data to dispatch so usually all the HTTP calls occurs here. Since useReducer will returns for you the current state and the dispatch function as well, you can basically pass this dispatch to your action. You can take a look at my example below based on your example for clearer image of what you might want to do:
You may want to put all your action in a action file called action.js like this:
async function addItem(item, dispatch) {
const response = await fetch('addItemRoute', {
method: "POST",
body: JSON.stringify({
item
})
});
return dispatch({
type: "ADD_ITEM",
payload: response.json()});
}
async function removeItem(itemId, dispatch) {
const response = await fetch('removeItemRoute/' + itemId, {
method: "DELETE"
});
return dispatch({
type: "ADD_ITEM",
payload: response.json()
});
}
async function editItem(itemId, item, dispatch) {
const response = await fetch('editItemRoute/'+ itemId, {
method: "PUT",
body: JSON.stringify({
item
})
});
return dispatch({
type: "ADD_ITEM",
payload: response.json()
});
}
Then in your reducer, you can do the regular without having to call the fetch or async calls like this:
async function reducer(action, state) {
const newState = {...state};
switch (action.type) {
case types.ADD_ITEM:
try {
const {item} = action.payload;
newState.items.push(item);
}
catch (e) {
newState.error = e.message;
}
break;
case types.REMOVE_ITEM:
try {
newState.items = newState.items.filter(value.id !== action.payload);
}
catch (e) {
newState.error = e.message;
}
break;
case types.EDIT_ITEM:
try {
const {item} = action.payload;
newState.items[newState.items.findIndex(value => value.id === action.payload.itemId)] = item;
}
catch (e) {
newState.error = e.message;
}
break;
}
return newState;
}
Then in your component with the button that you want to execute this, you can do something like this:
const MyComponent = ()=> {
...
const [state, dispatch] = useReducer(reducer, initialState);
...
return (
...
<button onClick={addItem(item, dispatch)}/>
...
}
You can reference the core concept of redux here which IMO explains very clearly the functionality of reducer, dispatch, actions and global state. If you want you can also tried out Redux as well, here's their tutorial.
Related
On a React page, I have a method called goOut. This method calls upon a Redux action > Node controller > Redux reducer. I can confirm that the correct data values are returned inside the Redux action, the controller method, and the reducer. However, nonetheless, at point 1 below inside the goOut method, it returns undefined.
What am I doing wrong / how could it return undefined if the the reducer is returning the correct values? It is as if the await inside the goOut method is not working...
React page:
import { go_payment } from "../../appRedux/actions/paymentAction";
<button onClick={this.goOut}>
Button
</button>
async goOut(ev) {
try {
const data = { user: parseInt(this.state.userId, 10) };
let result = await this.props.go_payment({data});
console.log(result);
// 1. RETURNS UNDEFINED. As if it tries to execute this line before it has finished the previous line.
{
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(
{go_payment}, dispatch
);
};
Redux Action:
export const go_payment = (data) => {
let token = getAuthToken();
return (dispatch) => {
axios
.post(`${url}/goController`, data, { headers: { Authorization: `${token}` } })
.then((res) => {
if (res.status === 200) {
// console.log confirms correct data for res.data
return dispatch({ type: GO_SUCCESS, payload: res.data });
})
}
}
Node controller method:
Returns the correct data in json format.
Reducer:
export default function paymentReducer(state = initial_state, action) {
switch (action.type) {
case GO_SUCCESS:
// console.log confirms action.payload contains the correct data
return { ...state, goData: action.payload, couponData: "" };
}
}
I'm trying to get data from json file, but it always return [object][object]
Here is json file https://my-json-server.typicode.com/khanh21011999/demo/user
Here is request function to get data
export function requestGetUser() {
return axios({
method: 'get',
url: 'https://my-json-server.typicode.com/khanh21011999/demo/user',
});
}
Here is the method i use to get data
function* loginSaga(action) {
console.log('Saga is working')
const getJson = yield call(requestGetUser) //same
const getJsonData = JSON.stringify(getJson)
const getJsonUsername = String(getJsonData.username)
console.log('GetJson '+getJson)
const getJsonPassword = String(getJsonData.password)
if (String(action.data.username) === getJsonUsername) {
if (String(action.data.password) === getJsonPassword) {
console.log('saga login success')
yield put({type: 'LOGIN_SUCCESS'})
SaveToAsyncStorage(action.data)
}
else {
console.log('saga password fail')
}
}
else {
console.log("saga user fail")
}
}
export {loginSaga}
It return like this
Weird things is i use a online tutorial to get data, it work with that(data show in above image)
worked method
export function* handleGetUser(action) {
try {
const response = yield call(requestGetUser); //same
const { data } = response;
yield put(setUser(data));
} catch (error) {
console.log(error);
}
}
setUser
export const setUser = (user) => ({
type: actionList.SET_USER,
user,
});
GetUserInfo
export const GetUserInfo = (user, password) => {
return{
type: actionList.GET_USER_INFO,
data: {user, password}, //same??
}
};
Here is export function
export function* watchSaga() {
yield takeLatest(GET_USER, handleGetUser); //work
yield takeLatest(GET_USER_INFO,loginSaga) //notwork
}
One different is the worked method have reducer
const initState = {
user: undefined,
};
const User = (state = initState, action) => {
switch (action.type) {
case actionList.SET_USER:
const {user} = action;
return {...state,user};
default:
return state;
}
};
export default User;
But my method have none,(i thoght data was save in state action)
console.log('GetJson ' + getJson); You're printing a concatenation of strings and objects. Modify it to console.log('GetJson ', getJson);
Besides, you should return the res.data from axios.get() method, see Response Schema. You will get the plain object of JavaScript, there is no need to use JSON.stringify().
import axios from 'axios';
export function requestGetUser() {
return axios({
method: 'get',
url: 'https://my-json-server.typicode.com/khanh21011999/demo/user',
}).then((res) => 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.
Just like the title says, my reducer returns a promise instead of a regular object. I am assuming this is because I am using redux-saga for async requests, which kinda does not make sense. Is there a way to have my reducer return a resolved object instead?
// reducer.tsx
...
const initialState = {
token: '',
error: null,
loading: false
};
const authenticationReducer = async (state = initialState, action) => {
switch (action.type) {
case EMAIL_LOGIN: {
console.log('action.payload: ', action.payload);
const {token} = action.payload;
return {
...state,
token
};
}
...
// saga.tsx
function* emailLogin({type, payload}) {
try {
const {email, password} = payload;
const loginUserInfo = {
email,
password,
};
const response = yield call(axios, `${USER_ADDRESS}/login/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: loginUserInfo,
});
yield put(setLoginStatus(response));
} catch (e) {}
}
But for some reason both useSelector() and useStore() indicates that my reducer returns a promise
// Login.tsx
...
const token = useSelector(state =>{ console.log(state)}) // {authentication: Promise}
const state = useStore().getState();
console.log(state) // {authenitcation: Promise}
...
Please help!
async functions always return a promise. Redux reducers must never be async!
In this case, all you should need to do is remove the async keyword from the reducer.
I'm trying to chain two calls in a single action using a thunk, but it doesn't seem to work as expected. I need the ID value from the first action to call the second one.
Actions look like this:
export const getParentRecords = filterId => {
return (dispatch, getState) => {
let headers = {
filter_id: filterId
};
const request = axios({
method: "GET",
url: `https://myapi.com/v1/parent-records`,
headers: headers
});
dispatch({
type: GET_PARENT_RECORDS,
payload: request
});
};
};
export const getChildRecords = (parentId = null) => {
let url = `https://myapi.com/v1/child-records`;
if (parentId) {
url = `https://myapi.com/v1/child-records/?parent_id=${parentId}`;
}
return (dispatch, getState) => {
let headers = {
//etc...
};
const request = axios({
method: "GET",
url: url,
headers: headers
});
dispatch({
type: GET_CHILD_RECORDS,
payload: request
});
};
};
export const getAllRecords = filterId => {
return (dispatch, getState) => {
dispatch(getParentRecords(filterId);
let { parentRecords } = getState();
let defaultParent = parentRecords.filter(p => p.is_default === true)[0];
dispatch(getChildRecords(defaultParent.parent_id));
};
};
In calling component:
const mapStateToProps = state => {
return {
parentRecords: state.parentRecords,
childRecords: state.childRecords
};
};
export default connect(mapStateToProps, { getAllRecords })(MyComponent);
Problem is; dispatching the first action doesn't seem to be doing anything. When I call getState() afterwards, the data isn't there. The parentRecords variable in getAllRecords is always empty.
I'm really not sure what to do with this. Pretty common scenario but haven't found a way through it.
I suggest you to use another library for side-effects handling, like redux-saga or redux-observable, since redux-thunk is very primitive.
Redux-saga is generator-based and imperative.
Redux-observable is RxJS-based and declarative.
So choose whatever you like more.
https://redux-saga.js.org/
https://redux-observable.js.org/
Each asynchronous action should have three action types, eg: GET_CHILD_RECORDS, GET_CHILD_RECORDS_SUCCESS and GET_CHILD_RECORDS_FAILURE.
Using redux-saga it will look like this:
Action creators:
const getChildRecords = (parentId = null) => ({
type: GET_PARENT_RECORDS,
payload: parentId
});
Then you can handle this action in saga generator:
function rootSaga*() {
yield takeLatest(GET_PARENT_RECORDS, onGetParentRecords);
yield takeLatest(GET_PARENT_RECORDS_SUCCESS, onGetChildRecords);
}
function onGetParentRecords*({ payload: parentId }) {
try {
const parentRecords = yield call(apiCallFunctionHere, parentId);
yield put({
type: GET_PARENT_RECORDS_SUCCESS,
payload: parentRecords
});
} catch(error) {
yield put({
type: GET_PARENT_RECORDS_FAILURE,
error
});
}
}
function onGetChildRecords*({ payload: parentRecords }) {
const defaultParent = parentRecords.filter(p => p.is_default === true)[0];
try {
const childRecords = call(apiFunctionToFetchChildRecords, defaultParent);
yield put({
type: GET_CHILDREN_RECORDS_SUCCESS,
payload: parentRecords
});
} catch(error) {
yield put({
type: GET_CHILDREN_RECORDS_FAILURE,
error
});
}
}
I'm not interested in introducing yet another framework for something so simple. After the commute home, an idea struck me. Please let me know the pros/cons.
A new getAllRecords function:
export const getAllRecords = filterId => {
return (dispatch, getState) => {
let headers = {
// etc...
};
const request = axios({
method: "GET",
url: `https://myapi.com/v1/parent-records`,
headers: headers
});
request.then(result => {
if (result.status === 200) {
let parentRecords = result.data.payload;
let defaultParent = parentRecords.filter(p => p.is_default === true)[0];
dispatch({
type: GET_PARENT_RECORDS,
payload: parentRecords
});
dispatch(getChildRecords(defaultParent.parent_id));
}
});
};
};
This seems to get me everything I need. Gets parent record(s) by executing the promise, dispatches parent and child results.