簡體   English   中英

React 函數不斷執行,狀態不斷刷新

[英]React function executes continuously and state keeps refreshing

我在 React 中有一個奇怪的問題。 我將 Blitz JS 框架與 Prisma 一起用於數據庫。

我有一個函數可以從用戶選擇的日期開始查詢數據庫中的所有條目。 它用於我正在嘗試構建的預訂系統。

獲得數據后,我使用它創建一個<select>元素並將數據庫中未出現的每個空間設置為<option> 一切正常, <select><option>顯示它們應該顯示的內容,但是一旦我單擊下拉列表以查看所有可用選項,我認為狀態會刷新並且菜單會關閉。

如果我在函數內部使用console.log() ,它將在控制台菜單中永遠存在。 同樣在終端中,我可以看到該函數大約每秒被調用一次。

終端日志

javascript控制台日志

我還嘗試從useEffect()查詢數據庫,但useEffect()useQuery (來自 Blitz.js)不能一起工作

為了便於閱讀,我將附上代碼和注釋。

感謝您的時間!

主頁:

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鈎子:

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
}

對數據庫的實際調用:

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() 每次依賴項更改時都會運行,在 useEffect 中您更新了狀態並再次調用 useEffect。 導致無限循環。

解析度:

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])

我之前遇到過這個問題,一直刷新一個 react 組件是因為 React 中的 Lifecycle。 如果您對此一無所知,請務必深入研究。

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

當你渲染你的組件時,它會調用 PescuitSelect() 函數

在這個函數中

setAvailableSpots({ ...availableSpots, pescuit: availableFishingSpots })

您的狀態之一將被更新。 在 React 中,當狀態更新時,組件將再次刷新以顯示該狀態的新數據

問題

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

最重要的是, PescuitSelect在每個渲染周期都會重新聲明,因為它是另一個 React 組件中定義的。 在 React 組件中聲明 React 組件是一種反模式。 它們都應該在頂層聲明。 如有必要,將任何回調作為道具傳遞,而不是嘗試使用從外部范圍關閉的值/回調。

還有一個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]);

更新state.totalPrice更新state值並觸發重新渲染,這將導致效果再次運行並將另一個狀態更新入隊。 totalPrice狀態很容易從其他現有狀態派生,因此不必也存儲在狀態中。

解決方案

PescuitSelect組件聲明移到此Add組件之外

由於PescuitSelect似乎沒有任何狀態,並且計算可用釣魚點的邏輯只存在於更新父級中的狀態,因此應該將此邏輯移至父級,並將availableSpots數組作為道具傳遞給PescuitSelect

例子:

const PescuitSelect = ({ options }) => (
  <select>
    {options.map((value) => (
      <option value={value} key={value}>
        {value}
      </option>
    ))}
  </select>
);

被移動的邏輯應該放在一個useEffect掛鈎中。 添加任何必要的依賴項。

刪除僅計算totalPriceuseEffect掛鈎,並在每次渲染時計算此值。 如果像這樣的計算很昂貴,那么使用useMemo掛鈎來記憶結果。

例子:

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