How should I update the Redux state in my React App? - javascript

I'm working on a React project, and I have a section with "Saved Games".
The "Saved Games" section maps the "Saved Games" state.
This is what it looks like:
let SavedGamesList = <h1>Loading...</h1>;
if (this.props.savedGamesState.length < 1) {
SavedGamesList = <StyledNotSavedGames>Such empty</StyledNotSavedGames>;
}
if (this.props.savedGamesState.length >= 1) {
SavedGamesList = this.props.savedGamesState.map(game => (
<GameCard
key={game.game}
title={game.title}
hoursViewed={game.hours_viewed}
saved={true}
/>
));
}
When I try to delete a game, it deletes a random one not the one I clicked, or multiple games at once.
This is what the "GameCard" (Where the delete button is) looks like:
deleteGame = () => {
let gameName = this.props.title;
this.props.deleteGame(gameName); //This is the Redux dispatch
console.log(this.props.savedGamesState);
};
And this is how I try to change the state in the Reducer:
case actionTypes.DELETE_GAME:
let updatedGames = [
...state.savedGames.splice(
state.savedGames.findIndex(e => e.title === action.payload),
1
)
];
return {
...state,
savedGames: updatedGames
};
Edit: Dispatch to props:
deleteGame: (res) => dispatch({type: actionType.DELETE_GAME, payload: res})
I also noticed that the last game in the list can't be deleted, the state updated but the component doesn't re-render so it's not disappearing.
Something in the reducer probably is wrong, what do you think?

I think your problem is that the return value of splice is the array of removed games,
try something like that (note you can also use the filter method):
case actionTypes.DELETE_GAME:{
let updatedGames = [
...state.savedGames
];
updatedGames.splice(
updatedGames.findIndex(e => e.title === action.payload),
1
)
return {
...state,
savedGames: updatedGames
};
}
also I think it is better for you to use the key of the game to remove it and not the title unless the title is unique

Related

How to remove items from a list in React Function Component

