I'm having issues while testing a slice with React Testing Library. I was running the following simple test:
import reducer from "states/slices";
test("should return the initial state", () => {
expect(reducer(undefined, {})).toEqual({
loading: true,
libraries: [],
books: [],
error: {
error: false,
variant: "error",
message: "",
},
});
});
The slice under test is the following:
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { getLibraries, getBooks } from "api";
const initialState = {
loading: true,
libraries: [],
books: [],
error: {
error: false,
variant: "error",
message: "",
},
};
export const fetchLibraries = createAsyncThunk("books/libraries", async () => {
const res = await getLibraries();
return res.data;
});
export const fetchBooks = createAsyncThunk(
"books/books",
async ({ title, libraryId, page }) => {
const res = await getBooks(title, libraryId, page);
return res.data;
}
);
const booksSlice = createSlice({
name: "books",
initialState,
reducers: {
unsetError: (state) => {
state.error = { error: false, variant: "error", message: "" };
},
},
extraReducers: (builder) => {
builder
.addCase(fetchLibraries.fulfilled, (state, action) => {
state.loading = false;
state.libraries = action.payload;
})
.addCase(fetchBooks.fulfilled, (state, action) => {
state.loading = false;
state.books = action.payload;
})
// .addCase(fetchBooks.pending, (state, action) => {
// state.loading = true;
// state.error = { error: false, variant: "error", message: "" };
// })
// .addCase(fetchLibraries.pending, (state, action) => {
// state.loading = true;
// state.error = { error: false, variant: "error", message: "" };
// })
// .addCase(fetchBooks.rejected, (state, action) => {
// state.loading = false;
// state.error.error = true;
// state.error.variant = "error";
// state.error.message =
// "Error. Try again.";
// })
// .addCase(fetchLibraries.rejected, (state, action) => {
// state.loading = false;
// state.error.error = true;
// state.error.variant = "error";
// state.error.message =
// "Error. Try again.";
// });
.addMatcher(
(action) => action.type.endsWith("/pending"),
(state, action) => {
state.loading = true;
state.error = { error: false, variant: "error", message: "" };
}
)
.addMatcher(
(action) => action.type.endsWith("/rejected"),
(state, action) => {
state.loading = false;
state.error.error = true;
state.error.variant = "error";
state.error.message =
"Error. Try again.";
}
);
},
});
const { actions, reducer } = booksSlice;
export const { unsetError } = actions;
export default reducer;
I'm getting back TypeError: Cannot read property 'endsWith' of undefined when running the test with the addMatchers in the slice. If I replace them with the addCases (the commented ones), the test works as expected.
Instead, if I normally launch the application, everything works correctly in either case.
Why does this happen? I am defining wrongly the matchers?
In your test case you are using {} as an action. Therefore when you are checking in the matcher action.type.endsWith() the action.type is not defined.
You can probably fix this if you use action.type?.endsWith in your matcher.
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?
IMAGE LINK HERE. it doubles the initial return.
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import axios from 'axios'
let url = 'http://localhost:3000/api/v1/posts'
const initialState = {
allPosts: [],
isLoading: false,
isError: false,
}
export const getAllPost = createAsyncThunk('allPosts/getAllPost', async (page, thunkAPI) => {
console.log(page)
try {
const { data } = await axios.get(`${url}?page=${page}`)
return data.posts
} catch (error) {
console.log(error)
}
})
const allPostSlice = createSlice({
name: 'allPosts',
initialState,
reducers: {},
extraReducers: {
[getAllPost.pending]: (state) => {
state.isLoading = true
},
[getAllPost.fulfilled]: (state, { payload }) => {
console.log(payload)
state.allPosts = [...state.allPosts, ...payload]
state.isLoading = false
},
[getAllPost.rejected]: (state) => {
state.isLoading = false
state.isError = true
},
},
})
export default allPostSlice.reducer
When I try to append ...state.allPosts, which is supposed to be the old state, it prints double the initial payload. I only need it printed once, not twice.
[getAllPost.fulfilled]: (state, { payload }) => {
console.log(payload)
state.allPosts = [...state.allPosts, ...payload]
state.isLoading = false
},
How can I print my payload only once using state.allPosts?
I have a problem with React redux. And I want to display the current state immediately. Unfortunately this doesn't work as intended. The changed data is only displayed correctly after the page has been reloaded.
This is the Main Part in my Articel.js
const buySomething = async (articelId) => {
await axios.put(`/articel/request/${articelId}`).then((res) => {
dispatch(requested(currentUser._id));
setSnackbarMessage(res.data);
setOpen(true);
});
};
Articel model:
requested: {
type: [String],
default: [],
},
articelSlice.js
const initialState = {
currentArticel: null,
loading: false,
error: false,
};
requested: (state, action) => {
if (!state.currentArticel.requested.includes(action.payload)) {
state.currentArticel.requested.push(action.payload);
} else {
state.currentArticel.requested.splice(
state.currentArticel.requested.findIndex(
(userId) => userId === action.payload
),
1
);
}
},
Complete articelSlice Code:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
currentArticel: null,
loading: false,
error: false,
};
export const articelSlice = createSlice({
name: "articel",
initialState,
reducers: {
fetchStart: (state) => {
state.loading = true;
},
fetchSuccess: (state, action) => {
state.loading = false;
state.currentArticel = action.payload;
},
fetchFailure: (state) => {
state.loading = false;
state.error = true;
},
requested: (state, action) => {
if (!state.currentArticel.requested.includes(action.payload)) {
state.currentArticel.requested.push(action.payload);
} else {
state.currentArticel.requested.splice(
state.currentArticel.requested.findIndex(
(userId) => userId === action.payload
),
1
);
}
},
like: (state, action) => {
if (!state.currentArticel.likes.includes(action.payload)) {
state.currentArticel.likes.push(action.payload);
state.currentArticel.dislikes.splice(
state.currentArticel.dislikes.findIndex(
(userId) => userId === action.payload
),
1
);
}
},
dislike: (state, action) => {
if (!state.currentArticel.dislikes.includes(action.payload)) {
state.currentArticel.dislikes.push(action.payload);
state.currentArticel.likes.splice(
state.currentArticel.likes.findIndex(
(userId) => userId === action.payload
),
1
);
}
},
},
});
export const {
fetchStart,
fetchSuccess,
fetchFailure,
like,
dislike,
requested,
} = articelSlice.actions;
export default articelSlice.reducer;
I am using redux toolkit and firebase firestore for backend. I just want to get an array of objects from the database. Below is the code for slice. When I log the payload in the console I am unable to get the data. Thanks in advance.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import firestore from '#react-native-firebase/firestore';
export const getCarouselImages = createAsyncThunk("/carouselImages", () =>
firestore().collection('Users').get()
);
const initialState = {
isLoading: false,
failed: true,
success: false,
imageArray:[],
};
const carouselData = createSlice({
name: "carouselImageSlice",
initialState,
reducers: {
resetCarouselData: () => initialState,
},
extraReducers: (builder) => {
builder.addCase(getCarouselImages.fulfilled, (state, { payload }) => {
state.isLoading = false;
state.failed = false;
state.success = true;
payload.forEach(doc => {
state.userData.push(doc.data())
});
console.log(payload)
});
builder.addCase(getCarouselImages.rejected, (state, action) => {
state.isLoading = false;
state.failed = true;
state.success = false;
});
builder.addCase(getCarouselImages.pending, (state, { payload }) => {
state.isLoading = true;
state.failed = false;
state.success = false;
});
},
});
export const { resetCarouselData } = carouselData.actions;
export default carouselData.reducer;
For the first time, I am using Redux in my React project. The code here I have added is for cookie-based authentication. I am worried that everything is here is in the correct format. It seems lots of duplicate code here. Especially for pending and rejected status in createSlice portion. How can I refactor this code and what will be the correct coding style in this case?
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import API from "../API";
// Register user:
export const signup = createAsyncThunk(
"user/signup",
async (userInfo, { rejectWithValue }) => {
try {
const { data } = await API.post("/signup", userInfo);
return data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Login:
export const login = createAsyncThunk(
"user/login",
async (loginInfo, { rejectWithValue }) => {
try {
const { data } = await API.post("/login", loginInfo);
return data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Logout:
export const logout = createAsyncThunk(
"user/logout",
async (args, { rejectWithValue }) => {
try {
const { data } = await API.get("/logout");
return data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Chek-Auth:
export const isAuthenticated = createAsyncThunk(
"user/isAuthenticated",
async (args, { rejectWithValue }) => {
try {
const { data } = await API.get("/check-auth");
return data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// createSlice portion is here:
export const userSlice = createSlice({
name: "user",
initialState: {
loading: true,
isLoggedIn: false,
message: "",
user: null,
error: null,
},
reducers: {},
extraReducers: {
[signup.pending]: (state, action) => {
state.loading = true;
},
[signup.fulfilled]: (state, action) => {
state.loading = false;
state.isLoggedIn = true;
state.message = action.payload.message;
state.user = action.payload.user;
state.error = null;
},
[signup.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload || action.error;
},
[login.pending]: (state, action) => {
state.loading = true;
},
[login.fulfilled]: (state, action) => {
state.loading = false;
state.isLoggedIn = true;
state.message = action.payload.message;
state.user = action.payload.user;
state.error = null;
},
[login.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload || action.error;
},
[logout.pending]: (state, action) => {
state.loading = true;
},
[logout.fulfilled]: (state, action) => {
state.loading = false;
state.isLoggedIn = false;
state.message = action.payload.message;
state.user = null;
},
[logout.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload || action.error;
},
[isAuthenticated.pending]: (state, action) => {
state.loading = true;
},
[isAuthenticated.fulfilled]: (state, action) => {
state.loading = false;
state.isLoggedIn = true;
state.message = action.payload.message;
state.user = action.payload.user;
},
[isAuthenticated.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload || action.error;
},
},
});
// export const { } = userSlice.actions;
export default userSlice.reducer;
We generally recommend to use the builder notation, not the object notation. That makes stuff like this easier:
extraReducers: builder => {
for (const thunk in [signup, login, logout, isAuthenticated]) {
builder.addCase(thunk.pending, (state) => { state.loading = true })
builder.addCase(thunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || action.error;
})
}
}
Keep in mind though that putting many asynchronous actions in the same state like you do here, sharing a loading state, may lead to race conditions.
Generally, for api cache stuff you should take a look into Redux Toolkit's Api cache abstraction, RTK-Query:
https://redux-toolkit.js.org/rtk-query/overview