ReactJS - infinite loop when dispatch the redux action - reactjs

i got some problem for a couple hours, i really dont know why useeffect give me infinite loop when i dispatch the redux action ? because i want my page is will refresh after i change the value from order state. is there not allowed to put the async state from redux in useeffect? sorry im still new..
orderSlice.js
export const orderPay = createAsyncThunk(
'order/orderPay',
async (order, thunkAPI) => {
try {
const user1 = thunkAPI.getState().user.userInfo;
const config = {
headers: {
'Content-Type': 'Application/json',
Authorization: `Bearer ${user1.token}`,
},
};
const res = await axios.put(
`http://localhost:8000/api/order/${order.orderId}/pay`,
order.paymentResult,
config
);
const data = await res.data;
return data;
} catch (error) {
thunkAPI.rejectWithValue(
error.response && error.response.data.message
? error.response.data.message
: error.response
);
}
}
);
const orderSlice = createSlice({
name: 'order',
initialState: {
loading: false,
error: '',
order: {},
success: false,
},
reducers: {
orderDetail: (state, action) => {
state.order = action.payload;
},
orderPayReset: (state, action) => {
state.loading = false;
state.error = '';
state.order = {};
state.success = false;
},
},
extraReducers: (builder) => {
builder
.addCase(postOrder.pending, (state, action) => {
state.loading = true;
state.order = {};
state.success = false;
})
.addCase(postOrder.fulfilled, (state, action) => {
state.loading = false;
state.order = action.payload;
state.success = true;
})
.addCase(postOrder.rejected, (state, action) => {
state.error = action.payload;
})
.addCase(orderDetail.pending, (state, action) => {
state.loading = true;
state.order = {};
state.success = false;
})
.addCase(orderDetail.fulfilled, (state, action) => {
state.loading = false;
state.order = action.payload;
state.success = true;
})
.addCase(orderDetail.rejected, (state, action) => {
state.error = action.payload;
})
.addCase(orderPay.pending, (state, action) => {
state.loading = true;
state.success = false;
})
.addCase(orderPay.fulfilled, (state, action) => {
state.loading = false;
state.success = true;
})
.addCase(orderPay.rejected, (state, action) => {
state.error = action.payload;
});
},
});
OrderScreen.jsx
const OrderScreen = () => {
const navigate = useNavigate();
const { orderId } = useParams();
const [sdkReady, setSdkReady] = useState(false);
const { paymentMethod } = useSelector((state) => state.cart);
const { userInfo } = useSelector((state) => state.user);
const { success, loading, error, order, orderPaid } = useSelector(
(state) => state.order
);
const dispatch = useDispatch();
useEffect(() => {
const addPaypalScript = async () => {
const res = await axios.get(`http://localhost:8000/api/config/paypal`);
const data = await res.data;
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = `https://www.paypal.com/sdk/js?client-id=${data}`;
script.async = true;
script.onload = () => {
setSdkReady(true);
};
document.body.appendChild(script);
return data;
};
(async () => await addPaypalScript())();
if (!order || success) {
dispatch(orderPayReset());
dispatch(orderDetail(orderId));
} else if (!order.isPaid) {
if (!window.paypal) {
addPaypalScript();
} else {
setSdkReady(true);
}
}
}, [orderId, dispatch]);
const successPaymentHandler = (paymentResult) => {
dispatch(orderPay({ orderId, paymentResult }));
order.isPaid = true;
};
// console.log(orderPaid);
return loading ? (
<h1>Order {order._id}</h1>
)

Related

Update Comments state in Redux Toolkit

I'm creating a CRUD project with React and Redux toolkit and the problem is I can't update Comments after creating a new one.
I structured Comment Slice like that:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import commentsService from "./commentService";
const initialState = {
comments: [],
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
export const createComment = createAsyncThunk(
"api/newcomment",
async (commentData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await commentsService.createComment(commentData, token);
} catch (error) {
console.log(error);
}
}
);
export const getAllComments = createAsyncThunk(
"api/comments",
async (PostId, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
return await commentsService.getAllComments(PostId, token);
}
);
export const deleteComment = createAsyncThunk(
"api/:id",
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await commentsService.deleteComment(id, token);
} catch (error) {
console.log(error);
}
}
);
export const commentsSlice = createSlice({
name: "comment",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(createComment.pending, (state) => {
state.isLoading = true;
})
.addCase(createComment.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.comments = state.comments.push(action.payload);
})
.addCase(createComment.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(getAllComments.pending, (state) => {
state.isLoading = true;
})
.addCase(getAllComments.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
const comments = state.comments.filter(
(comment) => comment.PostId === action.payload.PostId
);
if (comments.length === 0) {
state.comments.push(action.payload);
}
})
.addCase(getAllComments.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(deleteComment.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteComment.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.comments = state.comments.filter(
(comment) => comment.id !== action.payload.id
);
})
.addCase(deleteComment.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
And CardPost where I dispatch comments:
const CardPost = ({ post }) => {
const [show, setShow] = useState(false);
const dispatch = useDispatch();
const { user } = useSelector((state) => state.auth);
const PostId = post.id;
const { comments, isLoading, isError, message } = useSelector(
(state) => state.comments
);
useEffect(() => {
if (isError) {
console.log(message);
}
dispatch(getAllComments(PostId));
}, [dispatch, PostId, isError, message]);
//// Show Comments
<div>
<div>
<FormComment PostId={PostId} />
</div>
<div>
{comments.length > 0 ? (
<div className="comments">
{comments.map((id) => (
<div key={id}>
<CardComment post={post} />
</div>
))}
</div>
) : (
<Typography>No Comment..</Typography>
)}
</div>
</div>
When there are several comments on a Post that are displayed and I create a new one, it shows me the message "No comment.." but all the messages appear well when the page is refreshed.
Where did I make it wrong?
Is it the .addCase that I built wrong and the state is not updated or is it the useEffect dispatch?

rejectWithValue createAsyncThunk

I want to create createAsyncThunk function for register user but there is a problem when rejected action, it is throw an error
the code of authSlice:
`
export const authSlice = createSlice({
name: "auth",
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) => {
console.log({ action });
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
},
});
the code of function register user
// Register user
export const register = createAsyncThunk(
"auth/register",
async (user, thunkAPI) => {
const { rejectWithValue } = thunkAPI;
try {
return await authService.register(user);
} catch (error) {
const message = error.message;
return rejectWithValue(message);
}
}
);
import axios from "axios";
const API_URL = "http://localhost:5000/auth/condidate";
const register = async (userData) => {
const response = await axios.post(API_URL, userData);
if (response.data) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
};
const authService = {
register,
};
export default authService;
`
there is no problem with register fulfilled but rejected regisiter throw an error
enter image description here
try to register user with react and redux toolkit but there is a problem with reject the action of register

Why useEffect works only after manual reload?

I use react redux redux-toolkit
When switching to CardDetails, everything works and I get data in the state of only one element, but when I go from this page to any other page that has useEffect and in which I use the same state, it does not work and I have to reload the page manually. For what reason can this happen?
Here is the page code for one element
const CardDetails = () => {
const {id} = useParams()
const dispatch = useDispatch()
const location = useLocation()
const {posts, loading} = useSelector((state) => ({...state.posts}))
useEffect(() => {
dispatch(getPost(id))
}, []);
console.log(posts)
return (
<div>
<img style={{maxWidth: "150px"}} src={posts.img} alt="image"/>
</div>
);
};
Here is the page I go to from CardDetails
const Main = () => {
const dispatch = useDispatch()
const {posts, loading} = useSelector((state) => ({...state.posts}))
const user = JSON.parse(localStorage.getItem('profile'))
useEffect(() => {
dispatch(getPosts())
}, []);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [creator, setCreator] = useState("");
const [category, setCategory] = useState("");
const [img, setImg] = useState("");
const [tags, setTags] = useState(["tag1", "tag2"]);
const onSavePostClicked = () =>{
const newPost = {title, description, category, img}
dispatch(createPost(newPost))
setTitle('')
setDescription('')
setCreator('')
setCategory('')
setImg('')
}
return (
And here is the postsSlice
import {createAsyncThunk, createSlice} from "#reduxjs/toolkit";
import * as api from "../../../api/index";
import {fetchPost, fetchPostsCategory} from "../../../api/index";
export const getPosts = createAsyncThunk(
'posts/getPosts',
async (thunkAPI)=> {
const response = await api.fetchPosts()
return response.data.data
})
export const getPost = createAsyncThunk(
'posts/getPost',
async (id,thunkAPI)=> {
const response = await api.fetchPost(id)
return response.data.data
})
export const createPost = createAsyncThunk(
'posts/createPosts',
async (newPost,thunkAPI)=> {
const response = await api.createPost(newPost)
return response.data.data
})
export const deletePost = createAsyncThunk(
'posts/deletePost',
async (post,thunkAPI)=> {
const {_id} = post
const response = await api.deletePost(_id)
return response.data
})
export const updatePost = createAsyncThunk(
'posts/updatePost',
async (post,thunkAPI)=> {
const {_id} = post
const postEdited = {...post, published: true}
const response = await api.updatePost(_id, postEdited)
return response.data
})
export const getCategory = createAsyncThunk(
'posts/getCategory',
async (category,thunkAPI)=> {
const response = await api.fetchPostsCategory(category)
return response.data.data
})
const initialState = {
posts: [],
loading: false,
error:null
}
export const postsSlice = createSlice({
name: "posts",
initialState,
reducers:{
postAdded: {
reducer(state, action) {
state.posts.push(action.payload)
},
prepare(title, description, creator, category, img) {
return {
payload: {
title,
description,
creator,
category,
img
}
}
}
},
},
extraReducers: {
[createPost.pending]: (state, action) => {
state.loading = true;
},
[createPost.fulfilled]: (state, action) => {
state.loading = false;
state.posts.push(action.payload);
},
[createPost.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getPosts.pending]: (state) => {
state.loading = true
},
[getPosts.fulfilled]: (state, action) => {
state.loading = false
state.posts = action.payload
},
[getPosts.rejected]: (state) => {
state.loading = false
},
[deletePost.fulfilled]: (state, action)=>{
if (!action.payload?._id) {
console.log('Delete could not complete')
console.log(action.payload)
return;
}
const { _id } = action.payload;
state.posts = state.posts.filter(post => post._id !== _id);
},
[updatePost.pending]: (state, action) => {
state.loading = true;
},
[updatePost.fulfilled]: (state, action) => {
const { _id } = action.payload;
state.loading = false;
const posts = state.posts.filter(post => post._id !== _id);
state.posts = [...posts, action.payload];
},
[updatePost.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getCategory.pending]: (state) => {
state.loading = true
},
[getCategory.fulfilled]: (state, action) => {
state.loading = false
console.log()
state.posts = action.payload
},
[getCategory.rejected]: (state) => {
state.loading = false
},
[getPost.pending]: (state) => {
state.loading = true
},
[getPost.fulfilled]: (state, action) => {
state.loading = false
state.posts = action.payload
},
[getPost.rejected]: (state) => {
state.loading = false
},
},
})
export const {postAdded} = postsSlice.actions
export default postsSlice.reducer
When you are using useEffect with an empty array as deps [] it will only be trigger when you mount your component (and cleaned when you unmount the component)

Redux Error: TypeError: state.favorites.concat is not a function

I am using redux toolkit and ran into an issue where I cannot push the results into an array which I declared. This is my initial state:
const initialState = {
favorites: [],
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
In the dashboard of my application, I want to display the favorites that are in state in which I use a useEffect as such:
useEffect(() => {
if (isError) {
console.log(message);
}
if (!user) {
navigate("/login");
}
//dispatch(getFavorites());
// return () => {
// dispatch(reset());
// };
}, [user, navigate, isError, message, dispatch]);
The problem occurs when I uncomment the dispatch to get favorites, here is the getFavorites in my service:
//Get user favorites
const getFavorites = async (token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.get(API_URL, config);
return response.data;
};
Here is how I create a favorite:
const createFavorite = async (favoriteData, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
var qs = require("qs");
var data = qs.stringify({
address: favoriteData.location["address1"],
phone: favoriteData.display_phone,
rating: favoriteData.rating.toString(),
price: favoriteData.price,
});
console.log(favoriteData);
const response = await axios.post(API_URL, data, config);
return response.data;
};
This is where the error occurs in the slice file where my extra-reducers are:
export const favoriteSlice = createSlice({
name: "favorite",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(createFavorite.pending, (state) => {
state.isLoading = true;
})
.addCase(createFavorite.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.favorites.concat(action.payload);
})
.addCase(createFavorite.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(getFavorites.pending, (state) => {
state.isLoading = true;
})
.addCase(getFavorites.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.favorites = action.payload;
})
.addCase(getFavorites.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
The errors occurs on the line:
state.favorites.concat(action.payload)
Am I returning the data in a wrong format? I am new to redux and am following a tutorial so I am not sure how to fix this. Any help would be appreciated.
.concat() doesn't mutate it returns a copy so you have to also have to reset your state.favorites to it here:
.addCase(createFavorite.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.favorites = state.favorites.concat(action.payload);
})
As for the error, it seems your state.favorites either goes undefined or its set to a non-array/string value. .concat() only works on arrays or strings

How to fetch data partially in react, redux?

Actions
import { FETCH_BLOG, FETCH_BLOG_ERROR, FETCH_BLOG_LOADING } from "../constants/blogActionTypes"
const initialState = {
blogs: [],
error: '',
loading: false,
allBlogs: []
}
// eslint-disable-next-line import/no-anonymous-default-export
export default (blogs = initialState, action) => {
switch (action.type) {
case FETCH_BLOG_LOADING:
return {
blogs: [...blogs.blogs],
loading: true,
error: ''
};
case FETCH_BLOG_ERROR:
return {
blogs: [...blogs.blogs],
loading: false,
error: action.payload
};
case FETCH_BLOG:
return {
blogs: [...action.payload, ...blogs.blogs],
loading: false,
error: ''
};
default: return blogs;
}
}
Reducers
export const fetchBlogs = (data) => async (dispatch) =>{
dispatch({ type: FETCH_BLOG_LOADING, payload: true })
fetch('http://localhost:5000/blog?show=' + data, {
method: 'GET',
headers: {
authorization: userData.token
}
})
.then(res => res.json())
.then(data => {
if (data.message) {
dispatch(fetchBlogsError(data.message))
} else {
dispatch({ type: FETCH_BLOG, payload: data })
}
})
}
React
const [fetchData, setFetchData] = useState(0);
const showData = () => {
setFetchData(fetchData + 10)
}
const dispatch = useDispatch();
const { loading, error, blogs, } = useSelector(state => state.blogs)
const getData = useCallback( () => {
dispatch(fetchBlogs(fetchData))
}, [fetchData])
useEffect(() => {
getData()
}, [getData])
On the first render, I fetch 10 items.after clicking on load more I fetch another 10 data from database. On the blog component it's fine but after go back to the home page and get back to the blog page; the blog items duplicates. How to fix this duplicate issue>
There are two issues here which are inter-related, you possibly don't need to address #2 depending on how you address #1.
You should add a condition to your thunk action so that you don't fetch a page that you have previously fetched.
You should separate your blog items by page so that you aren't always appending the newest items at the end of the array if you fetch page 1 twice.
Sidenote: [...blogs.blogs] is unnecessary because there is reason to clone properties which you aren't changing.
I'm confused by your API calls. It looks like /blog?show=20 is getting posts 21-30 but I would think based on the name show that it would be posts 1-20.
Using position indexes:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchBlogs = createAsyncThunk(
"blogs/fetchBlog",
async (startIndex, { getState, rejectWithValue }) => {
const res = await fetch("http://localhost:5000/blog?show=" + startIndex, {
method: "GET",
headers: {
// where does userData come from ??
authorization: userData.token
}
});
const data = await res.json();
if (data.message) {
rejectWithValue(data.message);
} else {
return data;
}
},
{
condition: (startIndex, { getState }) => {
const { blogs } = getState();
// cancel if loading of if first post on paage is loaded
if (blogs.loading || blogs.blogs[startIndex]) {
return false;
}
}
}
);
const initialState = {
blogs: [],
error: "",
loading: false
};
export default createReducer(initialState, (builder) =>
builder
.addCase(fetchBlogs.pending, (state) => {
state.loading = true;
state.error = "";
})
.addCase(fetchBlogs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error;
})
.addCase(fetchBlogs.fulfilled, (state, action) => {
const startIndex = action.meta.arg;
const newBlogs = action.payload;
// insert in the array at the correct position
state.blogs.splice(startIndex, newBlogs.length, newBlogs);
})
);
Using separated pages:
import { createAsyncThunk, createReducer, createSelector } from "#reduxjs/toolkit";
export const fetchBlogs = createAsyncThunk(
"blogs/fetchBlog",
async (pageNumber, { getState, rejectWithValue }) => {
const startIndex = 10 * (pageNumber - 1);
const res = await fetch("http://localhost:5000/blog?show=" + startIndex, {
method: "GET",
headers: {
// where does userData come from ??
authorization: userData.token
}
});
const data = await res.json();
if (data.message) {
rejectWithValue(data.message);
} else {
return data;
}
},
{
condition: (pageNumber, { getState }) => {
const { blogs } = getState();
// cancel if loading of if there is a property for this page
if (blogs.loading || blogs.blogs[pageNumber]) {
return false;
}
}
}
);
const initialState = {
//arrays keyed by page number
blogs: {},
error: "",
loading: false
};
export default createReducer(initialState, (builder) =>
builder
.addCase(fetchBlogs.pending, (state) => {
state.loading = true;
state.error = "";
})
.addCase(fetchBlogs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error;
})
.addCase(fetchBlogs.fulfilled, (state, action) => {
const pageNumber = action.meta.arg;
state.blogs[pageNumber] = action.payload;
})
);
// want to flatten the blogs array when selecting
// create a memoized selector
export const selectBlogs = createSelector(
state => state.blogs,
(blogsState) => ({
...blogsState,
blogs: Object.values(blogsState.blogs).flat(1)
})
)
With component:
export default () => {
const [pageNumber, setPageNumber] = useState(1);
const showNext = () => {
setPageNumber((page) => page + 1);
};
const dispatch = useDispatch();
const { loading, error, blogs } = useSelector(selectBlogs);
useEffect(() => {
dispatch(fetchBlogs(pageNumber));
}, [dispatch, pageNumber]);

Resources