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))
})
Related
In a high level overview I am building a tinder like app functionality. Where in my example, 4 entries are fetched from my DB, the user is shown one at a time and can click a like or a dislike button. Each button click triggers some asynchronous functions for writing the event to my DB. Once the last entry that was fetched is clicked I need to go and fetch the next 4 entries in my DB.
MY COMPONENT
export const HomePage: React.FC = () => {
const { user } = useUser()
const userId = user?.id
const [current, setCurrent] = useState<number | null>(0)
const [skip, setSkip] = useState(0)
const [url, setUrl] = useState(
`/api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
)
const getUsers = async (url: string) => {
const { data } = await axios.get(url)
return data
}
const { error, data, isLoading, refetch } = useQuery(
['userSwipeData', url],
() => getUsers(url),
{
enabled: !!user?.id,
}
)
const handleRefetchUsers = () => {
setCurrent(0)
setUrl(
`/api/swipes/get-users/?userId=${user?.id}&skip=${
skip + FETCH_USERS_PAGINATION_LIMIT
}`
)
refetch()
setSkip(skip + FETCH_USERS_PAGINATION_LIMIT)
}
const handleSwipe = async (e: any) => {
if (current === null || !data) return
const { value } = e.target
await handleSwipeType(value, data?.users[current].id)
}
const handleSwipeType = async (type: string, id: string) => {
const values = {
userSwipeOn: id,
currentUser: userId,
}
// if (type === 'YES') {
// if (current && data?.users[current]?.isMatch) {
// await axios.post('/api/swipes/create-match', values)
// alert('You have a match!')
// }
// await axios.post('/api/swipes/like', values)
// } else {
// await axios.post('/api/swipes/dislike', values)
// }
if (current === data.users.length - 1) {
handleRefetchUsers()
} else {
setCurrent(current! + 1)
}
}
if (isLoading || current === null) return <Box>Fetching users...</Box>
if (error) return <Box>An error has occurred </Box>
if (data && !data?.users.length) return <Box>No more users for now</Box>
return (
<Box>
<h1>Current User: {data?.users[current].email}</h1>
<Flex>
<button value="NO" onClick={handleSwipe}>
NO
</button>
<button value="YES" onClick={handleSwipe}>
YES
</button>
</Flex>
</Box>
)
}
The problem I am facing is that in this current state when the handleRefetchUsers() function triggers it works as expected. However, if I am to uncomment all the asynchronous code which I need to run on every click to document the event, once the handleRefetchUsers() trigger I notice it is skipping 4 entries every time it runs. I'm really at a loss as to why because the check for a final entry should only run after the async code has finished. Any ideas would be helpful.
I'm pretty sure that refetch doesn't wait for setUrl to actually update the url
You shouldn't really base one state on another state
To fix it I would replace
const [url, setUrl] = useState(
`/api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
)
with
const url = /api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
and remove refetch entirely. react-query will refetch anyway because the url changed
There are a few things that could be improved, but your main issue is that setState is async. So when you use setUrl and then call refetch, refetch is still looking at the old url value.
I think a cleaner way would be to use refetch inside an effect, that has current and skip in the dependency array.
Also, url is a derived state, so it doesn't really need its own state. And you should also be using an arrow function when a new state relies on the previous state - again because setState is async, and it's possible that you are referencing an old state.
const buildUrl = (user, skip) => user?.id ? `/api/swipes/get-users/?userId=${user?.id}&skip=${skip}` : ''
export const HomePage: React.FC = () => {
const { user } = useUser()
const userId = user?.id
const [current, setCurrent] = useState<number>(0) // current page
const [skip, setSkip] = useState(0)
const getUsers = async (url: string) => {
const { data } = await axios.get(url)
return data
}
const { error, data, isLoading, refetch } = useQuery(
['userSwipeData', buildUrl(user, skip)],
() => getUsers(buildUrl(user, skip)),
{
enabled: !!user?.id,
}
)
const handleRefetchUsers = () => {
setCurrent(0)
setSkip((prev) => prev + FETCH_USERS_PAGINATION_LIMIT)
}
const handleSwipe = async (e: any) => {
if (current === null || !data) return
const { value } = e.target
await handleSwipeType(value, data?.users[current].id)
}
const handleSwipeType = async (type: string, id: string) => {
const values = {
userSwipeOn: id,
currentUser: userId,
}
if (type === 'YES') {
if (current && data?.users[current]?.isMatch) {
await axios.post('/api/swipes/create-match', values)
alert('You have a match!')
}
await axios.post('/api/swipes/like', values)
} else {
await axios.post('/api/swipes/dislike', values)
}
if (current === data.users.length - 1) {
handleRefetchUsers()
} else {
setCurrent((prev) => prev + 1)
}
}
useEffect(() => {
refetch()
}, [current, skip])
if (isLoading || current === null) return <Box>Fetching users...</Box>
if (error) return <Box>An error has occurred </Box>
if (data && !data?.users.length) return <Box>No more users for now</Box>
return (
<Box>
<h1>Current User: {data?.users[current].email}</h1>
<Flex>
<button value="NO" onClick={handleSwipe}>
NO
</button>
<button value="YES" onClick={handleSwipe}>
YES
</button>
</Flex>
</Box>
)
}
I am using react usestate() and I want to update device state(It is an object)
my problem is when ShowRelays component renders for the first time device is an empty object and It does not get updated during first rendering, but for the next renders everything is fine
How can I update device state for the first time rendering?
(sorry for my bad english)
.
function ShowRelays(props) {
const [device, setDevice] = useState({})
let reduxDevices = useSelector(state => state.devicesReducer.devices)
let findDevice = () => {
let myDevice = reduxDevices.find(x => x._id === props.id)
setDevice(prevState => {
return {
...prevState,
...myDevice
}
})
}
useEffect(() => {
if (props.show) {
findDevice()
}
}, [props.show])
return
(
<div>
test
</div>
)
}
myDevice object is like:
{active: true, name: "device1", id: "deviceId"}
You can pass a function to your useState-hook which will calculate your initial value for device.
see lazy init state
function ShowRelays(props) {
let reduxDevices = useSelector(state => state.devicesReducer.devices);
const [device, setDevice] = useState(() => {
return reduxDevices.find(x => x._id === props.id)
});
return <div>test</div>;
}
An other possible solution for your problem without using a separate state could be the following (directly select the right device from your selector function):
function ShowRelays(props) {
const device = useSelector(state => {
return state.devicesReducer.devices.find(x => x._id === props.id);
});
return <div>test</div>;
}
I have a set of card objects that I map over.
When I click on a card it adds the selected class which in turn gives it a border to show the user it is selected, it also adds the id of the card to the selectedCards useState array.
WHAT I WANT TO HAPPEN:
Each card object has a creditAvailable key state which is equal to a figure.
On selection (click) of the card, in addition to selecting the card I would also like to add up the creditAvailable and display it on the screen. and when I unselect the card I would like the figure to go down.
WHAT I HAVE TRIED:
I thought it would be as simple as calling the function to add up the credit inside the first function which selects the card, however when console logging inside the first function I see that the state has not yet updated. (scope).
I then tried to call the function outside of the first function but it gave me an infinite loop. Here is my code.
Any ideas? Thanks
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const [total, setTotal] = useState();
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
// addUpCreditAvailable(); // nothing happens
console.log(selectedCards); // []
};
console.log(selectedCards) // [1] for example. This is in the global scope
const addUpCreditAvailable = () => {
console.log("inside add up credit");
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
setTotal(result);
return result;
};
return (
<div className="Container">
<UserInputForm submitData={handleSubmitData} />
<h1> Cards available to you displayed are below!</h1>
{cards.map(
({
id,
name,
number,
apr,
balanceTransfer,
purchaseDuration,
creditAvailable,
expiry,
}) => (
<CreditCard
key={id}
name={name}
number={number}
apr={apr}
balanceTransferDuration={balanceTransfer}
purchaseOfferDuration={purchaseDuration}
creditAvailable={creditAvailable}
expiry={expiry}
onClickCard={() => handleSelectCard(id)}
selected={selectedCards.includes(id)}
/>
)
)}
<span> £{total}</span>
)}
I figured it out with the help from above. As Wilms said i had to return the result of the handleSelectCard function and return the result of the addUpCredit function. Then I called the addUpCreditAvailable with the selectedCards state and stored the result in a variable which i then displayed in my render method.
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
return selectedCards;
};
const addUpCreditAvailable = (selectedCards) => {
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
return result;
};
const totalCredit = addUpCreditAvailable(selectedCards);
render method:
render (
[...]
{selectedCards.length && (
<div className={bem(baseClass, "total-credit")}>
Total Credit available: £{totalCredit}
</div>
)}
[...]
)
I have to get a users from backend and push the result array into a property of third-party component. I can't push the result into the component's state, like setUsersList, because it leads to infinity render-loop. So I decided to use a function which will put the data from backend into a variable and return variable's value.
<InputAutocomplete
...
value={selectedUserName}
onChange={(value: string) => setSelectedUserName(value)}
options={getFilteredUsers(selectedUserName)}
/>
My function is:
const getFilteredUsers = (typedValue: any): any[] => {
console.log('***getting into getFilteredUsers method')
if (!typedValue) {
return []
}
let usersList: any[]
repository.GetUsers(typedValue).then((response) => {
if (response.StatusCode !== null) {
response.Message.then((errorText) => showNotificationPopup(errorText, consts.ERROR_OCCURED, 'error'))
return
}
response.Data.then((users) => {
console.log('filling usersList')
usersList = (users.map((u) => {
return {key: u.Id, value: `${u.Name} - ${u.Position} - ${u.DopOfficeName}` }
})
)
})
})
console.log('leaving function')
return usersList
}
It works unacceptable for me (although it works as expected):
***getting into getFilteredUsers method
leaving function (with empty array)
filling usersList (too late)
The property "options" of third's party component doesn't accept Promise<any[]>, it can only accept any[], thus I can't mark my function as async and await the result from repository before to leave the function.
How to prevent leaving function before I'll get the result?
Maybe I've made a wrong decision at all. If I'm wrong I need a help, how to implement server-side refilling of autocomplete input component.
I can't push the result into the component's state, like setUsersList, because it leads to infinity render-loop
This should not be the case.
const SomeComponent = () => {
const [options, setOptions] = React.useState([]);
const [username, setUsername] = React.useState([]);
React.useEffect(() => {
if (!typedValue) return [];
repository.GetUsers(typedValue).then((response) => {
if (response.StatusCode !== null) {
response.Message.then((errorText) => showNotificationPopup(errorText, consts.ERROR_OCCURED, 'error'));
return;
}
response.Data.then((users) => {
console.log('filling usersList');
const usersList = users.map(u => {
return { key: u.Id, value: `${u.Name} - ${u.Position} - ${u.DopOfficeName}`};
});
setOptions(usersList);
})
})
console.log('leaving function')
}, [username]);
return (
<InputAutocomplete
value={username}
onChange={(value) => setUsername(value)}
options={options}
/>
);
};
Above code should not go in infinite loop.
The best way to describe question is my code:
function EstateParamsList({ estateType, category }) {
const [isLoading, setIsLoading] = useState(true)
const [params, setParams] = useState({})
const [showPopUp, setShowPopUp] = useState(false)
useEffect(() => {
if (category && typeof category.id !== undefined) {
return db.collection(`dictionaries/ESTATE_PARAMS/${estateType}/${category.id}/params`).onSnapshot(response => {
const paramsObject = {}
response.forEach(param => {
paramsObject[param.id] = {
...convertParamObjetcToFieldsConfig(param.data()),
change: fieldChangedHandler
}
})
setParams(paramsObject)
setIsLoading(false)
})
} else {
setIsLoading(false)
}
}, [category])
console.log(params)
const fieldChangedHandler = (event, fieldIdentifier) => {
if(params)
console.log(params)
}
So i have params variable, that im init with object, that i'm getting async from firebase. Implementation of initializing you can see in useEffect method. For every object i want to pass ref for the function "fieldChangedHandler", for managing value of inputs.
fieldChangedHandler is a method of my EstateParamsList. But there i cant get value of params!
Question is WHY? I'm calling fieldChangedHandler only after everything was rendered, and async request was done.
Below is console log of params. Why in func is empty params?
Calling:
const renderParamsAsFields = params => {
const fields = []
for (const key in params) {
fields.push(<Field {...params[key]} changed={event => params[key].change(event, key)} />)
}
return fields.length ? fields : <div className='EstateParamsManager-EmptyValues'>Нет параметров</div>
}
Why not use curried function?
const createFieldChangedHandler = (
params,
fieldIdentifier
) => event => {
if (params) console.log(params);
};
function EstateParamsList({ estateType, category }) {
//... other code
useEffect(() => {
//...other code
change: createFieldChangedHandler(
paramsObject,
param.id
),
//...other code
}, [category, estateType]);//fixed missing dependency
When you call the change function you should already have the right values in scope:
<Field {...params[key]} changed={params[key].change} />