Why order of lines matters here? - javascript

I am creating a Memory Game and I can't understand how can these two lines order matters in the code:
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that works
setPicks([])
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));
}
}, 500)
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that doesn't work
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));
setPicks([])
}
}, 500)
In the first case, matched state receive the correct updates so why similar cards match, but in the second case it seems that matched state doesn't get correct values.
I can't understand how the order of these two lines affect matched state
const App = () => {
const icons = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
const shuffle = cards => [...cards].sort(() => Math.random() - 0.5);
const shuffledCards = useMemo(() => {
return shuffle(icons).map((icon, i) => ({
icon: icon,
id: i,
isFlipped: false
}));
});
const [picks, setPicks] = useState([]);
const [cards, setCards] = useState(shuffledCards);
const [matched, setMatched] = useState([]);
const handleClick = id => {
!picks.includes(id) && picks.length !== 2 && setPicks([...picks, id]);
};
useEffect(() => {
setCards(cards =>
cards.map((c, i) =>
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
const matches = cards.reduce((matches, { icon, isFlipped }) => {
!matches[icon] && (matches[icon] = 0);
isFlipped && matches[icon]++;
return matches;
}, {});
Object.entries(matches).forEach(([icon, count]) => {
count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
});
const timer = setTimeout(() => {
if (picks.length === 2) {
//the problem is here, that order doesn't work
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
return () => clearTimeout(timer);
}, [picks]);
return (
<div class="game">
<Deck
cards={cards}
handleClick={handleClick}
picks={picks}
matched={matched}
/>
</div>
);
};
const Deck = ({ numbers, cards, ...props }) => {
return (
<div class="deck">
{cards.map((c, i) => {
return <Card i={i} card={c} {...props} />;
})}
</div>
);
};
const Card = ({ handleClick, picks, card, i, matched }) => {
const { icon, isFlipped, id } = card;
return (
<div
className={`card${isFlipped ? " flipped" : ""} ${
matched.includes(icon) ? "matched" : ""
}`}
onClick={() => handleClick(id)}
>
<div class="front" />
<div class="back">{icon}</div>
</div>
);
};

ISSUE :
Here main issue is with the below line position in code:
const matches = cards.reduce((matches, { icon, isFlipped }) => {
So to understand the issue, first, we need to understand what is wrong with the above line position, please do read comments to understand the issue better
setCards(cards => // <--------- 1.
cards.map((c, i) => // <--------- 2.
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
// 1. current state of cards
// 2. returning updated state of cards
// so, if you are aware of async behavior or setState, then you know
// the below line will always point to snapshot of cards when the useEffect run
// and not to the 1. or 2.
// So here cards is pointing to snapshot when useEffect run
const matches = cards.reduce((matches, { icon, isFlipped }) => {
Now, let's see with both cases
useEffect(() => {
// Here you will always get the snapshot of cards values, which is currently available
setCards(cards => // <--------- 1.
cards.map((c, i) => // <--------- 2.
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
// so if you understand correctly, above code block doesn't make any sense for the below line
const matches = cards.reduce((matches, { icon, isFlipped }) => {
....
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that works
setPicks([]) // <---- this will trigger useEffect , and send the cards value from 2.
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false })));
}
}, 500)
const timer = setTimeout(() => {
if (picks.length === 2) {
//this order that works
setCards(cards => cards.map((c) => ({ ...c, isFlipped: false }))); // <------------- 3.
setPicks([]) // <---- this will trigger useEffect , and send the cards value from 3.
}
}, 500)
},[picks]); // <----- useEffect is dependent on picks
DEBUGGING DEMO :
You can change the order as per your case and check the console and see what are the values you are getting for cards when you switch the order
Solution :
Hope this will make your doubts clear, now If we are clear with issue, what's the solution then, here we can solve it 2 ways but the basic is common in both way and that is Always work with updated cards value, in that case order doesn't matter any longer
FIRST SOLUTION :
You can put the match condition code block inside the setCards(cards => { and work with latest cards values
useEffect(() => {
setCards(cards => {
const updated = cards.map((c, i) =>
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
// take whole code block and put it inside the `setCards`
// and directly work with latest update value
const matches = updated.reduce((matches, { icon, isFlipped }) => { // <---- HERE
!matches[icon] && (matches[icon] = 0);
isFlipped && matches[icon]++;
return matches;
}, {});
Object.entries(matches).forEach(([icon, count]) => {
count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
});
return updated;
});
const timer = setTimeout(() => {
if (picks.length === 2) {
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
return () => clearTimeout(timer);
}, [picks]);
WORKING DEMO :
SECOND SOLUTION : ( and I would suggest this one )
You can create a useEffect for cards value, so that you will always get the updated cards value and based on that you can set the match values
useEffect(() => {
setCards(cards =>
cards.map((c, i) =>
picks.includes(c.id) ? { ...c, isFlipped: true } : c
)
);
const timer = setTimeout(() => {
if (picks.length === 2) {
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
return () => clearTimeout(timer);
}, [picks]);
useEffect(() => {
const matches = cards.reduce((matches, { icon, isFlipped }) => {
!matches[icon] && (matches[icon] = 0);
isFlipped && matches[icon]++;
return matches;
}, {});
Object.entries(matches).forEach(([icon, count]) => {
count === 2 && !matched.includes(icon) && setMatched([...matched, icon]);
});
}, [cards]);
WORKING DEMO :

The code block you are asking about is responsible for resetting the card state everytime two cards are picked.
const timer = setTimeout(() => {
if (picks.length === 2) {
setCards(cards => cards.map(c => ({ ...c, isFlipped: false })));
setPicks([]);
}
}, 500);
The state of the picked cards picks must be reset by passing it an empty array through the hook setPicks before the card state is set with the hook setCards since picks can only be set if there is not exactly 2 picks made. Resetting the picks length to zero is required in the current logic you have because of what you have in your click handler.
const handleClick = id => {
!picks.includes(id) && picks.length !== 2 && setPicks([...picks, id]);
};

Related

Why do I have by default a prefilled name in my dropdown whereas I want to display 'Select' by default using react?

can someone see why in my Dropdown menu, I have by default a name filled, whereas I'd like to have by default Select. What is wrong please ?
export default function Overview() {
const [questions, setQuestions] = useState()
const [questionDrop, setQuestionDrop] = useState()
const [overview, setOverview] = useState()
useEffect(() => {
ClearApi.listQuestions()
.then(res => {
const i = res.data.questions
console.log('Questions: ', i)
setQuestions(i.map(i => ({ ...i, value: i._id, name: i.title })))
if (i.length > 0) setQuestionDrop(i[0])
})
.catch(err => {
console.log(err)
})
}, [])
useEffect(() => {
if (questionDrop !== undefined)
myfunctionAPI.displayOverview(questionDrop._id)
.then(res => {
const d = res.data.overviews
console.log('Overview: ', d)
setOverview(d)
})
.catch(err => {
console.log(err)
})
}, [questionDrop]);
return (
<div >
<Dropdown
label={questionDrop?.title ?? 'Select'}
items={questions}
onSelect={({ value }) => setQuestionDrop(questions.find(i => i._id === value))}
/>
</div>
)
}

Updating React state after axios PUT request without having to reload page

On my current application, if a user tries to enter an existing name that has a different number, it will prompt the user if they want to update that entry with the new number. If yes, the entry is updated using an axios PUT request. My issue is that I can only get it to change on the front end by reloading the page (it updates successfully on db.json) instead of it updating immediately after the user confirms. On my useEffect method I tried adding [persons] as the second argument and it seemed to work, but found out that it loops the GET requests infinitely. I have a similar function for when deleting an entry so I'm sure it must be something that has to be added to setPersons
Update methods
const addEntry = (event) => {
event.preventDefault();
const newPersonEntry = {
name: newName,
number: newNumber,
}
const all_names = persons.map(person => person.name.toUpperCase())
const all_numbers = persons.map(person => person.number)
const updatedPerson = persons.find(p => p.name.toUpperCase() === newName.toUpperCase())
const newPerson = { ...updatedPerson, number: newNumber };
if (newName === '') {
alert('Name entry cannot be blank')
return
}
if (newNumber === '') {
alert('Number entry cannot be blank')
return
}
if (all_numbers.includes(newNumber)) {
alert('That number already exists')
return
}
if (newNumber.length < 14) {
alert('Enter a valid number')
return
}
if (all_names.includes(newName.toUpperCase())) {
if (window.confirm(`${newName} already exists, replace number with the new one?`)) {
console.log(`${newName}'s number updated`)
personService
.update(updatedPerson.id, newPerson)
.then(res => {
setPersons() //something here
})
return
}
return
}
personService
.create(newPersonEntry)
.then(person => {
setPersons(persons.concat(person))
setNewName('')
setNewNumber('')
})
}
//PUT exported as personService
const update = (id, newObject) => {
const request = axios.put(`${baseURL}/${id}`,newObject)
return request.then(response => response.data)
}
Other code
const App = () => {
const [persons, setPersons] = useState([])
useEffect(() => {
personService
.getAll()
.then(initialPersons => {
setPersons(initialPersons)
})
}, [])
...
//Display method
const filteredNames = persons.filter(person => person.name.toLowerCase().includes(filter.toLowerCase()))
const row_names = () => {
return (
filteredNames.map(person =>
<p key={person.id}>{person.name} {person.number} <button onClick={() => handleDelete(person)}>delete</button></p>));
}
...
//Render
return (
<div>
<h2>Phonebook</h2>
<h2>Search</h2>
<SearchFilter value={filter} onChange={handleFilterChange} />
<h2>Add Entry</h2>
<Form onSubmit={addEntry}
name={{ value: newName, onChange: handleNameChange }}
number={{ value: newNumber, onChange: handleNumberChange }}
/>
<h2>Numbers</h2>
<DisplayPersons persons={row_names()} />
</div>
)
}
The solution here is a little bit tricky but doable . You need to split your logic into two parts like this :
const [dataChanged , setDataChanged] = useState(false)
useEffect(()=>{
// Rest of your logic here
} , [dataChanged])
useEffect(()=>{
// Your logic will run only one time
// on Success we change the dataChanged state so the other useEffect will
// run basically you can run the rest of your logic in the other
// useEffect so the infinite loop won't happen
// setDataChanged( (prev) => !prev )
} , [])
Was able to use map method that worked
personService
.update(updatedPerson.id, newPerson)
.then(res => {
setPersons(persons.map(p => p.id !== updatedPerson.id ? p : res))
})

Why my nested recursive modal mounting each time?

I am creating a side hierarchy for controlling all my Modal usage in the app
import ReactNativeModal, { ModalProps } from 'react-native-modal';
export const ModalsProvider = ({ children }: { children: ReactNode }) => {
const [modals, setModals] = useState<IModal[]>([]);
const [modalsMap, setModalsMap] = useState({});
const isModalsExist = useMemo<boolean>(() => !!modals.length, [!!modals.length]);
useEffect(() => {
console.log({ isModalsExist });
}, [isModalsExist]);
const MoadlComponent: ComponentType<{ idx: number }> = ({ idx }) => {
const isNextExist = useMemo<boolean>(
() => modals.length - 1 > idx,
[modals.length - 1 > idx]
);
const { name, modalProps, props } = useMemo<IModal>(() => modals[idx], [idx]);
const MyModal = useMemo<ComponentType>(() => modalsMap[name], [name]);
useEffect(() => {
console.log({ idx });
}, []);
return (
<ReactNativeModal {...modalProps}>
<MyModal {...props} />
{isNextExist && <MoadlComponent idx={idx + 1} />}
</ReactNativeModal>
);
};
return (
<modalsContext.Provider value={{ registerModals, open, close, closeAll }}>
{children}
{isModalsExist && <MoadlComponent idx={0} />}
</modalsContext.Provider>
);
};
each time I open another modal,
useEffect(() => {
console.log({ idx });
}, []);
runs inside all nested from top to bottom (e.g.
{ idx : 2 }
{ idx : 1 }
{ idx : 0 }
), and of course, inside component logs run as well, which create an unnecessary heavy calculation
The weird thing is that my first and main suspect, isModalsExist, isn't the trigger as
useEffect(() => {
console.log({ isModalsExist });
}, [isModalsExist]);
runs only if the boolean indeed change, and anyhow if it was, I expecting the logs to be from bottom to top, not the opposite.
Any help appreciated, can't figure what I totally missing
You don't want to put the MoadlComponent into the ModalsProvider's body without wrapping it into a useEffect, because it reassigns a completely new function component each time the ModalsProvider re-renders.

How to use react.usememo using react and javascript?

i am doing some filtering on items based on ids which is in selection object. and then evaluating true or not based on its completion true or false from items which is an array of objects.
below is my code,
const Items: React.FC<RouteComponentProps> = ({history}) => {
const [{selection}] = tableState;
const {
data: items,
count: itemsCount,
} = React.useMemo(() => {
(!isNil(data) ? data.items : { data: [], count: 0}),
[data]
);
let noJobs;
if (selection) { //how to put this block in useMemo
const filteredItems = items.filter(item =>
Object.keys(selection).includes(item.id)
);
noJobs = filteredItems.map(item => item.jobs.map(job => job.completed))
.flat().every(Boolean);
}
return (
<button disabled = {noJobs}> Click me </button>
);
}
How can i put the block which includes noJobs calcualtion (from if(selection)) to React.useMemo. could someone help me with this. I am learning react and hooks is new to me.
thanks.
Try followings it:
const Items: React.FC<RouteComponentProps> = ({history}) => {
const [{selection}] = tableState;
const {
data: items,
count: itemsCount,
} = React.useMemo(() => {
(!isNil(data) ? data.items : { data: [], count: 0}),
[data]
);
const noJobs = React.useMemo(() => {
if (selection) {
const filteredItems = items.filter(item => {
return Object.keys(selection).includes(item.id)
});
return filteredItems.map(item => item.jobs.map(job => {
return job.completed
}))
.flat()
.every(Boolean);
};
return undefined; //or everything you want
} , [items , selection]);
return (
<button disabled = {noJobs}> Click me </button>
);
}

Can access property of object in mapped array (Next.js)

I'm trying to map through an array of objects and return a link property as a <li></li> but for some reason nothing is being returned:
import { useState, useEffect } from "react";
import { Config } from "../../config";
export default function Navbar() {
const [navs, setNavs] = useState();
const [mainNav, setMainNav] = useState();
const [links, setLinks] = useState();
useEffect(async () => {
const res = await fetch(`${Config.apiUrl}/navs`);
const data = await res.json();
setNavs(data);
// !!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
}, []);
useEffect(() => {
!!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
!!mainNav && setLinks(mainNav.map((item) => item.link));
}, [navs]);
!!links && console.log(links);
return (
<nav>
<ul>{!!links && links.map((item) => <li>{item.link}</li>)}</ul>
</nav>
);
}
The result of my console.log(links) shows the following output:
[
{
_id: "5f975d6d7484be510af56903", link: "home", url: "/", __v: 0, id: "5f975d6d7484be510af56903"
},
{
_id: "5f975d6d7484be510af56904", link: "posts", url: "/posts", __v: 0, id: "5f975d6d7484be510af56904"
},
]
I don't see why I can access the link property through item.link.
P.S. This is starting too look far from elegant, does anyone else have any idea how I can clean this up more?
The problem is here,
useEffect(() => {
!!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
!!mainNav && setLinks(mainNav.map((item) => item.link));
}, [navs]);
The problem is you are using mainNav from state right after you are setting the value in state in the above useEffect. It doesn't wait for the state to be updated and then take the result. You have two options here,
Either declare another useEffect like this,
useEffect(() => {
!!navs && setMainNav(navs.map((x) => (x.name === "main-nav" ? x : null)));
}, [navs]);
useEffect(() => {
!!mainNav && setLinks(mainNav.map((item) => item.link));
}, [mainNav]);
Or you can store the mainNav in a variable at first. Like this,
useEffect(() => {
let mainNavData = [];
if(!!navs) {
mainNavData = navs.map((x) => (x.name === "main-nav" ? x : null));
setMainNav(mainNavData);
setLinks(mainNavData.map((item) => item.link));
}
}, [navs]);

Categories

Resources