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

时间:2020-04-16 16:24:07

标签: javascript reactjs react-hooks use-reducer

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

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

此刻,我有太多的rerender,但我也不确定我的减速器是否正确

在此仓库中可以看到没有任何注释的代码: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 个答案:

答案 0 :(得分:3)

我将为您提供一些有关计数时间和降低点击价值的意见。我将解释代码中的主要问题,并提供一种不同的实现方法,使您可以继续执行业务逻辑。

1。正确访问counts

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

2。 useReducer

的正确用法

useReducer在返回数组的第一项中为您提供一个状态对象。您立即使用{ counter }对其进行了分解。该计数器变量的值是初始值('')。减速器使用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保持完整状态,您可以使用其ID访问当前项目的counters对象的计数器:

    {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 are better suited for more complex state,但最后仍然是本地组件状态。

我不知道单击这些项目时想要实现什么,所以我减少了计数,因为我认为这就是这个问题。

以下是正在运行的以下代码的代码框: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;

答案 1 :(得分:1)

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

  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的答案,以了解如何更好地构造它,使其更具可读性和可维护性。