I have started to create my application using #reduxjs/toolkit and kind of got stuck. I find no resource anywhere which can guide me with how to unit tests the logic in extraReducers. Any help would be appreciable.
Example:
Example:
const fetchList = createAsyncThunk('example/fetchList', async ({skip, reset, ...rest}) => {
const request = {
skip: reset ? initialState.skip : skip,
...rest,
};
return await getList(request);
});
const exampleSlice = createSlice({
name: 'example',
initialState: {id: '', list: []},
reducers: {
resetParams() {
return {id: '', list: []}
},
setId(state, {payload}) {
state.id = payload.id
}
},
extraReducers: {
[fetchList.pending]: (state) => {
state.fetching = true;
},
[fetchList.fulfilled]: (state, {payload = []}) => {
return {
fetching: false,
id: state.id + 1,
list: payload
}
},
[fetchList.rejected]: (state, {error}) => {
state.fetching = false;
},
},
});
//Tests .. for setId()
const initialState = {
id: 1,
list : []
}
const result = exampleSlice.reducer(initialState, exampleSlice.actions.setId({id: 10}))
expect(result.id).toEqual(10)
How can I test logic in extraReducers for fetchList.fulfilled and fetchList.rejected!
You can test it the same way as what you've shown.
Here's a simple way to just test the logic of the reducer based on the action types that createAsyncThunk outputs.
import reducer, {
fetchList
} from './exampleSlice';
describe('exampleSlice', () => {
describe('reducers', () => {
const initialState = { id: '', list: [], fetching: false }
it('sets fetching true when fetchList is pending', () => {
const action = { type: fetchList.pending.type };
const state = reducer(initialState, action);
expect(state).toEqual({ id: '', list: [], fetching: true });
});
it('sets the id and list when fetchList is fulfilled', () => {
const action = { type: fetchList.fulfilled.type, payload: { id: 1, list: [2, 3]} };
const state = reducer(initialState, action);
expect(state).toEqual({ id: 1, list: [2, 3], fetching: false });
});
it('sets fetching false when fetchList is rejected', () => {
const action = { type: fetchList.rejected.type, payload: { error: 'some error' } };
const state = reducer(initialState, action);
expect(state).toEqual({ id: '', list: [], fetching: false });
});
});
});
I also threw up a simple example of the same concept on a demo CSB: https://codesandbox.io/s/rtk-14-addmatcher-counter-test-l11mt?file=/src/features/counter/counterSlice.test.ts
Related
I'm using redux-toolkit for my chat application. Currently there are two slices in the store:
userSlice - for managing the user state
import { createSlice } from "#reduxjs/toolkit";
import appApi from "../services/appApi";
const endpoints = [
appApi.endpoints.signUpUser,
appApi.endpoints.loginUser,
appApi.endpoints.profileUser,
appApi.endpoints.logout,
];
export const userSlice = createSlice({
name: "user",
initialState: {
loading: false,
error: null,
_id: null,
},
reducers: {
addNotifications: (state, { payload }) => {},
resetNotifications: (state, { payload }) => {},
clearError: (state) => {
state.error = null;
state.loading = false;
},
setError: (state,{payload}) =>
{
state.error = payload
},
loadID: (state) => {
state._id =
JSON.parse(localStorage.getItem("cherry-chat-status")) || null;
},
setLoading: (state, { payload }) =>
{
state.loading = payload
}
},
extraReducers: (builder) => {
builder.addMatcher(
appApi.endpoints.signUpUser.matchFulfilled,
(state, { payload }) => {
localStorage.setItem("cherry-chat-status", JSON.stringify(payload._id));
return { ...state, loading: false, error: null, ...payload };
}
);
builder.addMatcher(
appApi.endpoints.loginUser.matchFulfilled,
(state, { payload }) => {
localStorage.setItem("cherry-chat-status", JSON.stringify(payload._id));
return { ...state, loading: false, error: null, ...payload };
}
);
builder.addMatcher(
appApi.endpoints.profileUser.matchFulfilled,
(state, { payload }) =>
{
return { ...state, loading: false, error: null, ...payload };
}
);
builder.addMatcher(
appApi.endpoints.profileUser.matchFulfilled,
(state, { payload }) => {
return { ...state, loading: false, error: null, ...payload };
}
);
builder.addMatcher(appApi.endpoints.logout.matchFulfilled, () => {
localStorage.removeItem("cherry-chat-status");
return { loading: false, error: null };
});
endpoints.forEach(({ matchPending, matchRejected }) => {
builder.addMatcher(matchPending, (state) => {
state.loading = true;
state.error = null;
});
builder.addMatcher(matchRejected, (state, { payload: error }) => {
state.error = error?.data?.message;
state.loading = false;
});
});
},
});
export const {
addNotifications,
resetNotifications,
clearError,
loadID,
setError,
setLoading,
} = userSlice.actions;
export default userSlice.reducer;
messageSlice - for managing the messages
import { createSlice } from "#reduxjs/toolkit";
import io from "socket.io-client";
import msgApi from "../services/msgApi";
const SOCKET_URL = "localhost:5000";
export const socket = io(SOCKET_URL);
const endpoints = [msgApi.endpoints.profileUserRooms];
export const messageSlice = createSlice({
name: "message",
initialState: {
rooms: [],
currentRoom: [],
members: [],
messages: [],
privateMemberMsg: {},
newMessages: {},
error: null,
},
reducers: {
addMembers: (state, { payload }) => {
state.members = payload;
},
},
extraReducers: (builder) => {
// your extra reducers go here
builder.addMatcher(
msgApi.endpoints.profileUserRooms.matchFulfilled,
(state, { payload }) => {
//I want set the loading state of the user Slice to be false
return {
...state,
rooms: payload,
};
}
);
endpoints.forEach(({ matchPending, matchRejected }) => {
builder.addMatcher(matchPending, (state) => {
state.error = null;
//I want set the loading state of the user Slice to be true
});
builder.addMatcher(matchRejected, (state, { payload: error }) => {
state.error = error?.data?.message;
//I want set the loading state of the user Slice to be false
});
});
},
});
export const { addMembers } = messageSlice.actions;
export default messageSlice.reducer;
Is there any method such that I can access the loading and error from the user slice and can be set at the messageSlice also? Or should I change the structure of the state or should I duplicate those variables in both slices?
This is a Redux issue.
Take a look at the logic that dispatched this action: {type: 'users/handleverification/fulfilled', payload: {…}, meta: {…}}
export const UserValidation = createAsyncThunk( //This Function should move to another folder
'users/handleverification',
async(thunkAPI) => {
try{
//Logic
if(Authenticated.status === 201){
localStorage.setItem('token', Authenticated.data.token);
delete Authenticated.token;
return Authenticated
console.log("Test me",Authenticated.data);
}
}
catch(error){
window.alert("error");
if(NewUser){
//delete User????????
}
}
}
)
Here is the Create Slice function:
export const userSlice = createSlice({
name: 'user',
initialState: {
UserData: {},
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: '',
},
reducers: {
clearState: (state) => {
state.isError = false;
state.isSuccess = false;
state.isFetching = false;
return state;
},
},
extraReducers: {
[UserValidation.fulfilled]:(state,{payload}) => {
// console.log("Fullfilled:")
},
[UserValidation.pending]:(state) => {
// console.log("Pending");
},
[UserValidation.rejected]:(state,{payload}) => {
// console.log("Rejected");
},
}
})
Why do I get the error:
A non-serializable value was detected in an action, in the path: payload.config.adapter. Take a Look at the action 'users/handleverification'
Hi i have a reducer containing one array of objects. These objects have some properties. When i dispatch an action in redux using useDispatch Hook i want to add some more properties dynamically.How can i achive this thing
//reducer data
users: [
{
id: 1,
first_name: "JACKILINE",
status: "online",
},
{
id: 2,
first_name: "BRONNNZE",
status: "offline",
},
];
I want to add these two properties dynamically mesg: "how are you",lastSeenDate: "30/11/19", How can i update the state in reducer
//i want the reducer like this after dispatching an action
users: [
{
id: 1,
first_name: "JACKILINE",
status: "online",
mesg: "how are you",
lastSeenDate: "30/11/19",
},
{
id: 2,
first_name: "BRONNNZE",
status: "offline",
mesg: "how are you",
lastSeenDate: "30/11/19",
},
],
`
//My action
export const setLastMessage = (payload) => ({
type: actionTypes.SET_LAST_MESSAGE,
payload: {
id: payload.id,
lastSeenDate: payload.date,
mesg:payload.message
},
});
I am not sure what you exactly want to do, but I guess you could do it when you get users rather when you import it.
How do you get users?
Please try to do this in your slice file(maybe src/slices/user.js).
const initialState = {
users: []
}
const slice = createSlice({
name: 'user',
initialState,
reducers: {
getUsers(state, action) {
const { users } = action.payload;
state.users = users.map(user => ({
...user,
mesg: "how are you",
lastSeenDate: "30/11/19"
}))
},
}
});
export const reducer = slice.reducer;
export const getUsers = () => async (dispatch) => {
const response = await axios.get('/api/users');
dispatch(slice.actions.getUsers(response.data));
};
Or you could do the similar thing while importing users in your component.
Hope this would be helpful for you.
You have to add action creator:
export const ANY_ACTION = 'ANY_ACTION';
function actionFun (msg, lastSeenDate) {
return {
type: ANY_ACTION,
msg,
lastSeenDate
}
}
export function handleActionFun (msg, lastSeenDate) {
return (dispatch) => {
return dispatch(actionFun(msg, lastSeenDate))
}
}
In Reducer:
import { ANY_ACTION } from './actionCreators';
export default function users (state = [], action) {
switch (action.type) {
case ANY_ACTION :
return state.map(u => {
return u.mesg = action.msg, u.lastSeenDate = action.lastSeenDate;
})
default :
return state
}
}
You can call it using dispatch(handleActionFun(msg, lastSeenDate))
For the articlesFetched action I'm getting an array payload like below, how can I extract/assign the id that is coming from the payload into the ids state field and for the state.data assign the whole object?
const payload =
[
{ id: 1,
items: 4
color: ['green', 'blue']
},
{ id: 2,
items: 10
color: ['yellow', 'red']
}
]
export const articlesSlice = createSlice({
name: 'articles',
initialState,
reducers: {
startFetching: (state) => {
state.loading = true
},
articlesFetched: (state, action) => {
state.loading = false
state.ids = ??
state.data = ??
},
},
});
If I'm reading things correctly, you should be able to do something like:
export const articlesSlice = createSlice({
name: 'articles',
initialState,
reducers: {
startFetching: (state) => {
state.loading = true
},
articlesFetched: (state, action) => {
state.loading = false
state.ids = action.payload.reduce((acc, item) => {
return [...acc, item.id]
}, [])
state.data = action.payload.reduce((acc, item) => {
return [...acc, ...item.color]
}, [])
},
},
});
Hey I think you're not correctly updating the state, you should not assign the values to the current state because you need to return a new copy of the updated state without mutating the original, you can accomplish what you want using a map function like this.
export const articlesSlice = createSlice({
name: 'articles',
initialState,
reducers: {
startFetching: (state) => {
return {
...state,
loading: true,
}
},
articlesFetched: (state, action) => {
// Array with the ids
const ids = action.payload.map(({id}) => id)
// The whole array with the objects
const data = action.payload
return {
...state,
loading: false,
ids,
data,
}
},
},
});
I am using react and redux to write a todolist app.
Here is my initial state:
export const initialState = {
todoList:[
{
taskId:1
task: 'gym'
completed: true
},
{
taskId:2
task: 'buy dinner'
completed: false
}
]
}
const todoReducer = (state=initialState,action) => {
switch(action.type){
case UPDATE_TODO:
return Object.assign({}, state, {
todoList: state.todoList.map(todo =>
{
return todo.taskId === action.payload.taskId ?
Object.assign({},todo, {
task: action.payload.task,
completed: action.payload.completed
}) : todo
}
)
})
default:
return state;
}
}
export default todoReducer;
If I want to update the second task to 'buy book' and change completed to true, how can I add code in my reducer? The code above is not working now. Not sure the reason. Could anyone help?
Try this:
reducer
const initialState = {
todoList: [
{
taskId: 1,
task: "gym",
completed: true
},
{
taskId: 2,
task: "buy dinner",
completed: false
}
]
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_TODO':
const { payload } = action;
return {
...state,
todoList: state.todoList.map(item => {
if (item.taskId === payload.taskId) {
return {
...item,
task: payload.task,
completed: payload.completed
};
} else {
return item;
}
})
};
default:
return state;
}
};
Unit test:
const output = todoReducer(initialState, {type: 'UPDATE_TODO', payload: {
taskId: 1,
task: "newTask",
completed: true
}});
console.log('output', output);
test output:
output { todoList:
[ { taskId: 1, task: 'newTask', completed: true },
{ taskId: 2, task: 'buy dinner', completed: false } ] }