I am trying to remove object from a list in react but have no luck. I'm using react hook to maintain state:
const [temp, setTemp] = useState([]);
useEffect(() => {
call service().then(response => {
let list = response.data.list // [{name:"test"}, {name:"xyz"}]
setTemp(list); // empty
handleRemove(name);
console.log(temp) // empty
}, []);
function handleRemove(name) {
const newList = temp.filter((item) => item.name !== name);
console.log("remain lane--"+newList)
setTemp(newList);
}
I don't know what's happing but it is not setting the temp list.
I tried multiple ways to remove element from the list.
React useState is Async operation and you can not see the update immediately.
Actually your code works fine but you can't see it!. To see the changes that made to the state I changed the code as below:
React.useEffect(() => {
setTemp(list);
}, []);
React.useEffect(() => {
console.log(temp);
}, [temp]);
function handleRemove(s_name) {
const newList = temp.filter((item) => item.name !== s_name);
//console.log("remain lane--"+newList);
setTemp(newList);
}
return (
<div>
<button
onClick={() => {
handleRemove("blue");
}}
>
Remove 'blue'
</button>
</div>
);
I put the handleRemove function in a button click event to perform this action in different time as you click on it.
Please see the updated code in CodeSandBox:
Here is the CodeSandbox:
CodeSandbox
In handleRemove you have to use temp.filter instead of list.filter
list isn't in the scope of the useEffect so handleRemove can't access it, but temp can be accessed since it's in the scope above the useEffect and handleRemove.
So once you've fetched the data and assigned it to temp
list = temp
function handleRemove(name) {
const newList = temp.filter((item) => item.name !== name);
console.log("remain lane--"+newList)
setTemp(newList);
}

Unable to filter out redux state based on values from another local state

So I have this API data, which is being stored in redux store. The first element in the redux state data is then used in useEffect to set a local state used in the initial render. Now in a button click handler function, I'm trying to use that local state to filter out the data from redux state. Then the first element from filtered data should be pushed to the local state.
I'll explain the code here. Suppose I'm getting the API data from redux store like this:
const categoriesState = useSelector( state => state.getCats );
const { categories } = categoriesState;
Then in useEffect I use the first element from categories to be stored in a local state:
const [ catItems, setCatItems ] = useState( [] );
useEffect(() => {
if ( categories && categories.length !== 0 ) {
setCatItems( [ categories[ 0 ] ] );
}
}, [] );
catItems is then used to render something. But there is also a button that, upon clicking, will add a new category to catItems from categories state but leave out the item which is already in it.
Here's the click handler:
const handleAddCat = ( e ) => {
if ( categories && categories.length !== 0 ) {
const catCopy = [ ...catItems ];
const filterCategories = categories.filter( item => {
for ( let el of catCopy ) {
return item.category !== el.category;
}
});
catCopy.push( filterCategories[ 0 ] );
setCatItems( catCopy );
}
}
On the first click, the filter works perfectly. But on the second button click, I get the same data. For example, if the local state's initial data is 1, then on the first button click, I get both 1 and 2. But on second button click I get 1, 2 and 2. 2 is being added on subsequent clicks. Whereas, 2 should be filtered out. Then 3, then 4 and so on.
What am I doing wrong here?
You can just find the mismatched category instead of ( creating the filterCategories and then picking the first one ).
Here is the implementation.
const handleAddCat = ( e ) => {
if ( categories && categories.length !== 0 ) {
const newCategory = categories.find(category => !cat.includes(category));
if(newCategory) setCatItems((prevItems) => [...prevItems, newCategory]);
}
}
for loop does not return anything from filter. In your case you can do something like this:
const filterCategories = categories.filter((item) => {
return Boolean(catCopy.find((el) => item.category !== el.category));
});

I am changing the state, but subscribed components are not rendered

I find the index number of the element, change a value in that index in order not to break the order in the array, dispatch it as a new array but subscribed components are not rendered.
var userPhotoIndex = userPhotos.findIndex(p => p.photoId === photoId)
if (userPhotoIndex > -1) {
userPhotos[userPhotoIndex].likeCount -= 1;
dispatch(getUserPhotosSuccess([...userPhotos]))
}
Actually, the state changes in redux extension, but the subscribed component does not.
Hook
function LikeButton({ photo, photoId, setlikeCount }) {
const [isLike, setIsLike] = useState(false)
const dispatch = useDispatch()
const history = useHistory()
const isLogged = useSelector(state => state.isLoggedReducer);
const userPhotos = useSelector(state => state.userReducer.userPhotos);
const onClick = () => {
var fd = new FormData();
fd.append("photoId", photoId)
if (!isLike) {
axios.post(LIKE_API_URL, fd, { headers: authHeaderObj() }).then(() => {
setIsLike(!isLike);
setlikeCount(!isLike);
var userPhotoIndex = userPhotos.findIndex(p => p.photoId === photoId)
if (history.location.pathname.includes("me/" + profileFlowState.Likes)) {
if (userPhotoIndex > -1) {
userPhotos[userPhotoIndex].likeCount += 1;
dispatch(getUserPhotosSuccess([...userPhotos]))
}
}
}).catch(err => redirectErrPage(err, dispatch));
}
else {
axios.delete(deleteLikePath(photoId), { headers: authHeaderObj() }).then(() => {
setIsLike(!isLike);
setlikeCount(!isLike);
var userPhotoIndex = userPhotos.findIndex(p => p.photoId === photoId)
if (history.location.pathname.includes("me/" + profileFlowState.Likes)) {
if (userPhotoIndex > -1) {
userPhotos[userPhotoIndex].likeCount -= 1;
dispatch(getUserPhotosSuccess([...userPhotos]))
}
}
}).catch(err => redirectErrPage(err, dispatch));
}
}
useEffect(() => {
if (isLogged) {
axios.get(getIsLikePath(photoId), { headers: authHeaderObj() }).then(res => setIsLike(res.data))
}
}, [isLogged, photoId])
if (!isLogged) {
return <Button onClick={() => history.push("/login")} variant="outline-primary" style={{ borderRadius: 0 }} className="btn-sm">
<i className="fa fa-thumbs-up" style={{ fontSize: 16 }}></i> Beğen</Button>
}
return (
<Button onClick={onClick} variant={isLike ? "primary" : "outline-primary"} style={{ borderRadius: 0 }} className="btn-sm">
<i className="fa fa-thumbs-up" style={{ fontSize: 16 }}></i> Beğen</Button>
)}
Below I am rendering a subscribed hook.
Subscribed
function UserPhotos({ userId }) {
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(true)
const setFalseIsLoading = () => setIsLoading(false);
useEffect(() => { dispatch(getUserPhotosApi(userId, setFalseIsLoading)); }, [userId, dispatch])
const userPhotos = useSelector(state => state.userReducer.userPhotos)
return <div className="mt-3">{isLoading ? <Loading /> : <div>
<MapPhotoCard removeButton={true} refreshPhotos={(id) => {
dispatch(getUserPhotosSuccess([...userPhotos.filter(p => p.photoId !== id)]))
}} photos={userPhotos} /></div>}
</div>}
Problem: Mutating The State
Copying Too Late
dispatch(getUserPhotosSuccess([...userPhotos]))
You are dispatching your action with a copy of the array, but that's after you already mutated the state so it does not help.
Shallow Copies
Your array contains a bunch of objects, but behind the scenes your array contains references to the objects. When you clone the array with [...userPhotos] you get a new array which contains all of the same object references. So you when you set a property like likeCount on one of these objects, you are also setting that property on that object in the redux state.
Solution: New Object in a New Array
In order to avoid mutations, we must return a new array with a new object for the photo that you are updating. It is not necessary or desirable to perform a deep copy of the entire array. The photos which you are not changing can stay the same. A deep copy would cause unnecessary re-rendering of data which has not changed.
A common solution here is to use Array.prototype.map(). For the photo which matches the photoId we return a new photo object. All other elements of the array stay the same.
const newPhotos = userPhotos.map((photo) =>
photo.photoId === photoId
? {
...photo,
likeCount: photo.likeCount + 1
}
: photo
);
dispatch(getUserPhotosSuccess(newPhotos));
Suggestion: More Specific Actions
This sort of updating is usually done in the reducer rather than in the component. Why is it necessary to return an array of every photo in response to a "like" or "unlike" action?
I recommend dispatching actions with the minimal amount of data possible. Then in your reducer you can apply these changes. Depending on the data structure of your state, you might need to know the user in order to update the photos for that user or you might just need to know the photo id.
Here we are passing the changed properties of a photo. The reducer will combine them with the existing properties. This action is good because it has a lot of uses while still requiring little data.
{ type: "UPDATE_PHOTO", payload: { photoId: _id_, changes: { likeCount: _newCount_ } }
You can make liking into its own action. You could have separate actions for like and unlike which you handle separately in the reducer. This is not my personal favorite.
{ type: "LIKE_PHOTO", payload: { photoId: _id_ } }
{ type: "UNLIKE_PHOTO", payload: { photoId: _id_ } }
The implementation in the reducer is a lot easier if you have a shared action for like and unlike with a property change that has a value of 1 or -1. The reducer handles both the same way by adding the change to the existing like count. You can combine this with action creator functions to get the separation.
({ type: "UPDATE_LIKES", payload: { photoId: _id_, change: 1 } })
const likePhoto = (photoId) => ({ type: "UPDATE_LIKES", payload: { photoId, change: 1 } })
const unikePhoto = (photoId) => ({ type: "UPDATE_LIKES", payload: { photoId, change: -1 } })
There is an important issue that I want to mention:
Changing redux state
In this case you have a state variable, namely, userPhotos which resides in redux store. userPhotos is a variable of type array, and changing the likeCount via the line userPhotos[userPhotoIndex].likeCount -= 1; is an anti-pattern. Ideally, you want to clone the array first with one of two ways:
Using split operator
const userPhotosCopy = [...userPhotos]
userPhotosCopy[userPhotoIndex].likeCount -= 1
/*or userPhotosCopy.splice(userPhotoIndex, 1, {...userPhotosCopy[userPhotoIndex], likeCount: likeCount - 1 }]*/
Using lodash
import _ from 'lodash'
const userPhotosCopy = _.cloneDeep(userPhotos)
Fetching on useEffect
Once you update your userPhotos, in your Subscribed component, ideally you want a pattern like this:
const userPhotos = useSelector(state => state.userReducer.userPhotos);
return(
// use userPhotos in whatever way you like
)
Not sure what is the getUserPhotosSuccess action is doing, but it seems like you are not getting the updated userPhotos object in your subscribed component

componentWillReceiveProps() duplicates the elements from map

componentWillReceiveProps = (newProps) => {
console.log("data ");
let apiData = newProps.apiData;
if (apiData.topProfiles && apiData.topProfiles.success) {
let therapists = apiData.topProfiles.therapists
let hasMore = true
if (!therapists.length) {
hasMore = false
}
this.setState(() => ({
therapists: this.state.therapists.concat(therapists),
hasMore: hasMore,
pageLoading: false
}))
} else if (apiData.therapistsByName && apiData.therapistsByName.success) {
let therapists = apiData.therapistsByName.therapists,
resTitle = therapists.length ?
`Results for "${this.state.searchName}"`
: `Looks like there are no results for "${this.state.searchName}"`
this.setState(() => ({
therapists: therapists,
hasMore: false,
pageLoading: false,
resultsTitle: resTitle
}))
}
I read about componentWillReceiveProps and is not safe anymore. How can I implement it much more efficient.
I have a function which render a list of therapists, but if I am in therapist pages where the content is rendered, and click on "Specialists" (button path) again in Header, the therapist list duplicates.
renderTherapists = () => {
console.log("this state therapists: ", this.state.therapists)
let items = this.state.therapists.map( (t, idx) => (
<TherapistCard therapist={t} key={idx} />
))
return (
<div ref={0} className="therapist-list">
{ items }
</div>
)
}
console log after header button press
The code you have keeps concatenating therapists to the therapists array on the state because componentWillReceiveProps will run regardless if the props changed or not, as per React documentation.
The real question is, should you store something in your state if it's already available as a prop. If you have a list of therapists as a prop, why not simply render it instead of duplicating it and having to keep those two in sync?
If you really have to keep it in the state for whatever reason, you can use componentDidUpdate in which you can compare equality of previous and current props and set the state accordingly.
You need to compare the coming data with this.state.therapists state in componentWillReceiveProps before updating therapists state because every time componentWillReceiveProps is running and you concatenate even if the same data coming.

how to return all store when filter in react js and redux

i have an issue trying to create a "typehead" funcitonality in my app, i have an "input" that listen to onChange, and that onChange is calling to a Redux reducer that search for a tag in hole store, i need to retrive all matches with my search, over here everything is ok, but when i delete my search, my hole store is equals to my filtered results, and i want that when my search is empty it returns hole my store. (gif and code)
case 'SEARCH_BY_TAG':
let tag = action.tag
let filtered = state.slice(0)
if(tag != ""){
const copied = state.filter(item => {
return item.tags.find(obj => {
if(obj.name.indexOf(tag) > -1){
return true
}
})
})
return filtered = copied.filter(Boolean)
}else{
return filtered
}
break;
Instead of filtering things out inside your reducer, do it directly on render(), there is nothing wrong with that.
You can still use the SEARCH_BY_TAG action to keep track of the search keyword and use it to apply the filter when rendering your list.
I think you should refactor your state to change it from this:
[ item1, item2, item3, ... ]
to this:
{
query: '',
options: [ item1, item2, item3, ... ]
}
This way you can do what #Raspo said in his answer - do the filtering of the options in your render function.
Currently when you change the text in the search field, you are dispatching this action:
{
type: 'SEARCH_BY_TAG',
tag: 'new query'
}
I think you should change the action name, and the reducer code, to look more like this:
// note this is almost exactly the same as the old action
{
type: 'CHANGE_AUTOCOMPLETE_QUERY',
query: 'new query'
}
and the reducer could then change to this:
case CHANGE_AUTOCOMPLETE_QUERY:
return Object.assign({}, state, {
query: action.query
})
Note that in the reducer case I just wrote, the options part of the state isn't changed at all. It remains constant.
Now let's assume your current render function looks something like this:
const options = reduxState // not sure exactly how you get the state but whatever
return (
<div>
{options.map(option => {
<Option option={option} />
})}
</div>
)
This code relies on getting the state as an array. You could change it in the new setup to do this:
const query = reduxState.query
const options = reduxState.options.filter(item => {
return item.tags.find(obj => {
if(obj.name.indexOf(query) > -1){
return true
}
})
})
return (
<div>
{options.map(option => {
<Option option={option} />
})}
</div>
)

Categories