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
Related
I would like to write an App that allows multiple components to add and remove entries to a set by accessing the global context.
So far I am initialising a Set with useState and feeding the state and the changeState values down to the child components using a context provider, I am then adding and removing items from the Set based on the props passed down into that component.
The code:
The main App file
/App.js
export const myListContext = createContext(new Set());
function App() {
const [myList, changeMyList] = useState(() => new Set());
const Alphabet = {
A: 'a',
B: 'b',
};
useEffect(() => {
console.log(myList);
}, [myList]);
return (
<myListContext.Provider value={{ myList, changeMyList }}>
<div className="my-box">
<Checkbox myEnum={Alphabet.A} title={"Add A to Set"}></Checkbox>
<Checkbox myEnum={Alphabet.B} title={"Add B to Set"}></Checkbox>
</div>
</myListContext.Provider>
);
}
/Checkbox.js
const Checkbox = ({ myEnum, title }) => {
const { myList, changeMyList } = useContext(myListContext);
const [index, setIndex] = useState(0);
function changeIndex() {
setIndex(index + 1);
if (index >= 3) {
setIndex(1);
}
}
function addListItem(item) {
changeMyList((prev) => new Set(prev).add(item));
}
function removeMyListItem(item) {
changeMyList((prev) => {
const next = new Set(prev);
next.delete(item);
return next;
});
}
function filterItem(item) {
changeIndex();
if (index === 0) {
addListItem(item);
}
if (index === 1) {
removeMyListItem(item);
}
}
return (
<div className="my-checkbox">
<div
className="my-checkbox-inner"
onClick={() => {
filterItem(myEnum);
}}
></div>
<div className="the-title">{title}</div>
</div>
);
}
The reason for this set up is to Add or Remove Items based on how many times the user has pressed the div, each component needs to track the status of its own index.
Unfortunately, the effect hook on the top layer of the app reports that myList does not contain the new entries.
Why does changeMyList not have a global impact despite change the state via a context.
What am I doing wrong?
console.log(myList) prints (0) depite clicking the buttons that aught to trigger a new entry based on myEnum.
Thanks!
Set is different than Array and it only contains distinct elements.
The Set object lets you store unique values of any type, whether primitive values or object references, according to MDN
Therefore, your code already worked. If you console log as myList.size will return 2 and [...myList] will return an array ["a", "b"] with an updated values and you can iterate over it to create necessary jsx and etc.
console.log([...myList]); // ["a", "b"]
console.log(myList.size); // 2
I have two api requests that return JSON objects. They return an array of objects.
One API request that I make is fine and allows me to update the state with the response, but the other one (below) doesn't and I don't understand why.
API request to fetch genres list:
async getGenreList() {
const genresResults = await getGenres();
return genresResults;
}
The request:
export const getGenres = async () => {
try {
const response = await axios.get(
"https://api.themoviedb.org/3/genre/movie/list?api_key=<APIKEY>&language=en-US"
);
const { genres } = response.data;
return genres;
} catch (error) {
console.error(error);
}
};
The response is an array of 19 genre objects but this is just an example:
[
{id: 28, name: "Action"},
{id: 12, name: "Adventure"}
]
I then want to update the state like this and pass the response to genreOptions. But it tells me Error: Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead.
componentDidMount() {
this.getGenreList().then((response) => {
console.log(response)
this.setState({ genreOptions: response});
});
}
The below works when i update the state and map over it but I don't want to do that, i want to pass the whole response down so i can map over the data in my component as I need it there to do some data matching.
this.setState({ genreOptions: response.map((genreOption) => {
return genreOption.name
})});
This is the state:
this.state = {
results: [],
movieDetails: null,
genreOptions: [],
};
I want to pass the genreOptions here to genres then map over it in the MovieResults component.
<MovieResults>
{totalCount > 0 && <TotalCounter>{totalCount} results</TotalCounter>}
<MovieList movies={results || []} genres={genreOptions || []} />
</MovieResults>
Why can't I? Any ideas? I have done it for another similar request :S
UPDATE TO SHOW MOVIELIST COMPONENT
export default class MovieList extends React.Component {
render() {
const { movies, genres } = this.props;
const testFunction = (movieGenreIds) => {
const matchMovieGenresAndGenreIds = genres.map((genreId) => {
const matchedGenres = movieGenreIds.find((movieGenre) => {
return movieGenre.id === genreId
})
return matchedGenres // this returns the matching objects
})
const result = matchMovieGenresAndGenreIds.filter(Boolean).map((el)=> {
return el.name
})
return result
}
return (
<MoviesWrapper>
{movies.map((movie) => {
const {
title,
vote_average,
overview,
release_date,
poster_path,
genre_ids
} = movie;
return (
<MovieItem
title={title}
rating={vote_average}
overview={overview}
release={release_date}
poster={poster_path}
movieGenres={testFunction(genre_ids)}
/>
);
})}
</MoviesWrapper>
);
}
}
**** MOVIE ITEM COMPONENT***
export default class MovieItem extends React.Component {
render() {
const { title, overview, rating, release, poster, movieGenres } = this.props;
return (
// The MovieItemWrapper must be linked to the movie details popup
<MovieItemWrapper>
<LeftCont>
<img
className="movie-img"
src={`https://image.tmdb.org/t/p/w500${poster}`}
/>
</LeftCont>
<RightCont>
<div className="movie-title-container">
<h2 className="movie-title">{title}</h2>
<Rating>{rating}</Rating>
</div>
<div>{movieGenres}</div>
<p>{overview}</p>
<p>{release}</p>
</RightCont>
</MovieItemWrapper>
);
}
}
Please follow this steps to fix your code. I'll try yo explain what's happening along the way:
In your main component. Set the state to the value that you really want to pass to your child component. Remember that response will be an array of objects.
componentDidMount() {
this.getGenreList().then((response) => {
this.setState({genreOptions: response});
});
}
In your MovieList component. Please check your testFunction to respect data types. The following code will return you an array of strings containing the names of the genres that are included in the movies genres array.
const testFunction = (movieGenreIds) => {
return genres
.filter((genre) => {
return movieGenreIds.includes(genre.id);
})
.map((genre) => genre.name);
};
In your MovieItem component. (This is were the real problem was)
Instead of:
<div>{movieGenres}</div>
You may want to do something like this:
<div>{movieGenres.join(' ')}</div>
This converts your array into a string that can be rendered. Your error was due to the fact that you were passing there an array of objects that React couldn't render.
If you have any doubt, please let me know.
NOTE: I suggest you to use a type checker to avoid this kind of problems. And to be consistent with your variables naming conventions.
Update based on new information from chat:
In your ExpandableFilters component, you must fix the following piece of code to get the genre name (string). As explained in chat, you can't have objects as a result for a JSX expression ({}), but only primitives that can be coerced to strings, JSX elements or an array of JSX elements.
<GenreFilterCont marginTop>
{filtersShown && (
<ExpandableFiltersUl>
{this.props.movieGenres.map((genre, index) => {
return (
<ExpandableFiltersLi key={index}>
<Checkbox />
{genre.name}
</ExpandableFiltersLi>
);
})}
</ExpandableFiltersUl>
)}
</GenreFilterCont>
Please also note that I've added a key property. You should do it whenever you have a list of elements to render. For more about this I will refer you to the React Docs.
I am looking to modify a property of one of the items in the array using the updater returned from setState. The function is passed as props to the child, who then call this with their own index to change their status.
const tryMarkHabitAsComplete = (index: number) => {
let list: HabitType[] = [...habitList];
let habit = {
...list[index],
isComplete: true,
};
list[index] = habit;
setHabitList({list});
};
When running with
setHabitList(list); the array gets cleared and becomes undefined, so I am unable to use it after I call this function once.
The error that keeps appearing is:
Argument of type '{ list: HabitType[]; }' is not assignable to parameter of type 'SetStateAction<HabitType[]>'. Object literal may only specify known properties, and 'list' does not exist in type 'SetStateAction<HabitType[]>'
I am treating the state as immutable and attempting (I think) to set the state. When debugging, everytime I click I empty the array for my habitsList. Further up, I define the state as:
const [habitList, setHabitList] = useState<HabitType[]>([]);
If I try to setState in other ways, the array becomes undefined or blank and loses information. I should clarify, this is a side project written w/ react-native.
MRE Edit:
2 components in a react-native app, discarding css & irrelevant imports:
Chain.tsx
export type HabitType = {
text: string;
index: number;
isComplete: boolean;
tryMarkHabit: (index: number) => void;
};
const Chain = (props: any) => {
const [habitList, setHabitList] = useState<HabitType[]>([]);
// Set the initial state once w/ dummy values
useEffect(() => {
setHabitList([
{
text: "1",
index: 0,
isComplete: false,
tryMarkHabit: tryMarkHabitAsComplete,
},
{
text: "2",
index: 1,
isComplete: false,
tryMarkHabit: tryMarkHabitAsComplete,
}
]);
}, []);
// Only is previous item is complete, let habit be marked as complete
const tryMarkHabitAsComplete = (index: number) => {
let list: HabitType[] = [...habitList];
let habit = {
...list[index],
isComplete: true,
};
list[index] = habit;
setHabitList(list);
};
let habitKeyCount = 0;
return (
<View style={styles.container}>
{habitList.map((habit) => (
<Habit key={habitKeyCount++} {...habit} />
))}
</View>
);
};
export default Chain;
Habit.tsx
import { HabitType } from "./Chain";
const Habit = ({ text, index, isComplete, tryMarkHabit }: HabitType) => {
const [complete, setComplete] = useState<boolean>(false);
return (
<TouchableOpacity
style={complete ? styles.completeHabit : styles.uncompleteHabit}
onPress={() => tryMarkHabit(index)}
>
<Text style={styles.chainText}>{text}</Text>
</TouchableOpacity>
);
};
export default Habit;
When I press a habit, it calls tryMarkHabitAsComplete successfully, however it appears to clear the array - the array afterwards becomes undefined as far as I can tell.
Issue
If I had to guess I'd say you've a stale enclosure of your habitList state in the callback, which itself is a stale enclosure in your state.
Solution
I suggest to instead use a functional state update in your handler. A functional update allows your state to be updated from the previous state, not the state from the render cycle the update was enqueued in. It's a small, subtle, but important distinction.
I also suggest to not enclose the handler in an enclosure in state either. Just pass it to Habit as a normal callback, this way you avoid any stale enclosures.
const Chain = (props: any) => {
const [habitList, setHabitList] = useState<HabitType[]>([]);
// Set the initial state once w/ dummy values
useEffect(() => {
setHabitList([
{
text: "1",
index: 0,
isComplete: false,
},
{
text: "2",
index: 1,
isComplete: false,
}
]);
}, []);
// Only is previous item is complete, let habit be marked as complete
const tryMarkHabitAsComplete = (index: number) => {
setHabitList(list => list.map((habit, i) => i === index ? {
...habit,
isComplete: true,
} : habit);
};
let habitKeyCount = 0;
return (
<View style={styles.container}>
{habitList.map((habit) => (
<Habit
key={habitKeyCount++}
{...habit}
tryMarkHabit={tryMarkHabitAsComplete}
/>
))}
</View>
);
};
Edit for isComplete Style
Habit should just consume the passed isComplete prop to set the desired CSS style.
const Habit = ({ text, index, isComplete, tryMarkHabit }: HabitType) => {
return (
<TouchableOpacity
style={isComplete ? styles.completeHabit : styles.uncompleteHabit}
onPress={() => tryMarkHabit(index)}
>
<Text style={styles.chainText}>{text}</Text>
</TouchableOpacity>
);
};
I have a react component that uses hooks for state. I have set the initial state for home to {location:null, canCharge: 'yes'}.
I then have a couple of subcomponents that call setHome() to update the pieces of the state they are responsible for.
One sets the location, and the other sets the canCharge property of the home state.
The setter for the ChargeRadioGroup works as expected, only updating the canCharge property and has no effect on the value of location.
The PlacesAutoComplete set however seems to have captured the initial state of home, and after setting a breakpoint inside, I see that it always is called with home: {location:null, canCharge:'yes'}.
I realize I could break this single state into two separate states, one for location and one for canCharge, but I'd like to understand why this is happening instead of implementing a workaround.
export default function VerticalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [home, setHome] = useState({
location: null,
canCharge: "yes"
});
const [work, setWork] = useState({
location: null,
canCharge: "yes"
});
const steps = getSteps();
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepLabel>Where do you live?</StepLabel>
<StepContent>
<Box className={classes.stepContent}>
<PlacesAutocomplete
className={classes.formElement}
name={"Home"}
onPlaceSelected={location => setHome({ ...home, location })}
googleApiKey={"<API_KEY>"}
/>
<ChargeRadioGroup
className={classes.formElement}
label="Can you charge your car here?"
value={home.canCharge}
onChange={event =>
setHome({ ...home, canCharge: event.target.value })
}
/>
The code for the PlacesAutoComplete component can be seen here
I'm guessing this has something to do with the way that this component calls it's onPlaceSelected prop, but I can't figure out exactly what's going on, or how to fix it:
useEffect(() => {
if (!loaded) return;
const config = {
types,
bounds,
fields
};
if (componentRestrictions) {
config.componentRestrictions = componentRestrictions;
}
autocomplete = new window.google.maps.places.Autocomplete(
inputRef.current,
config
);
event = autocomplete.addListener("place_changed", onSelected);
return () => event && event.remove();
}, [loaded]);
const onSelected = () => {
if (onPlaceSelected && autocomplete) {
onPlaceSelected(autocomplete.getPlace());
}
};
Updating my original answer.
Instead of this:
onPlaceSelected={location => setHome({ ...home, location })}
This:
onPlaceSelected={newlocation => setHome( (prevState) => (
{ ...prevState, location:newlocation }
))}
The set state functions can take a value, and object or a function that receives the old state and returns the new state. Because setting state is sometimes asynchronous, object state with members getting set with different calls may result in captured variables overwriting new state.
More details at this link: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
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