Redux: dispatching an action multiple times results to too many api requests - javascript

Using an api for anime called Jikan, I'm trying to display promo thumbnails of new anime shows.
I'm using two api calls, one to get the new anime shows:
export const get_new_anime = () =>
`${base_url}search/anime?q&order_by=score&status=airing&sort=desc`;
and one for getting the videos (containing promos) of anime by getting its id.
export const get_news = (anime_id) => `${base_url}anime/${anime_id}/videos`;
In my home page, here I am mapping the shows, returning a component for each anime:
<Promos>
{new.map((anime, index) => (
<Anime key={anime.mal_id} index={index}></Anime>))}
</Promos>
And for each Anime component, I have a useEffect which uses useDispatch for every new id
const Anime = ({ id, index }) => {
const dispatch = useDispatch();
const loadDetailHandler = () => {
// eslint-disable-line react-hooks/exhaustive-deps
dispatch(loadDetail(id));
useEffect(() => {
loadDetailHandler(id);
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const promo = useSelector((state) => state.detail.promo);
const isLoading = useSelector((state) => state.detail.isLoading);
return (
<PromoBox
style={
!isLoading
? { backgroundImage: `url("${promo[index][0].image_url}")` }
: null
}
></PromoBox>);
};
Here is how my promoReducer looks like:
const initState = {
promo: [],
isLoading: true,
};
const promoReducer = (state = initState, action) => {
switch (action.type) {
case "LOADING_PROMO":
return {
...state,
isLoading: true,
};
case "GET_DETAIL":
return {
...state,
promo: [...state.promo, action.payload.promo],
isLoading: false,
};
default:
return { ...state };
}
};
export default promoReducer;
and here is the promoAction:
export const loadPromo = (id) => async (dispatch) => {
dispatch({
type: "LOADING_PROMO",
});
const promoData = await axios.get(get_promos(id));
dispatch({
type: "GET_DETAIL",
payload: {
promo: promoData.data.promo,
},
});
};
While it does return the promo data as the action is dispatched, the problem is that in some instances of dispatching, no data is returned. Here is a screenshot from redux devtools to show what I mean:
and I was trying to get the promos of all the new anime, in which I was expecting to get 50 results of promo data. In devtools, you can see I only got 9 of them. This is followed by an error 429 (too many requests):
How can I resolve this issue? And is there a better way to do this, because this seems like bad practice:

Well it seems that you're limited by the api itself and it's threshold for the number of request per unit of time. There should probably be a request that allows you to pass multiple anime ids to get request in order to avoid requesting details for each anime individually.

Related

React hooks, component is reading old data from Redux

My component is set to display a list of books, as card thumbnails. Each item from the list of books is generated by this component.
Each Card has a favorites icon, when clicking it adds the book to favoriteTitles array. By pressing again on the favorites icon it removes it from the list.
const Card = ({ title, history }) => {
const dispatch = useDispatch();
const { favoriteTitles } = useSelector(({ titles }) => titles);
const { id, name, thumbnail } = title;
const [favorite, setFavorite] = useState(favoriteTitles?.some(item => item.titleId === title.id));
const handleFavoriteClick = () => {
const isFavorite = favoriteTitles?.some(item => item.titleId === title.id);
if (isFavorite) {
dispatch(removeFavoriteTitle(title));
setFavorite(false);
} else {
dispatch(addFavoriteTitle(title));
setFavorite(true);
}
};
return (
<CardContainer>
<Thumbnail thumbnail={thumbnail} />
{name}
<FavesIcon isActive={favorite} onClick={handleFavoriteClick} />
</CardContainer>
);
};
The issue with this component is when you press once on FavesIcon to add, and if you changed your mind and want to remove it and press right away again, the favoritesTitles array still has the old value.
Let's suppose our current favorites list looks like this:
const favoritesTitles = [{titleId: 'book-1'}];
After pressing on favorites icon, the list in Redux gets updated:
const favoritesTitles = [{titleId: 'book-1'}, {titleId: 'book-2'}];
And if I press again to remove it, the favoritesTitles array inside the component is still the old array with 1 item in it. But if I look in Redux the list updated and correct.
How component should get the updated Redux value?
Update
I have specific endpoints for each action, where I add or remove from favorites:
GET: /users/{userId}/favorites - response list eg [{titleId: 'book-1'}, {titleId: 'book-2'}]
POST: /users/me/favorites/{titleId} - empty response
DELETE: /users/me/favorites/{titleId} - empty response
For each action when I add or remove items, on success request I dispatch the GET action. Bellow are my actions:
export const getFavoriteTitles = userId =>
apiDefaultAction({
url: GET_FAVORITE_TITLES_URL(userId),
onSuccess: data => {
return {
type: 'GET_FAVORITE_TITLES_SUCCESS',
payload: data,
};
},
});
export const addFavoriteTitle = (userId, id) => (dispatch, getState) => {
return dispatch(
apiDefaultAction({
method: 'POST',
url: SET_FAVORITE_TITLES_URL,
data: {
titleId: id,
},
onSuccess: () => {
dispatch(getFavoriteTitles(userId));
return { type: 'SET_FAVORITE_TITLE_SUCCESS' };
},
})
);
};
My reducers are pretty straight forward, I'm not mutating any arrays. Since only GET request is returning the list of array, I don't do any mutating in my reducers:
case 'GET_FAVORITE_TITLES_SUCCESS':
return {
...state,
favoriteTitles: action.payload,
};
case 'SET_FAVORITE_TITLE_SUCCESS':
return state;
case 'DELETE_FAVORITE_TITLE_SUCCESS':
return state;
It seems that by the time you click FavesIcon second time after adding to favourites, GET: /users/{userId}/favorites request is still pending and favoriteTitles list is not updated yet. That's why the component still contains an old value.
You need to update favoriteTitles list right away after triggering addFavoriteTitle or removeFavoriteTitle actions, without waiting GET_FAVORITE_TITLES_SUCCESS action to be dispatched. This pattern is called 'Optimistic UI':
export const toggleFavorite = itemId => {
return {
type: 'TOGGLE_FAVORITE',
payload: { itemId },
};
}
export const addFavoriteTitle = (userId, id) => (dispatch, getState) => {
dispatch(toggleFavorite(id));
return dispatch(
...
);
};
export const removeFavoriteTitle = (userId, id) => (dispatch, getState) => {
dispatch(toggleFavorite(id));
return dispatch(
...
);
};
And your reducer can look something like this:
case 'TOGGLE_FAVORITE':
return {
...state,
favoriteTitles: state.favoriteTitles.map(item => item.titleId).includes(action.payload.itemId)
? state.favoriteTitles.filter(item => item.titleId !== action.payload.itemId)
: [...state.favoriteTitles, { titleId: action.payload.itemId }],
};
UPD. Please, check out a minimal working sandbox example

Why my data is flickering between previous and new when state is updated in ReactJS?

I'm working on a SPA with data fetch from the Star Wars API.
In the characters tab of the project, the idea is to display the characters per page and you can click next or prev to go to page 2, 3, etc. That works, but! the character names flicker everytime the page changes, it doesn't happen after the first click, but if you keep going, it happens more and more.
I tried to fixed it by cleaning the state before rendering again, but it's not working. The data is first fetched after the component mounts, then when the btn is clicked I use the componentwillupdate function to update the character component.
You can see the component here: https://github.com/jesusrmz19/Star-Wars-App/blob/master/src/components/Characters.js
And the live project, here: https://starwarsspa.netlify.app/#/Characters
see if this solves your problem
class Characters extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
webPage: "https://swapi.dev/api/people/?page=",
pageNumber: 1,
characters: [],
};
this.fetchHero = this.fetchHero.bind(this);
}
async fetchHero(nextOrPrev) {
//nextOrPrev values-> 0: initial 1: next page -1:prev page
let pageNum = this.state.pageNumber + nextOrPrev;
try {
const response = await fetch(this.state.webPage + pageNum);
const data = await response.json();
const characters = data.results;
this.setState({
pageNumber: pageNum,
characters,
isLoaded: true,
});
} catch (error) {
this.setState({
pageNumber: pageNum,
isLoaded: true,
error,
});
}
}
componentDidMount() {
this.fetchHero(0);
}
/*you don't need this-> componentDidUpdate(prevState) {
if (this.state.pageNumber !== prevState.pageNumber) {
const fetchData = async () => {
const response = await fetch(
this.state.webPage + this.state.pageNumber
);
const data = await response.json();
this.setState({ characters: data.results });
};
fetchData();
}
}*/
getNextPage = () => {
if (this.state.pageNumber < 9) {
this.fetchHero(1);
}
};
getPrevPage = () => {
if (this.state.pageNumber > 1) {
this.fetchHero(-1);
}
};
render(
// the rest of your code
)
}
I took a look at your Characters component and spent some time refactoring it to React Hooks to simplify it.
Instead of making multiple requests, I created a function that does one request and assigns a state when needed. Plus, I would use status instead of isLoading so that you can render the content better based on the status that your component is currently on.
Also, I used useEffect which does the same thing that componentDidMount + componentDidUpdate does. I then used the request function in there and added pageNumber as a dependency which means that the request will be made only when the pageNumber changes. In your case, it will change only if you press previous or next buttons.
I also simplified your getPrev and getNext page functions. Finally, I rendered the page content based on the status.
I've pulled your project down and run it locally and I cannot see the flickers anymore. Instead, it shows loading screen while it fetches the data and then nicely renders what you need.
Hope that helps. I would also advise starting looking at React Hooks as they make using React way simpler and it's a modern way to develop React applications as well. I've refactored your Characters component and if you want then use this as an example of how to refactor from Class Components to React Hooks.
import React, { useState, useEffect } from "react";
import Character from "./Character";
const baseUrl = "https://swapi.dev/api/people/?page=";
const Characters = () => {
const [state, setState] = useState({
characters: [],
pageNumber: 1,
status: "idle",
error: null,
});
const { characters, pageNumber, status, error } = state;
const fetchData = async (pageNumber) => {
setState((prevState) => ({
...prevState,
status: "fetching",
}));
await fetch(`${baseUrl}${pageNumber}`).then(async (res) => {
if (res.ok) {
const data = await res.json();
setState((prevState) => ({
...prevState,
characters: data.results,
status: "processed",
}));
} else {
setState((prevState) => ({
...prevState,
characters: [],
status: "failed",
error: "Failed to fetch characters",
}));
}
});
};
useEffect(() => {
fetchData(pageNumber);
}, [pageNumber]);
const getNextPage = () => {
if (pageNumber !== 9) {
setState((prevState) => ({
...prevState,
pageNumber: prevState.pageNumber + 1,
}));
}
};
const getPrevPage = () => {
if (pageNumber !== 1) {
setState((prevState) => ({
...prevState,
pageNumber: prevState.pageNumber - 1,
}));
}
};
return (
<div>
{status === "failed" ? (
<div className="charact--container">
<div className="loading">{error}</div>
</div>
) : null}
{status === "fetching" ? (
<div className="charact--container">
<div className="loading">Loading...</div>
</div>
) : null}
{status === "processed" ? (
<div className="charact--container--loaded">
<h1>Characters</h1>
<button onClick={getPrevPage}>Prev Page</button>
<button onClick={getNextPage}>Next Page</button>
<ul className="characters">
{characters.map((character, index) => (
<Character details={character} key={index} index={index} />
))}
</ul>
</div>
) : null}
</div>
);
};
export default Characters;

