i have a little problem in my App ! it's a simple App that shows some restaurant tables and let you filter these tables by number of people eating (capacity) and the preferred place (Inside, Patio, Bar)
I have a problem specifically with this filter functionality: i'm able to filter the tables by Place, but when i try to simultaneosly filter the Tables by Capacity it won't work (it'll forget the Place filter, and just filter them by Capacity). I'll explain the user flow i used:
- I click on the Place select and choose the option 'Patio'
- At this point 2 tables are showed; one is for 5 people, the other for 9 people
- i click on the Capacity filter, choosing a table for 9 persons.
- the App will forget the 'Patio' value and it'll also show a table with Location ' Bar' (and that's the part i'm trying to fix, i want the App to remember 'Patio' and only showing that one)
const defaultState : any = {
tables: [],
tablesFiltered: [],
bookings: [],
error: null,
loading: false,
}
case FILTER_TABLE:
return {
...state,
tablesFiltered: action.payload,
}
this is my action
export const filterTables = (filteredTable: tableI[]) => {
return (dispatch: (arg0: { type: string; payload?: unknown; }) => void) =>
dispatch({type: FILTER_TABLE, payload: filteredTable})
}
finally, the component where the logic of the filter is:
TableFilter.tsx
import { Form } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { filterTables } from '../../store/actions';
import { tableI } from '../../Interfaces';
const TableFilter: React.FC = () => {
const tables: tableI[] = useSelector((state: any) => state.tables.tables);
const dispatch = useDispatch();
const handleChange = (event: any) => {
const locationFilter: string = event.target.value;
const capacityFilter: any = event.target.value;
// console.log(typeof capacityFilter);
const filteredArr = tables.filter(
(table) => table.location === locationFilter
);
// console.log(filteredArr);
dispatch(filterTables(filteredArr));
};
const changeCapacity = (event: any) => {
// const locationFilter: string = event.target.value;
const capacityFilter: any = event.target.value;
// console.log(typeof capacityFilter);
const filteredArr = tables.filter(
(table) => table.capacity >= Number.parseInt(capacityFilter)
);
// console.log(filteredArr);
dispatch(filterTables(filteredArr));
};
const locations: string[] = tables.map((table) => table.location);
const capacity: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const uniqueLocations = [...new Set(locations)];
return (
<Form>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Select a table</Form.Label>
<Form.Control
onChange={handleChange}
as="select"
aria-label="form-select-location"
>
<option>Select your table's location</option>
{uniqueLocations &&
uniqueLocations.map((location: string, index: number) => (
<option aria-label="location" key={index} value={location}>
{location}
</option>
))}
</Form.Control>
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Select the capacity of your table</Form.Label>
<Form.Control
onChange={changeCapacity}
as="select"
aria-label="form-select-capacity"
>
<option>Number of persons sitting </option>
{capacity &&
capacity.map((capacity: number, index: number) => (
<option aria-label="capacity" key={index} value={capacity}>
{capacity}
</option>
))}
</Form.Control>
</Form.Group>
</Form>
);
};
export default TableFilter;
here's my GitHub repo:
https://github.com/miki-miko/booking-system-testing
CodePudding user response:
You could take an array of all filters and filter the data by all filters, you actually have.
For example take two filters, like
capacity = n => ({ capacity }) => capacity >= n;
place = p => ({ place }) => place === p;
Then put them ino an array. It does not matter, if you use only one or more, if necessary.
filters = [
capacity(2),
place('Inside')
]
As result, it returns objects where all filters return true
, or at least a truthy return value.
This one with Array#every
returns the objects where all conditions are true.
result = data.filter(o => filters.every(fn => fn(o))); // all
If only one constraint has to be true, take Array#some
instead.
result = data.filter(o => filters.some(fn => fn(o))); // exists
CodePudding user response:
You should filter the data in your selector since it is derived data and you should not store such data in your redux state. Here is a simple example of how you could do this:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
tables: ['A', 'B', 'C', 'D', 'E'].flatMap((place) =>
[...new Array(5)].map((_, index) => ({
place,
capacity: index 1,
}))
), //not sure if filter needs to be here, is is shared
// by multiple components in your application?
filter: {
capacity: 'all',
place: 'all',
},
};
//action types
const SET_FILTER = 'SET_FILTER';
//action creators
const setFilter = (key, value) => ({
type: SET_FILTER,
payload: { key, value },
});
const reducer = (state, { type, payload }) => {
if (type === SET_FILTER) {
const { key, value } = payload;
return {
...state,
filter: {
...state.filter,
[key]: value,
},
};
}
return state;
};
//selectors
const selectTables = (state) => state.tables;
const selectFilter = (state) => state.filter;
const selectPlaces = createSelector(
[selectTables],
(tables) => [...new Set(tables.map(({ place }) => place))]
);
const selectCapacities = createSelector(
[selectTables],
(tables) => [
...new Set(tables.map(({ capacity }) => capacity)),
]
);
//return true if value is all, this is specific to the filer value
// if the value is "all" then return true
//When passing a function to this it returns a function that takes a value
// when calling that function with a value it returns a function that
// takes an item
const notIfAll = (fn) => (value) => (item) =>
value === 'all' ? true : fn(value, item);
//specific filter
const capacityBiggerThan = notIfAll(
//could do more abstraction here with getProp, and re usable compare
// functions but leave this out for simplicity
(value, item) => item.capacity >= Number(value)
);
const isPlace = notIfAll(
(value, item) => value === item.place
);
//Apply multiple filter functions, it receives an array of filter functions
// and returns a function that receives an item as parameter, when the item
// is passed to this function it will call all filter functions passing this item
const mergeFilters = (filterFunctions) => (item) =>
filterFunctions.every((filterFunction) =>
filterFunction(item)
);
//select filtered data
const selectFilterData = createSelector(
[selectTables, selectFilter],
(tables, { place, capacity }) => {
return tables.filter(
mergeFilters([
isPlace(place),
capacityBiggerThan(capacity),
])
);
}
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
const FilterSelect = React.memo(function FilterSelect({
filterKey,
value,
values,
}) {
const dispatch = useDispatch();
return (
<select
value={value}
onChange={(e) =>
dispatch(setFilter(filterKey, e.target.value))
}
>
<option value="all">all</option>
{values.map((place) => (
<option key={place} value={place}>
{place}
</option>
))}
</select>
);
});
const App = () => {
const { capacity, place } = useSelector(selectFilter);
const places = useSelector(selectPlaces);
const capacities = useSelector(selectCapacities);
const filterData = useSelector(selectFilterData);
return (
<div>
<FilterSelect
filterKey="place"
value={place}
values={places}
/>
<FilterSelect
filterKey="capacity"
value={capacity}
values={capacities}
/>
<div>
<h1>result</h1>
<pre>
{JSON.stringify(filterData, undefined, 2)}
</pre>
</div>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
The code may be confusing since there are functions that get a function passed to it and return a function that when called with a value will return yet another function. You could rewrite the filter in the following way but it would not be re usable and more difficult to extend:
const selectFilterData = createSelector(
[selectTables, selectFilter],
(tables, { place, capacity }) => {
//just put all logic in one function, easier to read but repeating logic
// and more difficult to maintain if many values are used or rules change
return tables.filter((item) => {
const placeFilter =
place === 'all' ? true : item.place === place;
const capacityFilter =
capacity === 'all'
? true
: item.capacity >= Number(capacity);
return placeFilter && capacityFilter;
});
}
);