如何在 ReactJS 中使用复选框在列表中应用过滤器?

vbe*_*nal 2 javascript reactjs redux react-redux

我有一个呈现一些产品的列表,这些产品分为一些类别。有些产品可能有多个类别。

我正在尝试通过复选框应用包含这些类别的过滤器。当用户选中该复选框时,必须使用所选类别更新列表。

在此输入图像描述

我仍然是初学者Redux,我不知道如何在组件之间进行通信以更新列表。我说组件之间的通信是因为我的类别列表在Drawer组件中,我的产品列表在Card组件中。

我把我的代码放入codesandbox,因为有很多文件

我在这里呈现我的产品列表:

import React, { useState, useMemo, useEffect } from 'react';
import { useSelector } from 'react-redux';

import CardItem from '../CardItem';
import Pagination from '../Pagination';
import Search from '../Search';
import { useStyles } from './styles';

const Card = (props) => {
  const { activeFilter } = props;
  const classes = useStyles();
  const data = useSelector((state) => state.perfume.collections);
  const [searchPerfume, setSearchPerfume] = useState('');
  const [currentPage, setCurrentPage] = useState(1);
  const [perfumesPerPage, setPerfumesPerPage] = useState(3);

  console.log('activeFilter: ', activeFilter);

  const filteredPerfumes = useMemo(() => {
    return data.filter((perfume) =>
      perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
    );
  }, [data, searchPerfume]);

  const currentPerfumes = filteredPerfumes.slice(
    (currentPage - 1) * perfumesPerPage,
    currentPage * perfumesPerPage
  );
  const pages = Math.ceil(filteredPerfumes.length / perfumesPerPage);

  useEffect(() => {
    if (currentPage > pages) {
      setCurrentPage(1);
    }
  }, [currentPage, pages]);

  const pageNumbers = Array(pages)
    .fill(null)
    .map((val, index) => index + 1);

  const handleClick = (page) => {
    setCurrentPage(page);
  };

  return (
    <div>
      <Search
        data-testid="input-filter-id"
        setSearchPerfume={setSearchPerfume}
      />
      {currentPerfumes
        .filter((perfume) => {
          return (
            perfume.name.toLowerCase().indexOf(searchPerfume.toLowerCase()) >= 0
          );
        })
        .map((item) => (
          <CardItem key={item.id} item={item} />
        ))}
      <Pagination
        pageNumbers={pageNumbers}
        handleClick={handleClick}
        currentPage={currentPage}
      />
    </div>
  );
};

export default Card;
Run Code Online (Sandbox Code Playgroud)

我在这里呈现我的类别列表:

import Divider from '@material-ui/core/Divider';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Checkbox from '@material-ui/core/Checkbox';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';

import { useStyles } from './styles';

const DrawerComponent = (props) => {
  const { activeFilter, setActiveFilter } = props;
  const classes = useStyles();
  const data = useSelector((state) => state.perfume.collections);

  const handleChange = (text) => (event) => {
    setActiveFilter((prev) => ({
      ...prev,
      value: event.target.checked,
      text,
    }));
  };

  const allCategories = data
    .reduce((p, c) => [...p, ...c.categories], [])
    .filter((elem, index, self) => index === self.indexOf(elem));

  return (
    <div className={classes.root}>
      <div className={classes.toolbar} />
      <Divider />
      <List className={classes.list}>
        {allCategories.sort().map((text, index) => (
          <ListItem className={classes.itemList} button key={text}>
            <Checkbox onChange={handleChange(text)} />
            <ListItemText primary={text} />
          </ListItem>
        ))}
      </List>
    </div>
  );
};

export default DrawerComponent;
Run Code Online (Sandbox Code Playgroud)

您知道如何将此过滤器应用到我的列表中吗?

预先非常感谢您。

Lin*_*ste 6

状态控制

通常,当您需要在组件之间进行通信时,解决方案是Lifting State Up。这意味着国家应该由共同的祖先控制。

而不是在你的DrawerComponent

const [activeFilter, setActiveFilter] = React.useState([]);
Run Code Online (Sandbox Code Playgroud)

将钩子移至您的并将和作为道具PerfumeStore传递给。activeFiltersetActiveFilterDrawerComponent

您需要向组件添加一个onChange函数,通过调用 来添加或删除类别。CheckboxDrawerComponentsetActiveFilter

所以现在您需要将 应用于activeFilter您的香水列表,该列表是在 中确定的Card。您可以将所有过滤移至PerfumeStore,但为了简单起见,让我们activeFilter作为 prop 向下传递Card(它只需要读取但不写入过滤器,因此我们不会向下传递setActiveFilter)。现在,该Card组件具有根据所选类别过滤项目所需的信息。

Redux 选择器

