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
Related
I have this extrareducers:
extraReducers: builder => {
builder.addCase(getTopicsByDcResponsable.fulfilled, (state, action) => {
if(action.payload.status === 200){
state.error = '';
state.userTopics.responsableTopics.push(...action.payload.data);
} else {
state.error = action.payload.data;
}
});
builder.addCase(getTopicsByDcResponsable.rejected, (state, action) => {
state.error = action.payload;
});
builder.addCase(getTopicsbyDcentroEdit.fulfilled, (state, action) => {
if(action.payload.status === 200){
state.error = '';
state.userTopics.editorTopics.push(...action.payload.data);
} else {
state.error = action.payload.data;
}
});
builder.addCase(getTopicsbyDcentroEdit.rejected, (state, action) => {
state.error = action.payload;
});
},
All the rejected recieve the same message in case that the backend has an error 'Network Error'.
In redux you can use the same reducer for all the errors.
How can i use the same rejected in redux Toolkit if all recieve the same error?
Thanks
You can define a single error handler function and pass it to the builder.addMatcher method.
Something like this should prevent redundancy.
const handleRejected = (state, action) => {
state.error = action.error.message === "Network Error" ? "Network Error" : "Unexpected Error";
};
extraReducers: builder => {
builder
.addCase(getTopicsByDcResponsable.fulfilled, (state, action) => {
if (action.payload.status === 200) {
state.error = '';
state.userTopics.responsableTopics.push(...action.payload.data);
} else {
state.error = action.payload.data;
}
})
.addMatcher(
action => action.type.endsWith("/rejected"),
handleRejected
)
.addCase(getTopicsbyDcentroEdit.fulfilled, (state, action) => {
if (action.payload.status === 200) {
state.error = '';
state.userTopics.editorTopics.push(...action.payload.data);
} else {
state.error = action.payload.data;
}
})
.addMatcher(
action => action.type.endsWith("/rejected"),
handleRejected
);
},
Edit: You can try this if you want to remove additional .addMatcher
const isRejected = action => action.type.endsWith("/rejected");
builder
.addCase(getTopicsByDcResponsable.fulfilled, (state, action) => {
// Handle fulfilled action for getTopicsByDcResponsable
})
.addCase(getTopicsbyDcentroEdit.fulfilled, (state, action) => {
// Handle fulfilled action for getTopicsbyDcentroEdit
})
.addMatcher(isRejected, (state, action) => {
// Handle all rejected actions
state.error = action.error.message === "Network Error" ? "Network Error" : "Unexpected Error";
});
I'm making a goal setter app inspired from Brad Traversy's MERN stack guide but instead, I used NextJS w/ TypeScript instead of just plain ReactJS since I want to get my feet wet on those as well as I learn MERN.
My problem is that my Redux state: goals keeps on storing another array variable that's named goals and actually has the goals which I need to map on my frontend.
import { createSlice, createAsyncThunk, PayloadAction } from "#reduxjs/toolkit";
import goalService from "./goalService";
type stateTypes = {
goals: any[];
isError: boolean;
isSuccess: boolean;
isLoading: boolean;
message: String;
};
const initialState: stateTypes = {
goals: [],
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
export const addGoal = createAsyncThunk(
"goals/add",
async (goalData, thunkAPI: any) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await goalService.addGoal(goalData, token);
} catch (error: any) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
// Get user goals
export const getGoals = createAsyncThunk(
"goals/getAll",
async (_, thunkAPI: any) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await goalService.getGoals(token);
} catch (error: any) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const goalSlice = createSlice({
name: "goal",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(addGoal.pending, (state) => {
state.isLoading = true;
})
.addCase(addGoal.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.goals.push(action.payload);
})
.addCase(addGoal.rejected, (state, action: PayloadAction<any>) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(getGoals.pending, (state) => {
state.isLoading = true;
})
.addCase(getGoals.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.goals = action.payload;
})
.addCase(getGoals.rejected, (state, action: PayloadAction<any>) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
export const { reset } = goalSlice.actions;
export default goalSlice.reducer;
Here is a screenshot from my react devtools showing the tree of goals:
I cloned Brad Traversy's code in my PC but it seems to have no problem about this as I tried to run his code and I even tried copying to how he coded it but to no avail, the problem still persists to mine. I hope someone can see this and help me.
I need the answer to this cause I can't map my state: goals as it just contains another array.
In my project, I want to put as much as logic and function inside the the slice.js. Sometime I want to create or export the function outside of the createSlice like this:
const checkValid = () => {
```need to access the current state to check for validation```
}
export const boardSlice = createSlice({
name: 'board',
initialState,
reducers: {
check: (state, actions) => {
checkValid(actions.payload);
}
}
});
The checkValid need to access the state in the store, my current solution is directly passing the state as props along with the actions.payload in the reducer. But is there a better or official way of doing this? Also, is it good to put as much as logic inside the slice? Much appreciated.
Let me share with you what am using, hope it helps
mport { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import authService from "Services/authService";
import { logOutUser } from "Services/userServices";
const user = JSON.parse(localStorage.getItem("user"));
const initialState = {
user: user ? user : null,
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
// Register user
export const register = createAsyncThunk("user/register", async (user, thunkAPI) => {
try {
return await authService.register(user);
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) || error.message || error.toString();
return thunkAPI.rejectWithValue(message);
}
});
// Login user
export const login = createAsyncThunk("user/login", async (user, thunkAPI) => {
try {
const { username, password } = user;
const res = await authService.loginUser(username, password);
return res;
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) || error.message || error.toString();
return thunkAPI.rejectWithValue(message);
}
});
export const logout = createAsyncThunk("user/logout", async () => {
try {
return await logOutUser();
return res;
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) || error.message || error.toString();
return thunkAPI.rejectWithValue(message);
}
});
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
reset: (state) => {
state.isLoading = false;
state.isSuccess = false;
state.isError = false;
state.message = "";
},
},
extraReducers: (builder) => {
builder
.addCase(register.pending, (state) => {
state.isLoading = true;
})
.addCase(register.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(register.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
.addCase(login.pending, (state) => {
state.isLoading = true;
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
.addCase(logout.fulfilled, (state) => {
state.isSuccess = true;
localStorage.removeItem("token");
localStorage.removeItem("user");
state.user = null;
});
},
});
export const { reset } = userSlice.actions;
export default userSlice.reducer;
for more clalification on the use of extra reducers
checkout this https://www.youtube.com/watch?v=mvfsC66xqj0
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.
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;