遍历列表时如何动态使用useReducer?
How to dynamically use useReducer when looping over a list?
我正在尝试显示时间列表(例如 07:00、07:30),但是当出现重复时间时,请在其旁边显示重复次数(例如 07:30 , 08:00³)
当我遍历列表时,每个项目都需要有自己的状态,以便可以设置计数器并显示在每个时间旁边
目前,我遇到了太多重新渲染的问题,但我也不确定我的 reducer 是否正确
没有任何注释的代码可以在这个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: '',
})
My Context 导入和 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)
我有一个有用的函数来计算 Time 重复的次数
//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>
</>
)
}
我会给你一些关于计算时间和减少点击值的观察结果。我解释了您代码中的主要问题,并提供了一种不同的实现方法,使您可以继续您的业务逻辑。
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
保持完整状态,您可以使用其 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;
您在减速器中设置状态的方式与您检索它的方式不匹配。你也得到了太多的重新渲染,因为你多次调用 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 的回答,了解如何更好地构建它,使其更具可读性和可维护性。
我正在尝试显示时间列表(例如 07:00、07:30),但是当出现重复时间时,请在其旁边显示重复次数(例如 07:30 , 08:00³)
当我遍历列表时,每个项目都需要有自己的状态,以便可以设置计数器并显示在每个时间旁边
目前,我遇到了太多重新渲染的问题,但我也不确定我的 reducer 是否正确
没有任何注释的代码可以在这个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: '',
})
My Context 导入和 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)
我有一个有用的函数来计算 Time 重复的次数
//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>
</>
)
}
我会给你一些关于计算时间和减少点击值的观察结果。我解释了您代码中的主要问题,并提供了一种不同的实现方法,使您可以继续您的业务逻辑。
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
保持完整状态,您可以使用其 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;
您在减速器中设置状态的方式与您检索它的方式不匹配。你也得到了太多的重新渲染,因为你多次调用 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 的回答,了解如何更好地构建它,使其更具可读性和可维护性。