简体   繁体   English

React 函数不断执行,状态不断刷新

[英]React function executes continuously and state keeps refreshing

I have kind of a weird issue in React.我在 React 中有一个奇怪的问题。 I am using the Blitz JS framework along side with Prisma for database stuff.我将 Blitz JS 框架与 Prisma 一起用于数据库。

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.获得数据后,我使用它创建一个<select>元素并将数据库中未出现的每个空间设置为<option> 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.一切正常, <select><option>显示它们应该显示的内容,但是一旦我单击下拉列表以查看所有可用选项,我认为状态会刷新并且菜单会关闭。

If I console.log() inside the function, it will go on forever in the console menu.如果我在函数内部使用console.log() ,它将在控制台菜单中永远存在。 Also in the terminal I can see the function being called about every second or so.同样在终端中,我可以看到该函数大约每秒被调用一次。

terminal log终端日志

javascript console log javascript控制台日志

I also tried querying the database from a useEffect() but useEffect() and useQuery (from Blitz.js) don't work together我还尝试从useEffect()查询数据库,但useEffect()useQuery (来自 Blitz.js)不能一起工作

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: useCurrentBookings钩子:

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) } },
  })
}

useEffect() runs everytime the dependencies change, inside the useEffect your updated the state and called useEffect again. useEffect() 每次依赖项更改时都会运行,在 useEffect 中您更新了状态并再次调用 useEffect。 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])

I had this problem before, keep refreshing a react component it's because of the Lifecycle in React.我之前遇到过这个问题,一直刷新一个 react 组件是因为 React 中的 Lifecycle。 If you do not know about it, be sure to research it deeply.如果您对此一无所知,请务必深入研究。

https://www.w3schools.com/react/react_lifecycle.asp#:~:text=Each%20component%20in%20React%20has,Mounting%2C%20Updating%2C%20and%20Unmounting . https://www.w3schools.com/react/react_lifecycle.asp#:~:text=Each%20component%20in%20React%20has,Mounting%2C%20Updating%2C%20and%20Unmounting

when you render your component it calls PescuitSelect() function当你渲染你的组件时,它会调用 PescuitSelect() 函数

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在 React 中,当状态更新时,组件将再次刷新以显示该状态的新数据

Issue问题

The PescuitSelect component is unconditionally updating state, which is an unintentional side-effect and triggers a rerender. PescuitSelect组件无条件更新状态,这是一个无意的副作用并触发重新渲染。

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.最重要的是, PescuitSelect在每个渲染周期都会重新声明,因为它是另一个 React 组件中定义的。 It's an anti-pattern to declare React components within React components.在 React 组件中声明 React 组件是一种反模式。 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.还有一个useEffect钩子正在更新它作为其依赖项使用的状态。

// 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.更新state.totalPrice更新state值并触发重新渲染,这将导致效果再次运行并将另一个状态更新入队。 This totalPrice state is easily derived from the other existing state, and as such isn't necessary to also be stored in state.totalPrice状态很容易从其他现有状态派生,因此不必也存储在状态中。

Solution解决方案

Move the PescuitSelect component declaration outside this Add component.PescuitSelect组件声明移到此Add组件之外

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 .由于PescuitSelect似乎没有任何状态,并且计算可用钓鱼点的逻辑只存在于更新父级中的状态,因此应该将此逻辑移至父级,并将availableSpots数组作为道具传递给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.被移动的逻辑应该放在一个useEffect挂钩中。 Add any necessary dependencies.添加任何必要的依赖项。

Remove the useEffect hook that is only computing a totalPrice and just compute this each render.删除仅计算totalPriceuseEffect挂钩,并在每次渲染时计算此值。 If computations like this are expensive then use the useMemo hook to memoize the result.如果像这样的计算很昂贵,那么使用useMemo挂钩来记忆结果。

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>
    </>
  )
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM