import { createSlice, PayloadAction } from "#reduxjs/toolkit"
import { Dispatch } from 'redux';
import axios from "axios"
const API_URL = process.env.REACT_APP_API_HOST_URL || ""
export type projectObj = {
id?: number
createdBy?: number,
title: string,
description: string,
endDate: string,
priority: 'Critical' | 'High' | 'Medium' | 'Low',
status: 'Not Active' | 'In Progress' | 'Completed',
progress: number,
favorite: boolean
}
interface projectState {
projects: projectObj[],
projectFetching: boolean
}
const initialState : projectState = {
projects : [],
projectFetching: false
}
export const projectSlice = createSlice({
name: 'projectReducer',
initialState,
reducers: {
/* errors here */
create: async (state, action : PayloadAction<projectObj>) => {
const projectObj = action.payload
state.projects.push(await createProject(projectObj))
}
},
})
// CREATE PROJECT
const createProject = async (projectObj : projectObj) : Promise<projectObj> => {
try {
const project : projectObj = await axios.post(`${API_URL}/api/projects`, projectObj)
return project
} catch (err : any) {
return projectObj
}
}
export const { create } = projectSlice.actions
export default projectSlice.reducer
Create takes in a projectObj with the props list above and my api will create a new project and then return the project object with id in it. I want to push that into my state.
This errors in the create action. The function createProject returns a promise that I need to await on. . What is the proper way to go about this ?
Edit to ask question about answer-
export const projectSlice = createSlice({
name: 'projectReducer',
initialState,
reducers: {
create: (state, action: PayloadAction<projectObj>) => {
const projectObj = action.payload
state.projects.push(projectObj)
}
},
})
export const createProject = (projectObj: projectObj) => async (dispatch: Dispatch) => {
try {
const response = await axios.post(`${API_URL}/api/projects`, projectObj)
const data: projectObj = response.data.project
dispatch(create(data))
} catch (err: any) {
console.log(err)
}
}
Handle Async Requests with the createAsyncThunk.
const createProjectThunk = createAsyncThunk(
"project/createNew",
async (projectObj: projectObj) => {
const response = await createProject(projectObj);
return response;
}
);
export const projectSlice = createSlice({
name: "projectReducer",
initialState,
reducers: {
/* errors here */
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(createProjectThunk.fulfilled, (state, action) => {
// Add user to the state array
state.projects.push(action.payload);
});
}
});
Related
I'm trying to fetch data in redux and return a part of them, not all of it, but Typescript tells me that "Property 'xxx' does not exist on type 'string[]'". I tried to see it it has to do with the interface or the initialState but cannot find a solid answer.
import { createSlice, createAsyncThunk, PayloadAction } from '#reduxjs/toolkit';
import axios from 'axios';
import { useAppDispatch } from './hooks';
export interface MealState {
meal: string[],
input: string,
favorites: string[],
categories: string[],
}
const initialState: MealState = {
meal: [],
input: '',
favorites: [],
categories: [],
};
const mealReducer = createSlice({
name: 'meal',
initialState,
reducers: {
// ADD INPUT
addInput: (state, action: PayloadAction<string>) => {
state.input = action.payload
},
},
extraReducers: (builder) => {
builder.addCase(getMeal.fulfilled, (state, action) => {
state.meal = action.payload;
});
}
});
// ----------------------------------------------------------------------
export default mealReducer.reducer
// Actions
export const {
addInput,
addFavorites,
} = mealReducer.actions;
export const getMeal = createAsyncThunk(
'meal/getMeal',
async (search: string) => {
const response = await axios.get<string[]>(`https://www.themealdb.com/api/json/v1/1/search.php?s=${search}`);
return response.data.meals; // <== this create the problem, I can only return response.data
}
);
I can work with response.data, but then the problem is the same when I use
const list = useAppSelector((state: RootState) => state.meal.meal.meals)
since meals does not exist in the first place i will get "Property 'meals' does not exist on type 'string[]'"
the return value of your function getMeal is what you will get as value for the meal attribute of your state, and since
meal: string[]
the getMeal function should return data of type string[] which is not the case here according to the error you got
what I suggest is :
console.log and understand better what you are getting as response from the request
create const myMeal from the response
return it
async (search: string) => {
const response = await axios.get<any>(`https://www.themealdb.com/api/json/v1/1/search.php?s=${search}`);
console.log(response.data);
const myMeal: string[] = // I don't know how your response.data looks like but myMeal should be of type string[]
return myMeal;
}
I am very new to RTK, so I am trying to create a store and a slicer.
At first, at least I want to fetch some data from an API so when it start loading and after being succeed, I know the state of it.
Here I am creatinf the slicer:
const initialState: PlayerState = {
players: [],
status: 'idle'
};
export const getPlayers = createAsyncThunk('players/getPlayers', async () => {
const response = await axios.get(
'https://6360055fca0fe3c21aaacc04.mockapi.io/player'
);
return response.data;
});
const playerSlice = createSlice({
name: 'players',
initialState,
reducers: {
addPlayer: (state, action: PayloadAction<IPlayerProps>) => {
console.log('done');
state.players.push(action.payload);
}
},
extraReducers: {
[getPlayers.pending]: (state, action) => {
console.log('loading');
state.status = 'loading';
},
[getPlayers.fulfilled]: (state, action) => {
console.log('succeeded');
state.status = 'succeeded';
state.players = state.players.concat(action.payload);
}
}
});
export const { addPlayer } = playerSlice.actions;
export const selectPlayers = (state: RootState) => state.players.payload;
And here I am trying to connect it to the store:
//#ts-nocheck
import { configureStore } from '#reduxjs/toolkit'
import { addPlayer } from './playerSlice'
export const store = configureStore({
reducer: {
players: addPlayer,
},
})
export type RootState = ReturnType<typeof store.getState>;
So, after that I have a page with a button, so when I click it I try to dispatch something out of it with no luck unfortunately:
const NextPage = () => {
const dispatch = useDispatch();
return (
<ButtonNext
onClick={() => {
dispatch(addPlayer);
}}
text="< Back"
/>
);
};
export default NextPage;
Any help would be appreciated! :)
The are several issues in your code
First fix your createAsyncThunk
export const getPlayers = createAsyncThunk('players/getPlayers'
async (_unusedArgs, _thunkApi) => {
const response = await fetch('http://localhost:3000/players')
return response.json()
}
)
Your slice should look like this, note the builder callbacks for the cases:
export const playerSlice = createSlice({
name: "players",
initialState,
reducers: {
addPlayer: (state, action) => {
console.log("done");
state.players.push(action.payload);
}
},
extraReducers: (builder) => {
builder.addCase(getPlayers.fulfilled, (state, action) => {
console.log(action.payload);
state.players = action.payload;
state.status = "idle";
});
builder.addCase(getPlayers.pending, (state, action) => {
console.log("loading");
state.status = "loading";
});
}
});
export default playerSlice.reducer;
Call it inside the anonymous fn
<ButtonNext
onClick={() => {
dispatch(getPlayers()); // call with no arguments.
}}
text="< Back"
/>
And I also think that your root reducer in store is not right
import playerSlice from './playerSlice' // defaulted export
export const store = configureStore({
reducer: {
players: playerSlice,
},
})
Please check this sandbox with working example: https://codesandbox.io/s/redux-toolkit-basic-players-w-pokemons-6wmjm0?file=/src/features/playerSlice.js:409-995
What I am trying to achieve is sending action payload from one slice to another and I have been stuck several hours trying to do so.
I have tried accessing the global store but the problem is I am getting errors on doing so
I am using redux-tool-kit to manage the state of my react application and I am trying to pass a payload from one slice to another, the following is my first slice:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from 'axios';
import { clearAlert, displayIncorrectEmail } from "./features.js/Alert";
const initialState = {
user: user ? JSON.parse(user) : null,
isMember: false,
isLoading: true
}
This section still for the first slice
export const getRegisteredUser = createAsyncThunk('auth/getRegistrationRes', async (currentUser, thunkAPI) => {
try {
const response = await axios.post('/api/v1/auth/register', currentUser)
return response.data
} catch (error) {
// console.log(error.message)
thunkAPI.rejectWithValue(error.message)
}
})
export const getLoginUser = createAsyncThunk('auth/getLoginRes', async (currentUser, thunkAPI) => {
try {
const response = await axios.post('/api/v1/auth/login', currentUser)
thunkAPI.dispatch(displaySuccess())
setTimeout(() => {
thunkAPI.dispatch(clearAlert())
}, 3000);
return response.data
} catch (error) {
thunkAPI.dispatch(displayIncorrectEmail())
// console.log(error.response.data.msg);
thunkAPI.rejectWithValue(error.message)
//the below return is the action-payload I want to pass to another slice
return error.response.data.message
//
}
})
const authenticationSlice = createSlice({
name: 'auth',
initialState,
reducers: {
},
extraReducers: {
// login user reducers
[getLoginUser.pending]: (state) => {
state.isLoading = true;
},
[getLoginUser.fulfilled]: (state, action) => {
state.isLoading = false;
// console.log(action.payload.getState());
// action.payload.load = true
state.user = action.payload.user
},
[getLoginUser.rejected]: (state) => {
state.isLoading = false;
state.user = null
},
}
})
export const { registerUser, loginUser } = authenticationSlice.actions
export default authenticationSlice.reducer
This is the second slice is the code below
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
showAlert: false,
alertText: '',
alertType: '',
}
const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
displayIncorrectEmail: (state, action) => {
state.showAlert = !state.showAlert
//I want to pass the action.payload to this instead of hard-coding it to 'incorrect email' //below
state.alertText = 'incorrect email'
//the state.alertText above
state.alertType = "danger"
},
clearAlert: (state) => {
// setTimeout(() => {
state.showAlert = !state.showAlert;
// }, 4000);
}
}
})
export const { displayDanger, clearAlert, displaySuccess, displayIncorrectEmail } = alertSlice.actions
export default alertSlice.reducer
Kindly help if you have an idea on how to sort this.
cheers.
Just add an extraReducer for getLoginUser.rejected to the second slice as well. You can add that to the extraReducers of as many slices as you want to.
By the way, you really should not be the map object notation of extraReducers, but the extraReducers "builder callback" notation. The object notation you are using is soon going to be deprecated and we have been recommending against it in the docs for a long time.
I have the following code where I want to send POST request in with a data object and a history function. How can I pass the history param in the action creator? Thanks
login action creator
import { createAsyncThunk } from '#reduxjs/toolkit';
import { client } from '../client';
import {User} from './userLoginSlice';
export const userLogin = createAsyncThunk('user/userLogin' , async (data: User, history: string, thunkAPI) => {
try {
const res = await client.post<User>('/User/Credentials/Login', data);
if(res.status === 200) history.push('/dashboard');
return res.data;
} catch (error) {
return thunkAPI.rejectWithValue('Sorry! Something went wrong ):') ;
}
});
login slice
import { createSlice } from '#reduxjs/toolkit';
import { userLogin } from './userLoginThunk';
export interface User{
email: string;
passowrd: string;
rememberMe: boolean;
}
const initialState = {
user: {} as User,
loading: false,
error: ''
};
export const userLoginSlice = createSlice({
name: 'user',
initialState: initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(userLogin.pending, (state) => {
state.loading = true;
state.error = '';
});
builder.addCase(userLogin.fulfilled, (state, { payload }) => {
state.loading = false;
state.user = payload;
});
builder.addCase(userLogin.rejected, (state, { payload }) => {
state.loading = false;
state.error = String(payload);
});
}
});
export const userLoginReducer = userLoginSlice.reducer;
Using the action creator in a component
import { Form, Input, Checkbox } from 'antd';
import { useHistory } from 'react-router-dom';
import { ConfirmBtn } from '../small-components/ActionBtns';
import { useAppDispatch } from '../../custom-hooks/reduxCustomHooks';
import {userLogin } from 'src/redux/user-login/userLoginThunk';
import {User} from '../../redux/user-login/userLoginSlice';
import '../../sass/light-theme/user-login.scss';
export const UserLogin = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const onFinish = (values: User) => {
dispatch(userLogin(values, history));
};
return (
<Form></Form>
);
};
It's throwing the error which says that the userLogin expects 1 arguments but got 2. What am I doing wrong?
A thunk only accepts one parameter, so you would have to provide an object containing both your values and the history object.
In your case, you could also use the the unwrap() function (https://stackoverflow.com/a/67876542/3170628) so you don't have to provide the history object to your thunk:
const onFinish = async (values: User) => {
try {
await dispatch(userLogin(values)).unwrap();
history.push('/dashboard');
} catch (error) {
...
}
};
Please help me how I can introduce new function like getOrdersByCustomer in ordersSlice. I have provided full code of ordersSlice below. Please tell me what is extraReducers and how it works.
import { createSlice, createAsyncThunk, createEntityAdapter } from '#reduxjs/toolkit';
import axios from 'axios';
export const getOrders = createAsyncThunk('eCommerceApp/orders/getOrders', async () => {
const response = await axios.get('/api/e-commerce-app/orders');
const data = await response.data;
return data;
});
export const removeOrders = createAsyncThunk(
'eCommerceApp/orders/removeOrders',
async (orderIds, { dispatch, getState }) => {
await axios.post('/api/e-commerce-app/remove-orders', { orderIds });
return orderIds;
}
);
const ordersAdapter = createEntityAdapter({});
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
const ordersSlice = createSlice({
name: 'eCommerceApp/orders',
initialState: ordersAdapter.getInitialState({
searchText: ''
}),
reducers: {
setOrdersSearchText: {
reducer: (state, action) => {
state.searchText = action.payload;
},
prepare: event => ({ payload: event.target.value || '' })
}
},
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload)
}
});
export const { setOrdersSearchText } = ordersSlice.actions;
export default ordersSlice.reducer;
In Addition
Also can you please tell me what I will do with this following code for my custom function getOrdersByCustomer.
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
because, in my component I have used like
const orders = useSelector(selectOrders);
You can introduce new (async) functions as you already have (I used the customerId as part of the url -> you could access it through the params in your backend):
export const getOrdersByCustomer = createAsyncThunk('eCommerceApp/orders/getOrdersByCustomer', async (customerId) => {
const response = await axios.get(`/api/e-commerce-app/orders/${customerId}`);
const data = await response.data;
return data;
});
Then you can handle the response in your extraReducer:
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload),
[getOrdersByCustomer.fulfilled]: (state, action) =>
// set your state to action.payload
}
The extraReducers handle actions like async thunks. The createAsyncThunk function return 3 possible states (along with other things): pending, rejected or fulfilled. In your case you only handle the fulfilled response. You could also set your state with the other two options (in your case [getOrdersByCustomer.pending] or [getOrdersByCustomer.rejected]