'React function executes continuously and state keeps refreshing
I have kind of a weird issue in React. I am using the Blitz JS framework along side with Prisma for database stuff. I have a function that queries the database for all entries from a date the user selects forward. It's used for a reservation system that I am trying to build. After I get the data I use it to create a <select>
element and set as <option>
every space that does not appear in the database. Everything works fine, the <select>
and <option>
display what they should, but as soon as I click the drop-down to see all available option, I think the state refreshes and the menu closes down. If I console.log()
inside the function, it will go on forever in the console menu. Also in the terminal I can see the function being called about every second or so.
terminal log
javascript console log
I also tried quering the DB from a useEffect()
but useEffect()
and useQuery
(from Blitz.js) don't work together
I will attach the code along side with comments for easier reading. Thank you for your time!
Main Page
import { BlitzPage, invoke, useQuery } from "blitz"
import { useState, useEffect, Suspense } from "react"
import { UserInfo } from "app/pages"
import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css"
import addDays from "date-fns/addDays"
import format from "date-fns/format"
import insertBooking from "app/bookings/mutations/insertBooking"
import getAllBookings from "app/bookings/queries/getAllBookings"
import { useCurrentBookings } from "app/bookings/hooks/useCurrentBookings"
import { useCurrentUser } from "app/core/hooks/useCurrentUser"
const Add: BlitzPage = () => {
//State for all options that will be added for the booking
const [state, setState] = useState({
intrare: 1,
locParcare: 0,
locPescuit: 0,
casuta: 0,
sezlong: 0,
sedintaFoto: false,
petrecerePrivata: false,
totalPrice: 20,
})
//Date state added separately
const [startDate, setStartDate] = useState(addDays(new Date(), 1))
const [availableSpots, setAvailableSpots] = useState({
pescuit: [0],
casute: {},
sezlonguri: {},
})
// The function that reads the DB, manipulates the data so I can have
// an array of open spots and then renders those values in a select
const PescuitSelect = () => {
const totalFishingSpots = Array.from(Array(114).keys())
const bookings = useCurrentBookings(startDate) //useCurrentBookings is a hook I created
const availableFishingSpots = totalFishingSpots.filter(
(o1) => !bookings.some((o2) => o1 === o2.loc_pescuit)
)
console.log(availableFishingSpots)
setAvailableSpots({ ...availableSpots, pescuit: availableFishingSpots })
return (
<select>
{availableSpots.pescuit.map((value) => {
return (
<option value={value} key={value}>
{value}
</option>
)
})}
</select>
)
}
// Date state handler
const handleDate = (date) => {
setStartDate(date)
}
// Update the price as soon as any of the options changed
useEffect(() => {
const totalPrice =
state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15
setState({ ...state, totalPrice: totalPrice })
}, [state])
type booking = {
starts_at: Date
ends_at: Date
intrare_complex: number
loc_parcare: number
loc_pescuit: number
casuta: number
sezlong: number
sedinta_foto: boolean
petrecere_privata: boolean
total_price: number
}
// Here I handle the submit. "petrecerePrivata" means a private party. If that is checked
// it does something, if not, something else
function handleSubmit(event) {
event.preventDefault()
if (state.petrecerePrivata === true) {
setState({
...state,
intrare: 0,
locParcare: 0,
locPescuit: 0,
casuta: 0,
sezlong: 0,
sedintaFoto: false,
totalPrice: 100,
})
} else {
const booking: booking = {
starts_at: startDate,
ends_at: addDays(startDate, 1),
intrare_complex: state.intrare,
loc_parcare: state.locParcare,
loc_pescuit: state.locPescuit,
casuta: state.casuta,
sezlong: state.sezlong,
sedinta_foto: state.sedintaFoto,
petrecere_privata: state.petrecerePrivata,
total_price: state.totalPrice,
}
invoke(insertBooking, booking) // Insert the new created booking into the database
}
}
// State handler for everything but the price, that updates in the useEffect
const handleChange = (evt) => {
const name = evt.target.name
const value = evt.target.type === "checkbox" ? evt.target.checked : evt.target.value
setState({
...state,
[name]: value,
})
}
return (
<>
<Suspense fallback="Loading...">
<UserInfo />
</Suspense>
{
// Here starts the actual page itself
}
<div className="mx-auto max-w-xs ">
<div className="my-10 p-4 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md sm:p-6 lg:p-8 dark:bg-gray-800 dark:border-gray-700">
<form className="space-y-6" action="#" onSubmit={handleSubmit}>
<h5 className="text-xl font-medium text-gray-900 dark:text-white">
Fa o rezervare noua
</h5>
{state.petrecerePrivata ? (
<>
<div>
<label
htmlFor="date"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Data
</label>
<div className="border-2 rounded">
<DatePicker
selected={startDate}
onChange={(date) => handleDate(date)}
dateFormat="dd/MM/yyyy"
includeDateIntervals={[{ start: new Date(), end: addDays(new Date(), 30) }]}
className="cursor-pointer p-2"
/>
</div>
</div>
<label
htmlFor="checked-toggle"
className="relative inline-flex items-center mb-4 cursor-pointer"
>
<input
type="checkbox"
name="petrecerePrivata"
id="checked-toggle"
className="sr-only peer"
checked={state.petrecerePrivata}
onChange={handleChange}
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
Petrecere Privata
</span>
</label>
</>
) : (
<>
<div>
<label
htmlFor="date"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Data
</label>
<div className="border-2 rounded">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
dateFormat="dd/MM/yyyy"
includeDateIntervals={[{ start: new Date(), end: addDays(new Date(), 30) }]}
className="cursor-pointer p-2"
/>
</div>
</div>
<div>
<label
htmlFor="intrare"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Bilete Intrare Complex
</label>
<input
type="number"
name="intrare"
id="intrare"
placeholder="1"
value={state.intrare}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
required
/>
</div>
<div>
<label
htmlFor="loParcare"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Numar Locuri de Parcare
</label>
<input
type="number"
name="locParcare"
id="locParcare"
placeholder="0"
min="0"
value={state.locParcare}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<div>
<label
htmlFor="locPescuit"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Locul de Pescuit
</label>
{
// Here I call that function inside a Suspense and things go south
}
<Suspense fallback="Cautam locurile de pescuit">
<PescuitSelect />
</Suspense>
</div>
<div>
<label
htmlFor="casuta"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Casuta
</label>
<input
type="number"
name="casuta"
id="casuta"
placeholder="0"
min="0"
max="18"
value={state.casuta}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<div>
<label
htmlFor="sezlong"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Sezlong
</label>
<input
type="number"
name="sezlong"
id="sezlong"
placeholder="0"
min="0"
max="21"
value={state.sezlong}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<label
htmlFor="sedintaFoto"
className="relative inline-flex items-center mb-4 cursor-pointer"
>
<input
type="checkbox"
name="sedintaFoto"
id="sedintaFoto"
className="sr-only peer"
checked={state.sedintaFoto}
onChange={handleChange}
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
Sedinta foto
</span>
</label>
<label
htmlFor="petrecerePrivata"
className="relative inline-flex items-center mb-4 cursor-pointer"
>
<input
type="checkbox"
name="petrecerePrivata"
id="petrecerePrivata"
className="sr-only peer"
checked={state.petrecerePrivata}
onChange={handleChange}
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
Petrecere Privata
</span>
</label>
</>
)}
<button
type="submit"
className="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Subimt
</button>
</form>
</div>
</div>
</>
)
}
export default Add
useCurrentBookings hook
import { useQuery } from "blitz"
import getAllBookings from "../queries/getAllBookings"
import format from "date-fns/format"
export const useCurrentBookings = (startDate) => {
const [booking] = useQuery(getAllBookings, format(startDate, "yyyy-MM-dd")) // Here I query the database
return booking
}
Actual call to the database
import db from "db"
//And this is the actual call to the database
export default async function getAllBookings(startsAt: string) {
return await db.booking.findMany({
where: { starts_at: { gte: new Date(startsAt) } },
})
}
Solution 1:[1]
useEffect() runs everytime the dependencies change, inside the useEffect your updated the state and called useEffect again. Resulting in an infinite loop.
Resolution:
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
const totalPrice =
state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15
setTotalPrice(totalPrice);
}, [state])
Solution 2:[2]
I had this problem before, keep refreshing a react component it's because of the Lifecycle in React. If you do not know about it, be sure to research it deeply.
when you render your component it calls PescuitSelect() function
and in this function
setAvailableSpots({ ...availableSpots, pescuit: availableFishingSpots })
one of your state will be updated. in React when a state updated, the component will refresh again for showing new data of that state
Solution 3:[3]
Issue
The PescuitSelect
component is unconditionally updating state, which is an unintentional side-effect and triggers a rerender.
const PescuitSelect = () => {
const totalFishingSpots = Array.from(Array(114).keys())
const bookings = useCurrentBookings(startDate) //useCurrentBookings is a hook I created
const availableFishingSpots = totalFishingSpots.filter(
(o1) => !bookings.some((o2) => o1 === o2.loc_pescuit)
)
console.log(availableFishingSpots)
setAvailableSpots({ // <-- unconditional state update
...availableSpots,
pescuit: availableFishingSpots
})
return (
<select>
{availableSpots.pescuit.map((value) => {
return (
<option value={value} key={value}>
{value}
</option>
)
})}
</select>
)
}
On top of this, PescuitSelect
is redeclared each render cycle since it is defined inside another React component. It's an anti-pattern to declare React components within React components. They should all be declared at the top-level. Pass any callbacks in as props if necessary instead of trying to use values/callbacks closed over from the outer scope.
There is also a useEffect
hook that is updating the state it's using as its dependency.
// Update the price as soon as any of the options changed
useEffect(() => {
const totalPrice =
state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15
setState({ ...state, totalPrice: totalPrice })
}, [state]);
Updating state.totalPrice
updates the state
value and also triggers a rerender which will cause the effect to run again and enqueue another state update. This totalPrice
state is easily derived from the other existing state, and as such isn't necessary to also be stored in state.
Solution
Move the PescuitSelect
component declaration outside this Add
component.
Since it seems that PescuitSelect
doesn't have any state and the logic to compute the available fishing spots only exists to update the state in the parent, this logic should be moved to the parent and an availableSpots
array passed as a prop to PescuitSelect
.
Example:
const PescuitSelect = ({ options }) => (
<select>
{options.map((value) => (
<option value={value} key={value}>
{value}
</option>
))}
</select>
);
The logic that was moved should be placed in an useEffect
hook. Add any necessary dependencies.
Remove the useEffect
hook that is only computing a totalPrice
and just compute this each render. If computations like this are expensive then use the useMemo
hook to memoize the result.
Example:
type booking = {
starts_at: Date
ends_at: Date
intrare_complex: number
loc_parcare: number
loc_pescuit: number
casuta: number
sezlong: number
sedinta_foto: boolean
petrecere_privata: boolean
total_price: number
}
const Add: BlitzPage = () => {
//State for all options that will be added for the booking
const [state, setState] = useState({
intrare: 1,
locParcare: 0,
locPescuit: 0,
casuta: 0,
sezlong: 0,
sedintaFoto: false,
petrecerePrivata: false,
});
...
const [availableSpots, setAvailableSpots] = useState({
pescuit: [0],
casute: {},
sezlonguri: {},
});
const bookings = useCurrentBookings(startDate);
useEffect(() => {
const availableFishingSpots = Array.from(Array(114).keys())
.filter(o1 => !bookings.some((o2) => o1 === o2.loc_pescuit));
console.log(availableFishingSpots);
setAvailableSpots(availableSpots => ({
...availableSpots,
pescuit: availableFishingSpots,
}));
}, [bookings]);
...
// Update the price as soon as any of the options changed
const totalPrice = useMemo(() => {
return state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15;
}, [state]);
...
return (
<>
...
<div className="mx-auto max-w-xs ">
<div className="....">
<form className="space-y-6" action="#" onSubmit={handleSubmit}>
...
{state.petrecerePrivata ? (
...
) : (
<>
...
<div>
...
<Suspense fallback="Cautam locurile de pescuit">
<PescuitSelect options={availableSpots.pescuit} />
</Suspense>
</div>
...
</>
)}
...
</form>
</div>
</div>
</>
)
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | alireza kargar |
Solution 3 |