I have a list that renders some products, these products are divided into some categories. Some products may have more than one category.
I am trying to apply a filter with these categories through checkboxes. When the user checks the checkbox, the list must be updated with the selected category.
I'm still a beginner in Redux
and I don't know how to municate between the ponents to update the list. I said munication between ponents because my list of categories is in the Drawer
Component, and my list of products is in the Card
ponent.
I put my code into codesandbox because has a lot of files
Here I'm rendering my list of products:
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;
I have a list that renders some products, these products are divided into some categories. Some products may have more than one category.
I am trying to apply a filter with these categories through checkboxes. When the user checks the checkbox, the list must be updated with the selected category.
I'm still a beginner in Redux
and I don't know how to municate between the ponents to update the list. I said munication between ponents because my list of categories is in the Drawer
Component, and my list of products is in the Card
ponent.
I put my code into codesandbox because has a lot of files
Here I'm rendering my list of products:
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;
Here I'm rendering my list of categories:
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;
Do you know how I can apply this filter to my list?
Thank you very much in advance.
Share Improve this question edited Oct 11, 2020 at 23:04 vbernal asked Oct 11, 2020 at 9:26 vbernalvbernal 7239 silver badges23 bronze badges1 Answer
Reset to default 6State Contol
Generally when you need to municate between ponents, the solution is Lifting State Up. Meaning that the state should be controlled by a mon ancestor.
Instead of having this in your DrawerComponent
:
const [activeFilter, setActiveFilter] = React.useState([]);
Move that hook up to your PerfumeStore
and pass activeFilter
and setActiveFilter
as props to DrawerComponent
.
You need to add an onChange
function to the Checkbox
ponents in DrawerComponent
which adds or removes the category by calling setActiveFilter
.
So now you need to apply the activeFilter
to your list of perfumes, which is determined in Card
. You could move all of that filtering up to PerfumeStore
, but to keep it simple let's pass activeFilter
down as a prop to Card
(it just needs to read but not write the filter, so we don't pass down setActiveFilter
). Now the Card
ponent has the information that it needs to filter the items based on the selected categories.
Redux Selectors
Everything so far has just had to do with react and the fact that you are using redux hasn't e into play at all. The way that you would incorporate redux principles, if you wanted to, is do define some of the filtering and mapping logic outside of your ponents as selector functions of state
and other arguments. Then instead of calling useSelector
to get a huge chunk of state which you process inside the ponent, you can call useSelector
with a selector that gets just the data which you need.
An obvious place to do this is in DrawerComponent
, where you are getting the category list. Make a selector function getPerfumeCategories
which takes the state
and returns the categories. Then in your ponent, you call const allCategories = useSelector(getPerfumeCategories);
Now all that this ponent is responsible for is rendering the categories. It is no longer responsible for storing the selections (we've already moved the useState
out) or for finding the categories from the state. This is good! You can read up on principles like the Single Responsibility Principle, Separation of Concerns, Logic vs. Presentation ponents, etc. if you want a deeper understanding of why this is good.
In Card
you could use a selector that gets an already-filtered list of perfumes. But in this case a getCurrentPagePerfumes
function would take a lot of different arguments so it's kind of messy.
Edit: Filtering
You've asked for help with how to apply the value of activeFilter
to filter the perfumes which are shown in your list.
Multiple categories can be selected at once, so activeFilter
needs to identify all of the actively selected categories. I first suggested an array of names, but removing items from an array (without mutation) is more plicated than assigning values to objects.
So then I thought about having an object where the keys are the category names and the values are a boolean
true/false of whether the category is checked. This makes handleChange
really simple because we can update the value for that key to the value of event.target.checked
.
const handleChange = (text) => (event) => {
setActiveFilter((prev) => ({
...prev,
[text]: event.target.checked,
}));
};
...prev
says "keep everything the same except the key that I am changing".[text]
says "the key I am updating is the variabletext
, not the literal key 'text'"event.target.checked
is the boolean of whether this category is checked.
We could set an initial state for activeFilter
which includes a key for every category and all the values are false
(ie. nothing selected). Or we could allow for the object to be inplete with the assumption that if it key isn't included, then it isn't checked. Let's do that.
So now our activeFilter
looks something like: {Floral: true, Floriental: false, Fresh: true}
where some are true
, some are false
, and lots are missing and therefore assumed to be false
.
We need to figure out how to filter the displayed perfumes based on the value of activeFilter
. Let's start by writing a function that determines whether one perfume is eligible to be shown, and then we can use that as a callback of array.filter() on an array of perfumes. We want a perfume to be included if any of its categories are checked (unless you want it to match all the checked categories?). That looks like:
perfume.categories.some(
category => activeFilter[category] === true
);
Where .some() loops through the categories and returns true
if our callback is true
for any category.
I added this to your filteredPerfumes
memo and added activeFilter
as a dependency so that it will re-filter when the checkboxes change.
const filteredPerfumes = useMemo(() => {
return data.filter((perfume) =>
perfume.name.toLowerCase().includes(searchPerfume.toLowerCase())
&& perfume.categories.some(
category => activeFilter[category] === true
)
);
}, [data, searchPerfume, activeFilter]);
That works, except that nothing shows when no categories are checked -- whoops! So we want to add a special case that says "all perfumes pass the category filter if no categories are checked." To do that, we need to know if there are checked categories or not. There's a lot of ways to do that, but here's one:
const hasCategoryFilter = Object.values(activeFilter).includes(true);
We look at all of the values in the activeFilter
object and see if it includes any which are true
.
Now we need to use this value to only filter based on categories when it's true
. I'll pull our previous logic into a function and add an if
statement (note: the boolean operator ||
is shorter to use, but I think the if
is more readable).
const matchesCategories = (perfume) => {
if ( hasCategoryFilter ) {
return perfume.categories.some(
category => activeFilter[category] === true
);
} else return true;
}
Sidenote: we have two independent filters, one for search and one for category, so we could call data.filter
once and check for both conditions at once or twice and check each condition separately. It does not matter which you do.
The final filter is:
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]);
Updated Sandbox