I'm creating a recipe application using MERN stack.
The issue I am stuck on is trying to delete an ingredient found in an array, inside a recipe object. My recipe object looks like this:
MongoDB Recipe Object
Each ingredient has a cross next to it that allows you to click on it to remove.
<div>
<ol className='pad5px20px'>
{addIngredients.ingredients.map(data => (
<div className='ingredient-item padTB5px' key={data}>
<li>{data}</li>
<span onClick={() => removeIngredient(data)}>
<i className='fas fa-trash'></i>
</span>{' '}
</div>
))}
</ol>
</div>
The addIngredients and removeIngredient functions look like this:
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
addIngredients.ingredients.push(query);
setIngredients(addIngredients);
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
setIngredients(
addIngredients.ingredients.filter(e => e.ingredients !== data)
);
};
Every time I remove an ingredient from my list I get an error that states "TypeError: Cannot read property 'map' of undefined".
Is there something that i'm missing here? I have been working on this app for the past couple of months now and I am stuck on this particular bit. I thought a better way would be to use Redux as I have been able to delete a whole recipe using a reducer:
case DELETE_RECIPE:
return {
...state,
recipes: state.recipes.filter(recipe => recipe._id !== action.payload),
loading: false
};
but how would I be able to target one particular ingredient?
Any suggestions would be greatly appreciated.
I added notes to your code problems)
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
***STATE MUTATION***
addIngredients.ingredients.push(query);
setIngredients(addIngredients);
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
***You're adding ingredients object instead of addIngredients as you used in addIngredient method***
setIngredients(
addIngredients.ingredients.filter(e => e.ingredients !== data)
);
};
addIngredients.ingredients.filter(e => e.ingredients !== data) returns filtered ingredients instead of addIngredients with filtered ingredients field
How it should be
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
setIngredients({
...addIngredients,
ingredients: [
...addIngredients,
query
]
});
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
setIngredients(
{
...addIngredients,
ingredients: results
});
};
Related
I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);
So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])
I am trying to delete an actor from actors array which is inside movie object.
Firstly, I map actors to display actors one by one and adding a remove action to button.
Whole ActorsList component
const PostList = ({ movie, removeActorFromMovie }, props) => {
return (
<div>
<h3>Actors in film </h3>
{movie.actors.map((actor, i) => (
<div key={actor}>
{actor}
<button onClick={() => removeActorFromMovie(actor)}>Usuń</button>
</div>
))}
</div>
);
};
const mapStateToProps = (state, props) => {
return {
movie: state.movies.find((movie) => movie.id === props.match.params.id),
};
};
const mapDispatchToProps = {
removeActorFromMovie,
};
export default withRouter(
connect
(mapStateToProps, mapDispatchToProps)(PostList)
);
Action:
export const removeActorFromMovie = (payload) => ({
type: "MOVIE_ACTOR_DELETE",
payload,
});
Reducer:
case "MOVIE_ACTOR_DELETE":
return [...state.filter((el) => el !== action.payload)];
Although, the payload wokrs great and returns the array item. It does not remove it from an array.
My movies array with movie under index 0:
movies: Array(1)
0:
actors: (2) ['Robert XYZ', 'Jake XYZ']
director: "Quentino TARANTINO, TARANTINO"
id: "352cc51e-87c4-46ec-84a0-9f308ec1a71a"
productionYear: "123"
title: "Titanic"
and payload:
payload: "Robert XYZ"
type: "MOVIE_ACTOR_DELETE"
and as I said, this doest not remove the actor from actors array which is under index 0 in movies array
Update:
I'm supposing your state is something like this:
state: {
movies: [...]
}
If you want to remove an actor from a movie you should also pass the payload a movie to remove the actor from.
removeActorFromMovie(actor, movie)
then in the reducer:
const { movie, actor } = action.payload;
const nextMovie = {
...movie,
actors: movie.actors.filter((a) => a !== actor)
};
const movieIndex = state.movies.findIndex((m) => m.id === movie.id);
return {
...state,
movies: [
...state.movies.slice(0, movieIndex),
nextMovie,
...state.movies.slice(movieIndex + 1)
]
};
OLD ANSWER (If you instead want to remove a movie):
If you want to remove a movie based on its actors you filter function will not work. You are trying to remove an item whose value is "Robert XYZ" but the element passed to you filter function is a movie.
if the state is like this:
movies: [
{
actors: ['Robert XYZ', 'Jake XYZ']
director: "Quentino TARANTINO, TARANTINO"
id: "352cc51e-87c4-46ec-84a0-9f308ec1a71a"
productionYear: "123"
title: "Titanic"
}
]
then you can do:
const nextItems = state.filter(movie => !movie.actors.includes(action.payload))
return nextItems;
There is no need to create a new copy of the filtered array with the spread operator because filter return a new copy of the original array.
I'm doing a facebook clone, and everytime i press like's button, i want to see the change immediately, that's something that swr provides, but, it only updates after 4-8 seconds :/
What i tried to do is the following: when i click like's button, i first mutate the cache that swr provides, then i make the call to the API, then revalidate data to see if everything is right with the data, actually i console log the cache and it updates immediately, but it the UI doesn't and i don't know why
Let me give sou some context with my code
This is how my publication looks like ( inside pub it's the likes property )
export type theLikes = {
identifier: string;
};
export type theComment = {
_id?: string;
body: string;
name: string;
perfil?: string;
identifier: string;
createdAt: string;
likesComments?: theLikes[];
};
export interface Ipublication {
_id?: string;
body: string;
photo: string;
creator: {
name: string;
perfil?: string;
identifier: string;
};
likes?: theLikes[];
comments?: theComment[];
createdAt: string;
}
export type thePublication = {
data: Ipublication[];
};
This is where i'm asking for all publications with getStaticProps
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
return (
<>
{Publications ? (
<>
<PublicationsHomeHero>
{/* Create pub */}
<CreatePubs />
{/* Show pub */}
{Publications.map(publication => {
return <Pubs key={publication._id} publication={publication} />;
})}
</PublicationsHomeHero>
</>
) : (
<div></div>
)}
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
This is where the like button is ( focus on LikePub, there is where the logic is )
The conditional is simple, if user already liked a pub, cut the like, otherwise, like the pub
interface IlikesCommentsProps {
publication: Ipublication;
}
const LikesComments: React.FC<IlikesCommentsProps> = ({ publication }) => {
const LikePub = async (): Promise<void> => {
try {
if (publication.likes.find(f => f.identifier === userAuth.user.id)) {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
} else {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
currentPub.likes.push({ identifier: userAuth.user.id });
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
}
console.log(publication.likes);
await like({ identifier: userAuth.user.id }, publication._id);
mutate(`${process.env.URL}/api/publication`);
} catch (err) {
if (err) {
mutate(`${process.env.URL}/api/publication`);
}
}
};
return (
<motion.div
onClick={LikePub}
variants={Actions}
whileHover="whileHover"
whileTap="whileTap"
>
<motion.span>
<LikeIcon colorIcon="rgb(32, 120, 244)" />
</motion.span>
<LikeCommentText
separation="0.2rem"
colorText="rgb(32, 120, 244)"
>
Me gusta
</LikeCommentText>
</motion.div>
)
}
As you can see as well, i console.log publication's likes, and this is what happened
The identifier is added to the cache, meaning that user liked the pub, but the UI doesn't update, it takes 4 - 7 seconds to update, probably even more, the same thing happened with removing the like, look at this
Cut the like, but, the UI doesn't update
I'm desperate, i've tried everything, i've been trying to fix this for almost a week, but found nothing, what am i doing wrong, is this a bug ?
I believe the problem is you're directly mutating (in the javascript not swr sense) swr's data that is completely invisible to swr. And only when response is returned from the API your state is updated and that finally triggers swr's observers.
Here you may notice that currentPub.likes is an array (reference) inside currentPub object. You're directly mutating it (with splice) and then insert the same reference back into allPublications object. From swr's perspective the likes array didn't change. It still holds the same reference as before the mutation:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
}
Snippet to illustrate the behavior:
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
currentPub.likes.splice(1, 1)
const updatePub = allPublications.map((pub, idx) => idx === 0 ? currentPub : pub)
console.log(updatePub[0] === allPublications[0]) // true
console.log(updatePub[0].likes === allPublications[0].likes) // true. the reference stays the same
console.log(updatePub[0]) // while the object has changed
You should rewrite it to exlude direct mutation and always return changed references for changed objects/arrays. Something like:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const likes = currentPub.likes.filter( // filter creates new array (reference)
f => f.identifier !== userAuth.user.id
);
const updatePub = allPublications.map(pub => // map creates new array
pub._id === currentPub._id ? { ...currentPub, likes } : pub // {} creates new object (reference)
);
return updatePub;
}
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
const likes = currentPub.likes.filter((el) => el !== 2)
const updatePub = allPublications.map((pub, idx) =>
idx === 0 ? { ...currentPub, likes } : pub
)
console.log(updatePub[0] === allPublications[0]) // false
console.log(updatePub[0].likes === allPublications[0].likes) // false
console.log(updatePub[0])
And do the same for else branch mutation function.
hello I will ask this question again because I still can’t find a answer
I try to create a form similar to google form with react and redux
each question is represented by: QuestionCard
contains a title and can have several types (short question, long question, choice ,multiple, single choice ...)
I manage to add cardQuestion and delete them
my problem is that when I select single choice I want to give the user the possibility to add the number of choices he wants but it does not work
this is my reducer
const initialState = {
formId: '',
form: {
title: '',
description: ''
},
basicForm: {
fullName: '',
age: '',
saved: false
},
cardQuestion: [{
title: '',
choix: [],
type: '',
}],
error: "",
loading: false,
}
const addRequestReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_STATE':
return { ...state, ...action.payload }
default:
return state
}
}
i use a global state
and this how i try to add choices
1- i handle change of inputs (each choice) like this :
const { cardQuestion } = useSelector((state) => state.addRequest)
const choice = cardQuestion.map(e => e.choix)
const onChangeInput = (i, key, value) => {
console.log(value)
dispatch({
type: 'SET_STATE',
payload: {
cardQuestion: cardQuestion.map((elem, index) => {
console.log(i)
console.log(i === index)
if (i !== index) return elem
return { ...elem, [`${key}`]: value }
})
}
})
}
2- use the ChangeInptut like this
<div >{choice.map((elem, i) => <div key={i}>
<Input value={elem.choix} onChange={(e) => onChangeInput(i, 'choix', e.target.value)} />
</div>
3- button to add new input (choice)
<Button onClick={() => dispatch({
type: 'SET_STATE',
payload: {
cardQuestion: [...cardQuestion, { choix: '' }]
}
})} >Add</Button>
</div>
but it give me results like this :
and when i add a choice in question card1 it is also added in questioncard2
how can i resolve this , any help will be appreciated
This
cardQuestion: [...cardQuestion, { choix: '' }]
adds a new card without a title or type to the list of cards. If I understand you correctly, you want to add a new choice to an existing card, which will look something like
const newCards = [...cardQuestion]
newCards[currentCardPosition] = {...newCards[currentCardPosition]}
newCards[currentCardPosition].choix = [...newCards[currentCardPosition].choix, '' ]
but I don't see where currentCardPosition will come from
So I have 2 arrays of objects: planned, backlog.
const planned = [
{
order_number: "1",
part_name: "example",
part_number: "1",
process_id: 1
},
....
];
const backlog = [
{
order_number: "2",
part_name: "example",
part_number: "2",
process_id: 2
},
....
];
I am trying to filter both of them at the same time, each one individually that's not a problem.
So what I am actually doing is first adding key of planned to planned array and backlog to backlog array in order to know later from where the order originally came from.
var newPlanned = planned.map(function (el) {
var o = Object.assign({}, el);
o.planned = true;
return o;
});
var newBacklog = backlog.map(function (el) {
var o = Object.assign({}, el);
o.backlog = true;
return o;
});
Later I am merging the 2 arrays into 1 array, and filtering the merged array with one input onChange, but what I can't achieve is to render the "planned" and "backlog" arrays separately from the new merged array.
const newPlannedAndBacklog = [...newBacklog, ...newPlanned];
Link to codeSandBox:
SandBox
I added a type instead of a boolean property for several types. This may be more scalable so you can add more types (refined, done, etc.), but let me know if you'd like the boolean property.
const combinedList = [
...planned.map((item) => ({ ...item, type: 'planned' })),
...backlog.map((item) => ({ ...item, type: 'backlog' })),
];
const getFilteredList = (type) => combinedList.filter((item) => item.type === type);
UPDATE
I believe this is the behavior you want. Let me know if not and I'll update the answer.
// Combines all backlog and planned stories into one array
const newPlannedAndBacklog = [
...planned.map((item) => ({ ...item, type: 'planned' })),
...backlog.map((item) => ({ ...item, type: 'backlog' })),
];
// Filters property values based on input value
const getFilteredList = (filter) => newPlannedAndBacklog
.filter(item => Object
.values(item)
.map(String)
.some(v => v.includes(filter))
);
export default function App() {
const [form, setForm] = useState({});
const [stories, setStories] = useState([]);
const handleChange = (event) => {
const { name, value } = event.target;
setForm({ [name]: value });
setStories(getFilteredList(value));
};
// Separates the return of getfilteredList based on type
const renderDiv = (type) => stories
.filter((story) => story.type === type)
.map((story) => {
// Render each property individually:
return Object.entries(story).map(([key, value]) => {
const content = <>{key}: {value}</>;
if (key === 'order_number') return <h3>{content}</h3>;
return <p>{content}</p>;
});
});
return (
<div className="App">
<h2>PlannedAndBacklog</h2>
<p>Search for planned and backlog:</p>
<input
name='type'
onChange={handleChange}
value={form.type || ''}
/>
<div>{renderDiv('backlog')}</div>
<div>{renderDiv('planned')}</div>
</div>
);
};