【问题标题】:React function executes continuously and state keeps refreshingReact 函数不断执行,状态不断刷新
【发布时间】:2022-06-12 02:36:12
【问题描述】:

我在 React 中有一个奇怪的问题。我将 Blitz JS 框架与 Prisma 一起用于数据库。

我有一个函数可以从用户选择的日期开始查询数据库中的所有条目。它用于我正在尝试构建的预订系统。

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

如果我在函数内console.log(),它将永远在控制台菜单中运行。同样在终端中,我可以看到该函数大约每秒被调用一次。

terminal log

javascript console log

我也尝试从 useEffect() 查询数据库,但 useEffect()useQuery(来自 Blitz.js)不能一起工作

为了便于阅读,我将代码与 cmets 一起附上。

感谢您的宝贵时间!

主页:

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 每次状态更新时都会运行,但它也会更新状态,导致它循环

标签: javascript reactjs react-hooks blitz.js


【解决方案1】:

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

【讨论】:

  • 似乎 OP 想要在其他状态更新时重新计算 totalPrice 值。这个答案并没有完全捕捉到这一点。
  • 哎呀,你是对的。它只能为 totalPrice 创建另一个状态。
【解决方案2】:

我之前遇到过这个问题,一直刷新一个 React 组件是因为 React 中的生命周期。 如果您不了解,请务必深入研究。

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

【讨论】:

    【解决方案3】:

    问题

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

    【讨论】:

      猜你喜欢
      • 2023-01-27
      • 1970-01-01
      • 1970-01-01
      • 2013-01-08
      • 1970-01-01
      • 1970-01-01
      • 2023-02-09
      • 2021-04-28
      • 2020-05-26
      相关资源
      最近更新 更多