I have an array of objects which I display in a grid. The goal is to have multiple filters on the data - one by text in a specific column, one by different date ranges, then sort by each of the columns in ascending/descending order and also have pagination(I already have the helper functions for this in place). My first approach was to have multiple useEffects inside which I do filter/sort my data and update the state. Problem is that the function that sets the state apparently doesn't update the state right away so in each next useEffect I'm not working with the filtered by the previous one data but with the original data. Basically I need to chain the 3 filter methods, then sorting, then pagination in this order, on the original array, but I don't want to do some of the filtering/sortings unless the user has changed the settings. Any ideas for the best way to approach this are appreciated. Here's a quick nonworking prototype. https://codesandbox.io/s/peaceful-nigh-jysdy The user can change filters at all times and all 3 should take effect, I also need to set an initial config to hold initial filter values
1 Answer
Your code has 2 main issues:
- the reducer is producing new filtered arrays at every dispatch,
- you are abusing useEffect and useState and you constructed a very convoluted data flow.
Let me explain both issues.
Reducer filtering
You have n items in the initial state of the reducer.
[
{ id: "124", name: "Michael", dateCreated: "2019-07-07T00:00:00.000Z" },
{ id: "12", name: "Jessica", dateCreated: "2019-08-07T00:00:00.000Z" },
{ id: "53", name: "Olivia", dateCreated: "2019-01-07T00:00:00.000Z" }
]
When you dispatch an action (such as FILTER_BY_TEXT), you are producing a new filtered array:
dispatch({ type: "FILTER_BY_TEXT", payload: { text: 'iv' } });
Gives:
[ { id: "53", name: "Olivia", dateCreated: "2019-01-07T00:00:00.000Z" } ]
But if you then dispatch a new value (in this case ae so that it should match Michael):
dispatch({ type: "FILTER_BY_TEXT", payload: { text: 'ae' } });
You get an empty array!
[]
This is because you are applying the ae filter over an already filtered list!
Convoluted data flow
Your application state is composed of:
- the data you want to show, filtered and sorted,
- the current values the user has chosen for the filters and sorts.
For every filter/sort “XXX” your current approach uses the following pattern:
function reducer(state, action) {
switch (action.type) {
case 'FILTER_BY_XXX': return filterByXXX(state, action);
default: return state;
}
}
function filterByXXX(state, action) { … }
function App() {
const [state, dispatch] React.useReducer(reducer, []);
// (1) Double state “slots”
const [xxx, setXXX] = React.useState(INITIAL_XXX);
const [inputXXX, setInputXXX] = React.useState(INITIAL_INPUT_XXX);
// (2) Synchronization effects (to apply filters)
React.useEffect(() => {
dispatch({ type: 'FILTER_BY_XXX', payload: xxx });
}, [xxx]);
return (
<input
value={inputXXX}
onChange={event => {
// (4) Store the raw input value
setInputXXX(event.currentTarget.value);
// (5) Store a computed “parsed” input value
setXXX((new Date(event.currentTarget.value));
}}
/>
);
}
Let me show the fallacies:
You don’t need a double state at (1), you are just over-complicating your code.
This is pretty obvious for string values, such as
filterByTextandinputVal, but for “parsed” values you need a little bit of explanation.It does make sense to separate the UI driven state from the “parsed” state, but you don’t need to store it in state! In fact you can always re-compute them from the actual input state.
Please have a look at this part of the React documentation: Main Concepts › Thinking in React › Identify The Minimal (but complete) Representation Of UI State
The “correct” approach is to remove the double slots at (1) and remove the setter at (5). You can then recompute the values at render time:
const [inputXXX, setInputXXX] = React.useState(INITIAL_INPUT_XXX);
// Here we just recompute a new Date value from the inputXXX state
const xxx = new Date(inputXXX);
return (
<input
value={inputXXX}
onChange={event => {
setInputXXX(event.currentTarget.value);
}}
/>
);
Your reducer, as explained before, starts with the full data set and is imperatively filtered at every dispatch. Every dispatch instructs the application to add another filter on top of the previously applied filters.
This happens because in the state you only have the result of the previous operations. This is similar to how you can treat long arithmetic operations as a succession of simpler ones:
11 + 23 + 560 + 999 = 1593can be rewritten as
( ( ( 11 + 23 ) + 560 ) + 999 ) = 1593 ( ( ( 34 ) + 560 ) + 999 ) = 1593 ( ( 594 ) + 999 ) = 1593At every step you are losing the information about how you got to that value, but you are seeing the result nonetheless!
When your application wants to “filter by text” it doesn’t want to “add” a filter, but substitute the previous text filter with the new text filter.
- You are reacting to changes in state by synchronizing “from the state to the reducer’s state” with an effect.
This is indeed a very correct use of useEffect, but if both the values you are synchronizing from and to are defined in the same place (or very near) as in this case, then probably you are over-complicating your code for no apparent reason.
You can, for example, just dispatch in the event handler:
return <input onChange={event => { dispatch( … ); } />;
But the real problem is the next one.
You are storing computed results as “state”, but it is actually “computed values”. Your real state is the whole dataset, the filtered list is an artifact of “applying filters to the whole dataset”.
You need to free yourself from the fear of computing values at render time:
const [inputVal, setInputVal] = React.useState("");
const [selectTimeVal, setSelectTimeVal] = React.useState("");
const [selectSortVal, setSelectSortVal] = React.useState("");
const data = [ {…}, {…}, {…} ];
let displayData = data;
displayData = filterByText(displayData, inputVal);
displayData = filterByTimeFrame(displayData, selectTimeVal);
displayData = sortByField(displayData, selectSortVal);
return (
<>
<input value={inputVal} onChange={………} />
<select value={selectTimeVal} onChange={………} />
<select value={selectSortVal} onChange={………} />
{displayData.map(data => <p key={data.id}>{data.name}</p>)}
</>
);
Further reading
Once you understand the topics listed above, you may want to avoid useless recalculations, but only once you actually hit some performance issues! (See the very detailed instructions about performance optimizations in the React docs)
If you get to that point, you’ll find very useful the following React APIs and Hooks:
6 Comments
render method for class components). Performance wise, there’s no way to compute each filter or sort separately: they literally just stack on each other. The order should not be important, since every filter check for values of a single item, it doesn’t depend on other filters.onChange={.....} parts supposed to do? For the selectTimeVal field, should its onChang call setSelectTimeVal, filterByTimeFrame, or something else entirely? It's not quite clear to me how that updates displayData on user interaction. Thank you!