循环遍历列表时如何动态使用useReducer?

IT技术 javascript reactjs react-hooks use-reducer
2021-05-20 12:03:15

我正在尝试显示时间列表(例如 07:00、07:30)但是当出现重复时间时,显示其旁边的重复次数(例如 07:30、08:00³)

当我遍历一个列表时,每个项目都需要自己的状态,以便可以在每个时间旁边设置和显示计数器

目前,我遇到了太多重新渲染的问题,但我也不确定我的减速器是否正确

没有任何注释的代码可以在这个 repo 中看到:https : //github.com/charles7771/decrease-number-wont-work/blob/master/index.js

const TimeGrid = () => {

  const reducer = (state, action) => {
    switch(action.type) {
      case 'SET_COUNTER':
        return {
          ...state,
          [`counter${action.id}`]: action.payload
        }
        default:
          return state
    }
  }

  //not sure if this bit is correct
  let [{ counter }, dispatchReducer] = useReducer(reducer, {
    counter: '',
  })

我的上下文导入和 allBookedTimes

const { theme, timesUnavailable, 
        removeFromTimesUnavailable, 
        addToTimesUnavailable } = useContext(Context)

const allBookedTimes = allBookings.map(element => element.time)
//below, both have been mapped out of a JSON file
const extractedTimesOnly = availableTimesJSON.map(item => item.time)
const availableTimes = availableTimesJSON.map(item => item)

我有有用的功能来计算时间重复的次数

  //used to count instances. (e.g. 22:30: 3, 23:00: 1)
  const counts = {}
  extractedTimesOnly.forEach(x => {
    counts[x] = (counts[x] || 0) + 1
  })

  //used to not repeat a Time
  const timeAlreadyDisplayed = []

这就是我用来循环遍历时间列表并显示每个时间列表并在其旁边显示计数器的逻辑,并尝试通过单击减少计数器。

  const displayAvailableTimes = availableTimes.map((item, index) => {
    //tries to set the value of counter0 (or counter1, ... counterN) 
    //to the number of instances it appears,
    //too many rerenders occurs...
    dispatchReducer({
      type: 'SET_COUNTER',
      id: item.id,
      payload: counts[`${item.time}`] //doesn't seem to be working. tried logging it and it shows nothing
    })

    //counter = counts[`${item.time}`] -----> works, but then I am not doing this through the dispatcher

    //maybe this logic could be flawed too?
    if (index > 0 &&
      item.time === availableTimes[index - 1].time &&
      item.time !== availableTimes[index - 2].time) {
      return (
          //tries to show the counter below
        <span> {counter} </span>
      )
    }
    else if (item.time > currentTime - 10 && !timeAlreadyDisplayed[item.time]) {
      timeAlreadyDisplayed[item.time] = true
      return (
        <li
          key={item.id}
          id={item.id}
          onClick={() => {
            //tries to decrease the counter, I don't think it works
            counter > 1 ? dispatchReducer({
              type: 'SET_COUNTER',
              id: item.id,
              payload: counter - 1
            }) :
              allBookedTimes.includes(item.time) && item.day === 'today'
                ? void 0
                timesUnavailable.includes(item)
                  ? removeFromTimesUnavailable(item)
                  : addToTimesUnavailable(item)
          }}>
          {item.time}
        </li>
      )
    } 
    return null
  })

  return (
    <>
      <ul>{displayAvailableTimes}</ul>
    </>
  )
}
2个回答

我会给你一些关于计算时间和减少点击值的意见。我解释了您代码中的主要问题,并提供了一种不同的实现方法,使您可以继续处理您的业务逻辑。

1. 正确访问 counts

forEach循环使用了数组的值作为密钥counts对象。似乎您更愿意使用该x.time值,因为这是您以后访问它的方式 ( payload: counts[${item.time} ])。x本身就是一个对象。

2. 正确使用 useReducer

useReducer在返回数组的第一项中为您提供一个状态对象。您立即使用{ counter }. 该计数器变量的值是初始值 ( '')。您的 reducer 使用 形式的键在状态对象中设置值counter${action.id},因此分解后的counter变量不会改变。

我想你想要这样的东西:

const [counters, dispatchReducer] = useReducer(reducer, {}); // not decomposed, the counters variable holds the full state of all counters you add using your `SET_COUNTER` action.

