I have an array of incomes that I display in a table. There's a "+" button to add a new income. I want to increment database and redux store with a dispatch.
Increment database works fine, but I have to reload page to see new data.
Why my dispatch does not update the state.data of incomes ?
// AddNewData.tsx
[...]
const onSubmit: SubmitHandler<FieldValues> = async (data) => {
if (user) {
const formData = {
userId: user._id,
label: data.label,
amount: data.amountInput,
};
dispatch(addUserIncome(formData));
}
};
[...]
//incomes_slice.ts
import { IDataState, IEntry } from "../../helpers/Interfaces";
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { deleteData, getData, updateData, addData } from "../../APIs/UserData";
import { Notifier } from "../../helpers/Notifier";
const initialState: IDataState = {
data: [],
loading: false,
error: null,
};
export const getUserIncomes = createAsyncThunk("incomes/getUserIncomes", (userId: string) => {
return getData("incomes", userId);
});
export const addUserIncome = createAsyncThunk("incomes/addUserIncome", async (data: { userId: string; label: string; amount: number }) => {
const response = await addData("incomes", data).then((res) => {
const newEntry: IEntry = {
_id: res.data.income._id,
user: data.userId,
label: data.label,
amount: data.amount,
};
return newEntry;
});
return response;
});
export const updateUserIncome = createAsyncThunk("incomes/updateUserIncome", async (data: { id: string; data: IEntry }) => {
await updateData("incomes", data.id, data.data);
return data;
});
export const deleteUserIncome = createAsyncThunk("incomes/deleteUserIncome", async (incomeId: string) => {
await deleteData("incomes", incomeId);
return incomeId;
});
const incomesSlice = createSlice({
name: "incomes",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getUserIncomes.pending, (state) => {
state.loading = true;
});
builder.addCase(getUserIncomes.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
state.error = null;
});
builder.addCase(getUserIncomes.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
action.error.message && Notifier(action.error.message, "error");
});
builder.addCase(addUserIncome.fulfilled, (state, action) => {
state.data.push(action.payload);
});
builder.addCase(updateUserIncome.fulfilled, (state, action) => {
state.data = state.data.map((income) => {
if (income._id === action.payload.id) {
return action.payload.data;
}
return income;
});
});
builder.addCase(deleteUserIncome.fulfilled, (state, action) => {
state.data = state.data.filter((income) => income._id !== action.payload);
});
},
});
export const incomesReducer = incomesSlice.reducer;
PS: addData function is just an axios.post that works fine.
Inside addUserIncome you are mixing styles of awaiting a promise.
Instead of this:
const response = await addData("incomes", data).then((res) => {
const newEntry: IEntry = {
_id: res.data.income._id,
user: data.userId,
label: data.label,
amount: data.amount,
};
return newEntry;
});
Either use this:
try {
const response = await addData("incomes", data);
// do stuff
} catch (e) {
console.log(e);
}
Or do this:
addData("incomes", data).then((res) => {
// do stuff
})
I can't leave a comment (just don't have enough credit yet).
You said you checked your store? Are you sure that the data is changed in the store?
(Because this is async of course, and you're not managing loading and pending state of your call, probably your UI is rendered before the data is updated.)
Related
const redirectURL = 'http://localhost:3000/';
const authorizePage = `https://accounts.spotify.com/authorize?response_type=token&client_id=${clientID}`
+`&redirect_uri=${redirectURL}&scope=playlist-modify-public`;
export const searchTerm = createAsyncThunk('musics/searchTerm',
async (term, {dispatch, getState}) => {
let accessToken = getState().result.accessToken;
if (!accessToken) {
window.location.replace(authorizePage);
let tempToken = /access_token=(.*?)&/.exec(window.location.href);
let expireTime = /expires_in=(.*)/.exec(window.location.href);
dispatch(setAccessToken(tempToken[1]));
window.setTimeout(() => {
console.log('here is mistake right?');
dispatch(setAccessToken(''));
}, Number(expireTime[1]) * 1000);
window.history.pushState('Access Token', null, '/')
}
return await fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {
headers: {Authorization: `Bearer ${accessToken}`}
})
.then(res => {
if (res.ok)
return res.json();
}, networkErr => console.log(networkErr.message)
)
.then(jsonResponse => {
if (jsonResponse && jsonResponse.tracks) {
console.log(jsonResponse, 'here');
}
});
}
);
export const resultSlice = createSlice({
name: 'result',
initialState: { accessToken: '', searchResult: [] },
reducers: {
setAccessToken: (state, action) => {
state.accessToken = action.payload;
console.log('here we go', action.payload);
}
},
extraReducers(builder) {
builder
.addCase(searchTerm.fulfilled, (state, action) => {
console.log(action.payload);
})
.addCase(searchTerm.rejected, (state, action) => {
})
}
});
const { setAccessToken } = resultSlice.actions;
First, I'm checking if accessToken exists or not in state.accessToken if not, I'm redirecting user to spotify page to authorization and I'm taking accessToken from location.href and I'm trying to save tempToken to reducer but after dispatching it, it is gone and it is happening again and again. It's re-rendering, can I find any other way to prevent it to not happen?
I m coding a bookmark list for users. I want to stock the api response in an array. I m using Google Book Api. I m using Redux Toolkit.
User can add books in his personal wishlist and to display and made some operations I need to stock in state books informations added.
For the moment I have only 1 book stock cause if I try to add another one in state, this one changes with the new one only. I need to stock all books added.
Any ideas ?
bookSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { BOOKS_BY_ID } from "../../services/api/googleBooks";
export const getBook = createAsyncThunk(
"book/getBook",
async ({ bookid }) => {
const response = await fetch(`${BOOKS_BY_ID}${bookid}`);
const data = await response.json();
return data;
}
);
const bookSlice = createSlice({
name: "book",
initialState: {
list: [],
status: null,
},
reducers: {},
extraReducers: {
[getBook.pending]: (state, action) => {
state.status = "loading";
},
[getBook.fulfilled]: (state, action) => {
state.status = "success";
state.list = action.payload;
},
[getBook.rejected]: (state, action) => {
state.status = "failed";
},
},
});
export default bookSlice.reducer;
FetchBookBookmarked.js
useEffect(() => {
// let allBook = [];
let ignore = false;
bookInfos?.map(async (book) => {
dispatch(getBook({ bookid: book }));
await axios.get(`${BOOKS_BY_ID}${book}`).then((res) => {
// allBook.push(res.data);
if (!ignore) {
setBookDetail((oldValues) => [...oldValues, res.data]);
}
});
});
return () => {
ignore = true;
};
}, [bookInfos, dispatch]);
Push the book data into the list.
Replace following line:
state.list = action.payload;
To:
state.list.push(action.payload)
To avoid duplicates, you can check the ID before adding it:
const bookAlreadyExists = state.list.some(book => book.id === action.payload.id)
if (!bookAlreadyExists) {
state.list.push(action.payload)
}
I'm using the rudux toolkit in react. After rendering the first page, useEffect is executed and the farmloadPost action is executed.
By the way, armloadPost.pending is executed three times. How can I make it run only once?
i tried disabled stricmode, but it same
this is my code
useEffect( () => {
dispatch(farmloadPost());
}, [dispatch]);
export const farmloadPost = createAsyncThunk(
"farm/farmloadPost",
async (data, { rejectWithValue }) => {
try {
const response = await axios.get(api);
return response.data;
} catch (error: any) {
console.log("error:",error);
return rejectWithValue(error.response.data);
}
}
);
const postSlice = createSlice({
name: "post",
initialState,
reducers: {},
extraReducers: (builder) =>
builder
// loadPost
.addCase(farmloadPost.pending, (state) => {
state.farmLoading = true;
state.farmDone = false;
state.farmError = null;
})
.addCase(farmloadPost.fulfilled, (state, action) => {
// console.log("action.payload:", action.payload);
state.farmLoading = false;
state.farmDone = true;
state.farm = action.payload;
})
.addCase(farmloadPost.rejected, (state, action) => {
state.farmLoading = false;
// state.farmDone = action.error.message;
})
.addDefaultCase((state) => state),
});
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
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]);