Losing Local Storage on Page Refresh in React/Redux

I'm using React and Redux and storing data in a loggedUser variable upon user login.
my login reducer looks like this:
const loginReducer = (state = null, action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};
export const fetchUser = () => {
return dispatch => {
const userStr = window.localStorage.getItem("loggedVintageUser");
const user = JSON.parse(userStr);
if (user) {
dispatch({ type: "SET_USER", data: user });
}
};
};
export const setUser = data => {
return dispatch => {
dispatch({ type: "SET_USER", data });
};
};
export const login = data => {
return async dispatch => {
const user = await loginService.login({
username: data.username,
password: data.password
});
window.localStorage.setItem("loggedVintageUser", JSON.stringify(user));
dispatch({ type: "SET_USER", data: user });
};
};
In my core App component i'm dispatching the fetchUser and setUser creators
useEffect(() => {
fetchUser();
}, [props.fetchUser]);
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem("loggedVintageUser");
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON);
props.setUser(user);
userService.setToken(user.token);
}
}, []);
I'm displaying a list of favorite items for a user and when i go to refresh the page, i'm getting the following error:
TypeError: Cannot read property 'favorites' of null
Here is relevant code for my Favorites component. The error is triggered on the loggedUser.favorites data. I can see when visiting the favorites page, the loggedUser field is there and data displays fine but on refresh the loggedUser variable turns to null.
const searchCards = ({ loggedUser, search }) => {
const favorites = loggedUser.favorites;
console.log("FAVORITES", favorites);
return search
? favorites.filter(a =>
a.title
.toString()
.toLowerCase()
.includes(search.toLowerCase())
)
: favorites;
};
const Cards = props => {
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const [filteredData, setData] = useState(props.cardsToShow);
const mapStateToProps = state => {
return {
baseball: state.baseball,
loggedUser: state.loggedUser,
page: state.page,
entries: state.entries,
query: state.query,
pageOutput: state.pageOutput,
search: state.search,
cardsToShow: searchCards(state)
};
};
const mapDispatchToProps = {
searchChange,
fetchData,
updateUser
};
I tried to add this before i render the data, but it's not working
if (!props.loggedUser) return null;
How can i retain that state if a user is refreshing the page. The odd part is that on my home page where i have a similar sort of display a refresh isn't causing the same problems.
check once loggedUser is exist in state or not. Print state using console.log(state). you may also open inspect tool and go to application tab and click on local storage, you will get localStorage data.
Well, i figured this out and got some help from this post here. Redux store changes when reload page
My loggedUser state was disappearing after reload, so i just loaded the inital state for loggedUser pulling the data from the local storage:
function initState() {
return {
token: localStorage.token,
firstName: localStorage.firstName,
id: localStorage.id,
favorites: localStorage.favorites,
username: localStorage.username
};
}
const loginReducer = (state = initState(), action) => {
switch (action.type) {
case "SET_USER":
if (action.data) userService.setToken(action.data.token);
return action.data;
default:
return state;
}
};

how to map nested array within a mapped array in reducer

TLDR:How would i better map an array within a initalState prop
The following code im trying to attempt is to retrieve all postIds from the like array, and compare post.id to like.postId
However
like.postId is undefined.
post.id is available on the console log, the difference between posts, and likes is that posts are mapped on the client side, and likes are not. So im trying to do the mapping in the reducer because i would need to get amount of like counts stored for each post, and map it to its respected post.id.
The reason why i would need to set the values within the likes initialState is because i would need to use this following action to upvote posts
case ADD_LIKE:
// console.log(action.id) // renders post id which is 2
// console.log(state.posts) // logs posts array
// console.log(state.posts)
return {
...state,
likes: state.likes + 1
};
if i do something like this like[0].postId it will only get the values for that like.id only. I need to be able to get all of likes from all posts, and compare like.postId to post.id and then set the value.
And retrieve the count like
{this.props.likes}
just to get an idea what the array looks like.
This is example posts array, and within the Posts array, you have the likes array.
Here is how im calling posts
export const GetPosts = () => {
return (dispatch, getState) => {
return Axios.get('/api/posts/myPosts')
.then( (res) => {
const data = res.data
const likes = res.data // gets the first item within array, and shows likes.
const myLikes = likes.map( (post) => {
return post.Likes
})
console.log(myLikes)
dispatch({type: GET_POSTS, data, myLikes})
})
}
}
reducer
export default (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
console.log(action.data[0].Likes.length)
return {
...state,
posts: action.data, // maps posts fine
// set likes to but it only gets the first post, when it should get all posts
likes: action.data.map( (post) => {
action.myLikes.map( (like) => {
// if(post.id === like.postId){
console.log(like) // renders an array of likes for all posts
console.log(like.postId) // renders undefined,
// }
})
})
}
}
this is how its being mapped
PostList.js
render(){
const {posts} = this.props;
return (
<div>
{posts.map((post, i) => (
<Paper key={post.id} style={Styles.myPaper}>
{/* {...post} prevents us from writing all of the properties out */}
<PostItem
myTitle={this.state.title}
editChange={this.onChange}
editForm={this.formEditing}
isEditing={this.props.isEditingId === post.id}
removePost={this.removePost}
{...post}
/>
</Paper>
))}
</div>
)
}
GetPosts action is called within this component
class Posts extends Component {
state = {
posts: [],
loading: true,
isEditing: false,
}
async componentWillMount(){
await this.props.GetPosts();
this.setState({ loading: false })
const reduxPosts = this.props.myPosts;
const ourPosts = reduxPosts
console.log(reduxPosts); // shows posts line 35
}
render() {
const {loading} = this.state;
const { myPosts} = this.props
if (!this.props.isAuthenticated) {
return (<Redirect to='/signIn' />);
}
if(loading){
return "loading..."
}
return (
<div className="App" style={Styles.wrapper}>
<h1> Posts </h1>
<PostList posts={myPosts}/>
</div>
);
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.user.isAuthenticated,
myPosts: state.post.posts
})
const mapDispatchToProps = (dispatch, state) => ({
GetPosts: () => dispatch( GetPosts())
});
export default withRouter(connect(mapStateToProps,mapDispatchToProps)(Posts));
When you use Array.map() it will create a new array with the results of calling a provided function on every element in the calling array, so you will get an array of arrays (array of each post's likes array) in order to solve your issue and get an array of likes you need to use the reducer function as follows:
export const GetPosts = () => {
return (dispatch, getState) => {
return Axios.get('/api/posts/myPosts')
.then( (res) => {
const data = res.data
const likes = res.data // gets the first item within array, and shows likes.
const myLikes = likes.reduce( (acc,post) => {
return acc.concat(post.Likes)
},[])
console.log(myLikes)
dispatch({type: GET_POSTS, data, myLikes})
})
}}

Is it efficient to dispatch data from just firebase listeners?

const listRef = firebase.database().ref();
const reducer = (state = {}, action) => {
switch (action.type) {
case: "GET_LIST" : {
const { list } = action;
return { list };
};
case: "ADD_ELEMENT" : {
const { elementId } = action;
return { ...state, [elementId]: true };
};
default : return state;
};
};
const getList = list => ({
type: "GET_LIST", list
});
const addElement = elementId => ({
type: "ADD_ELEMENT", elementId
})
const getListFromDb = () =>
async dispatch => listRef.once("value", snap => {
dispatch(
getList(
list
)
);
});
const listenToElementAdded = () =>
async dispatch => listRef.on("child_added", ({ key: elementId }) => {
dispatch(
addElement(
element
)
);
});
const addElementToDb = () =>
async dispatch => dispatch(
addElement(
listRef.push().key
)
);
const Component = ({
list,
getListFromDb,
listenToElementAdded,
addElementToDb
}) => {
useEffect(() => getListFromDb(), []);
useEffect(() => listenToElementsAdded());
return (
<div>
{ Object.keys(list).map(id => <p>{id}</p>) }
<p onClick={addElementToDb}>Add</p>
</div>
);
};
const mapStateToProps = list => ({ list });
const mapStateToProps = {
getListFromDb,
listenToElementAdded,
addElementToDb
};
export default connect(mapStateToProps, mapStateToProps)(Component);
The simplified example above shows what I mean. The question is: is firebase listeners fast enough to replace data pre-dispatching with redux?
If we want to build an application, where we want to refresh data live which comes from outside, listeners can help. But if we pre-dispatch the data we get, we're dispatching twice. Also, if we're using just listeners, and dispatch from there, it can make the code, and the working of it much more cleaner. But what if we have loads of data inside the database? Will the listeners be fast? Is it efficient to handle dispatching just through listeners, or is it worth for it to pre-dispatch in place?
I would recommend having your firebase event listeners call redux-thunk actions that then manipulate and dispatch data to your application. There's nothing wrong with that if done properly.
However consider some not better but different approaches.
There's react-redux-firebase which allows for tight-nit coupling with your applications store. However if that is too much of an integration (you're literally melding your app with firebase), then go with the former.
Regardless I recommend you search some more for content about using firebase with your redux app.

Categories