稍后,当您尝试渲染您当前所做的计数器时{ counter },它始终为空 ( ''),因为这仍然是指您的原始初始状态。现在counters保持完整状态,您可以counters使用其 id访问当前项目的对象计数器

    {if (index > 0 &&
      item.time === availableTimes[index - 1].time &&
      item.time !== availableTimes[index - 2].time) {
      return (
        <span> {counters[`counter${item.id}`]} </span>
      )
    }

3.通用代码结构

还有更多问题,代码非常疯狂且难以理解(例如,由于以令人困惑的方式混合概念)。即使你修正了上述观察,我怀疑它会导致你想要的东西或者你能够维持的东西。所以我想出了一个不同的代码结构,它可能会给你一种新的思考方式来实现它。

您不需要,useReducer因为您的状态非常平坦。Reducers 更适合更复杂的状态,但最终它仍然是本地组件状态。

我不知道您在单击项目时究竟想达到什么目的,所以我只是减少了数量,因为我认为这就是这个问题的意义所在。

这是以下代码的代码和框:https ://codesandbox.io/s/relaxed-roentgen-xeqfi?file=/src/App.js

import React, { useCallback, useEffect, useState } from "react";

const availableTimes = [
  { time: "07:30" },
  { time: "08:00" },
  { time: "08:00" },
  { time: "08:00" },
  { time: "09:30" },
  { time: "10:00" }
];

const CounterApp = () => {
  const [counts, setCounts] = useState({});
  useEffect(() => {
    const counts = {};
    availableTimes.forEach(x => {
      counts[x.time] = (counts[x.time] || 0) + 1;
    });
    setCounts(counts);
  }, []);

  const onClick = useCallback(time => {
    // Your logic on what to do on click goes here
    // Fore example, I only reduce the count of the given time.
    setCounts(prev => ({
      ...prev,
      [time]: prev[time] - 1,
    }));
  }, []);

  return (
    <div>
      <h2>Counts:</h2>
      <ul>
        {Object.keys(counts).map(time => (
          <li key={time} onClick={() => onClick(time)}>
            {time} ({counts[time]})
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CounterApp;

您在减速器中设置状态的方式与您检索它的方式不匹配。你也得到了太多的重新渲染,因为你调用dispatchReducer了多次(对于 中的每个元素一次availableTimes)。所有的逻辑都displayAvailableTimes应该在初始化 reducer 的状态时发生。

  const reducer = (state, action) => {
    switch(action.type) {
      case 'SET_COUNTER':
        return {
          ...state,
          [`counter${action.id}`]: action.payload
        }
        default:
          return state
    }
  }

  const counts = {}
  extractedTimesOnly.forEach(x => {
    counts[x] = (counts[x] || 0) + 1
  })

  const init = (initialState) => availableTimes.reduce((accum, item, index) => ({
      ...accum,
      `counter${item.id}`: counts[`${item.time}`]
  }), initialState);

  let [state, dispatchReducer] = useReducer(reducer, {
    counter: '',
  }, init)
const displayAvailableTimes = availableTimes.map((item, index) => {
  if (index > 0 &&
    item.time === availableTimes[index - 1].time &&
    item.time !== availableTimes[index - 2].time) { //An array out of bounds error could happen here, FYI
    return (
      <span> {state[`counter${item.id}`]} </span>
    )
  } else if (item.time > currentTime - 10 && !timeAlreadyDisplayed[item.time]) {
    timeAlreadyDisplayed[item.time] = true
      return (
        <li
          key={item.id}
          id={item.id}
          onClick={() => {
            state[`counter${item.id}`] > 1 ? dispatchReducer({
              type: 'SET_COUNTER',
              id: item.id,
              payload: state[`counter${item.id}`] - 1
            }) :
              allBookedTimes.includes(item.time) && item.day === 'today'
                ? void 0  //did you miss a colon here?
                timesUnavailable.includes(item)
                  ? removeFromTimesUnavailable(item)
                  : addToTimesUnavailable(item)
          }}>
          {item.time}
        </li>
      )
    } 
});

这将解决您现在面临的问题。但是,如果这就是你使用它的全部目的,你真的不需要减速器。请参考 Stuck 的答案,看看您应该如何更好地构建它,使其更具可读性和可维护性。