Let’s refactor. React components and the only source of truth

Anna Prykhodko
4 min readFeb 27, 2023

--

In today’s example, I want to refactor a React component that implements filter logic. For the sake of example clarity, I removed properties and internal logic of components that are not relevant to the main functionality.

Photo by Tyler Nix on Unsplash

Disclaimer. I take refactoring examples from real sources (websites, open-source projects, articles), changing the names of entities or other unique components. I don’t and cannot know the context, conditions, or time when the code was written, and therefore all my fixes are my personal subjective perception of what could be changed in the code. Therefore my fixes do not carry any negative assessment of the code source or its creators.

Source

const Section = () => {
const [filters, setFilters] = useState({});
const onFilterChange = newFilters => setFilters(newFilters);
return (
<main>
<Filters onFilterChange={onFilterChange} />
{/* other components that use `filters` */}
</main>
);
};
const Filters = ({ onFilterChange }) => {
const [currentFilters, setCurrentFilters] = useState({});
const onChange = ({ name, value }) => {
const newCurrentFilters = { ...currentFilters, [name]: value };
setCurrentFilters(newCurrentFilters);
onFilterChange && onFilterChange(newCurrentFilters);
};
return (
<>
<Dropdown
name="periods"
options={options.periods}
onChange={onChange}
label="Filter by period"
value={currentFilters.periods}
/>
<Dropdown
name="types"
options={options.types}
onChange={onChange}
label="Filter by type"
value={currentFilters.types}
/>
</>
);
};

First of all, I notice duplication of data storage logic. We have two functions that update filters and two objects in which this data is stored. Their logic is identical. Maybe it was different before, and this is just the remnants after the changes. In any case, we do not need this duplication now.

This example’s second and last issue is the user experience (UX). From this perspective, our filters could be more convenient to use, and I will talk more about this below.

It should also be noted that I am not raising accessibility issues. I know that Dropdown is a material-ui component that I fully trust.

Whose password reigns, they hold the reins

Hooks in React significantly simplify data flow management. However, as is often the case with convenient solutions, people forget their cost and limitations. The data flow in React, which historically goes from top to bottom, is not always easy to control when there are many components using shared state. And considering how easy it is to use useState, the data flow can sometimes split or duplicate due to oversight.

This is exactly what can be noticed in our source code. It seems that instead of lifting the state, the developers duplicated it in the parent component, but also continued to use the old one. Such mistakes can be quite easily missed during code review.

The right solution in this case will still be to lift the state. However, in this case, we will not duplicate logic and create a single source of truth.

const Section = () => {
const [filters, setFilters] = useState({});
const onFilterChange = ({ name, value }) => {
setFilters({ ...filters, [name]: value });
};
return (
<main>
<Filters filters={filters} onFilterChange={onFilterChange} />
{/* other components that use `filters` */}
</main>
);
};
const Filters = ({ filters, onFilterChange }) => (
<>
<Dropdown
name="periods"
options={options.periods}
onChange={onFilterChange}
label="Filter by period"
value={filters.periods}
/>
<Dropdown
name="types"
options={options.types}
onChange={onFilterChange}
label="Filter by type"
value={filters.types}
/>
</>
);

The downside of this approach is that the filter processing logic is moved to an external component. If we want to use filters on another page, we will have to duplicate the onFilterChange function.

User experience

The next stage of refactoring is more abstract because its necessity is difficult to notice in the code. However, during development, it is enough to ask ourselves what we are doing and why.

What are the user scenarios for the filter? The user selects certain properties that narrow down the data selection. The user can pause work or share their selection with other people.

Does our component cover all user scenarios? No, because the data of enabled filters is stored in the component state and will be lost when the browser is reloaded.

Therefore, to refactor the component, we need to remove the filtering data from the component state. The best approach for this task, in my opinion, is to use the URL. It covers two main scenarios — saving filter results after reloading and the ability to share the state with other users.

For this example I will use the useSearchParams hook from the react-router-dom package. But of course, any other library that implements this functionality will be suitable.

const Section = () => {
const [searchParams] = useSearchParams();
return (
<main>
<Filters />
{/* other components that use `searchParams` */}
</main>
);
};
const Filters = () => {
const [searchParams, setSearchParams] = useSearchParams();
const onChange = ({ name, value }) => {
setSearchParams({ ...searchParams, [name]: value });
};
return (
<>
<Dropdown
name="periods"
options={options.periods}
onChange={onChange}
label="Filter by period"
value={searchParams.periods}
/>
<Dropdown
name="types"
options={options.types}
onChange={onChange}
label="Filter by type"
value={searchParams.types}
/>
</>
);
};

In this approach, the logic of filter processing returns to the Filters component, which makes it easier to reuse.

The downside, however, is that Filters does not have explicit data or interface binding. Therefore, to understand how to use it, you need to look inside the component or at examples of its use in the project.

Result

As you can see, there can always be several implementations, some of which are more suitable for the task, and some are less suitable. Each has its pros and cons. Some can be fixed by rewriting the code or changing the design, while others require writing detailed documentation and tests.

So don’t be afraid to experiment, relying on the needs of your users and your team.

Thanks for reading so far!
See you next time!
🤹🏻‍♀️

--

--

Anna Prykhodko
Anna Prykhodko

Written by Anna Prykhodko

Front-end developer · Kyiv 🇺🇦

No responses yet