到目前为止,一切都与 React 有关,而您使用 redux 的事实根本没有发挥作用。如果您愿意,合并 redux 原则的方法是将组件外部的一些过滤和映射逻辑定义为选择器函数state和其他参数。然后,您可以使用仅获取所需数据的选择器进行调用useSelector,而不是调用以获取在组件内处理的大量状态。useSelector

执行此操作的一个明显位置是在 中DrawerComponent,您可以在其中获取类别列表。创建一个选择器函数getPerfumeCategories,它接受state并返回类别。然后在你的组件中,你调用const allCategories = useSelector(getPerfumeCategories);

现在该组件负责渲染类别。它不再负责存储选择(我们已经将其移出useState)或从状态中查找类别。这很好!如果您想更深入地了解为什么这是好的,您可以阅读单一职责原则、关注点分离、逻辑与表示组件等原则。

Card可以使用一个选择器来获取已过滤的香水列表。但在这种情况下,一个getCurrentPagePerfumes函数会接受很多不同的参数,所以有点混乱。

编辑:过滤

您已就如何应用 的值activeFilter来过滤列表中显示的香水寻求帮助。

可以一次选择多个类别,因此activeFilter需要识别所有主动选择的类别。我首先建议使用一个名称数组,但是从数组中删除项目(不进行突变)比为对象赋值更复杂。

所以后来我想到有一个对象,其中键是类别名称,值是boolean是否检查类别的真/假。这变得handleChange非常简单,因为我们可以将该键的值更新为 的值event.target.checked

  const handleChange = (text) => (event) => {
    setActiveFilter((prev) => ({
      ...prev,
      [text]: event.target.checked,
    }));
  };
Run Code Online (Sandbox Code Playgroud)
  • ...prev说“除了我要更改的密钥之外,保持一切不变”。
  • [text]说“我正在更新的键是变量text,而不是文字键‘text’”
  • event.target.checked是是否检查该类别的布尔值。

我们可以设置一个初始状态,其中activeFilter包括每个类别的键和所有值false(即未选择任何内容)。或者我们可以允许对象不完整,假设如果不包含它的键,则不会检查它。让我们这样做吧。

所以现在我们的activeFilter看起来像这样:{Floral: true, Floriental: false, Fresh: true}有些在true,有些在false,很多都丢失了,因此假设是false

我们需要弄清楚如何根据 的值过滤显示的香水activeFilter。让我们首先编写一个函数来确定一种香水是否有资格显示,然后我们可以将其用作香水数组上的 array.filter() 的回调。如果选中了任何类别的香水,我们希望将其包含在内(除非您希望它与所有选中的类别相匹配?)。看起来像:

    perfume.categories.some( 
      category => activeFilter[category] === true
    );
Run Code Online (Sandbox Code Playgroud)

其中.some()循环遍历类别,true如果我们的回调适用true于任何类别,则返回。

我将其添加到您的filteredPerfumes备忘录中并添加activeFilter为依赖项,以便在复选框更改时它会重新过滤。

  const filteredPerfumes = useMemo(() => {
    return data.filter((perfume) =>
      perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
      && perfume.categories.some( 
        category => activeFilter[category] === true
      )
    );
  }, [data, searchPerfume, activeFilter]);
Run Code Online (Sandbox Code Playgroud)

这是可行的,只是当没有检查类别时什么也没有显示 - 哎呀!因此,我们想添加一个特殊情况,即“如果没有检查类别,则所有香水都会通过类别过滤器”。为此,我们需要知道是否有已检查的类别。有很多方法可以做到这一点,但这里有一个:

const hasCategoryFilter = Object.values(activeFilter).includes(true);
Run Code Online (Sandbox Code Playgroud)

我们查看对象中的所有值activeFilter,看看它是否包含任何true.

现在我们需要使用这个值来仅在它为 时根据类别进行过滤true。我将把之前的逻辑拉入一个函数中并添加一条if语句(注意:布尔运算符||使用起来更短,但我认为if更具可读性)。

    const matchesCategories = (perfume) => {
      if ( hasCategoryFilter ) {
        return perfume.categories.some( 
          category => activeFilter[category] === true
        );
      } else return true;
    }
Run Code Online (Sandbox Code Playgroud)

旁注:我们有两个独立的过滤器,一个用于搜索,一个用于类别,因此我们可以调用data.filter一次并一次或两次检查两个条件,然后分别检查每个条件。你做什么并不重要。

最终的过滤器是:

  const filteredPerfumes = useMemo(() => {
    const hasCategoryFilter = Object.values(activeFilter).includes(true);

    const matchesCategories = (perfume) => {
      if ( hasCategoryFilter ) {
        return perfume.categories.some( 
          category => activeFilter[category] === true
        );
      } else return true;
    }

    return data.filter((perfume) =>
      perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
    ).filter( matchesCategories );
    
  }, [data, searchPerfume, activeFilter]);
Run Code Online (Sandbox Code Playgroud)

更新沙盒