Нумо рефакторити. React компоненти та єдине джерело правди

Anna Prykhodko
4 min readFeb 27, 2023

--

У сьогоднішньому прикладі я хочу відрефакторити React компонент, що реалізує логіку фільтрації. Для чистоти прикладу я видалила властивості та внутрішню логіку компонентів, що не релевантні до основного функціоналу.

Photo by Tyler Nix on Unsplash

Disclaimer. Приклади для рефакторингу я беру із реальних джерел (сайтів, open-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}
/>
</>
);
};

Перш за все, мою увагу притягує дублювання логіки зберігання даних. У нас є дві функції, що оновлюють фільтри, та два об’єкти, в яких ці дані зберігаються. Їх логіка ідентична, можливо раніше вона була різна, а це всього лише залишки після змін. В будь-якому випадку, зараз нам не потрібне це дублювання.

Друге та останнє питання до цього прикладу це UX — досвід користувача. З цієї точки зору, наші фільтри не дуже зручні у використанні, більш докладно поговорю про це нижче.

Також слід зазначити, що я не піднімаю питання доступності, так як знаю, що Dropdown це компонент material-ui, яким я повністю довіряю щодо цього.

Чий пароль, той і король

Хуки у Реакті значно полегшили управління потоком даних. Але, як часто буває зі зручними рішеннями, — люди забувають їх ціну та обмеження. Потік даних, який в Реакті історично йде зверху вниз, не завжди легко контролювати, маючи багато компонентів, що використовують спільний стан. А враховуючи як просто можна зробити useState, потік даних за недоглядом іноді може розщеплюватися, дублюватися.

Саме це можна помітити у нашому першоджерелі. Здається, що замість підйому стану, розробники продублювали його у батьківському компоненті, але також продовжили використовувати старий. Такі помилки досить легко пропустити під час перевірки кода.

Правильним рішенням у цьому випадку буде все ж таки підняти стан. В такому випадку ми не будемо дублювати логіку та створимо єдине джерело істини.

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}
/>
</>
);

Мінусом цього підходу є те, що логіка обробки фільтрів винесена у зовнішній компонент. Якщо ми захочемо використати фільтри на іншій сторінці, то доведеться дублювати функцію onFilterChange.

Досвід користувача

Наступний етап рефакторингу більш абстрактний, так як його необхідність важко помітити у коді. Але достатньо під час розробки запитати себе — що ми робимо та навіщо.

Які є користувацькі сценарії для фільтру? Користувач обирає деякі властивості, що зменшують вибірку даних. Користувач може призупиняти роботу або ділитися своєю вибіркою з іншими людьми.

Чи покриває наш компонент усі користувацькі сценарії? Ні, бо дані ввімкнутих фільтрів зберігаються у стані компонента і будуть втрачені при перезавантаженні браузера.

Тож, щоб відрефакторити компонент, нам потрібно винести дані фільтрації із стану компонента. Найкраще для такої задачі, на мою думку, підходить URL, так як він покриє два основних сценарії — збереження результатів фільтрації після перезавантаження та можливість ділитися станом з іншими користувачами.

Для прикладу я використаю useSearchParams хук із пакету react-router-dom, але, звісно, підійдуть будь-які інші бібліотеки, що реалізують цей функціонал.

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}
/>
</>
);
};

У цьому підході логіка обробки фільтрів знову повертається у компонент Filters, що полегшує його перевикористання.

Мінусом же є те, що Filters не має явної прив’язки даних, інтерфейсу. Тому, щоб зрозуміти як його використовувати, треба подивитися всередину компонента, або на приклади його використання в проєкті.

Результат

Як бачите, реалізацій завжди може бути декілька, якісь із них більше підходять для задачі, якісь менше. У кожної є свої плюси та мінуси. Щось можна виправити переписуванням коду або зміною дизайну, а іноді написанням докладної документації та тестів.

Тож не бійтеся експериментувати, полягаючись на потреби ваших користувачів та вашої команди.

Дякую за те, що дочитали!
Побачимось наступного разу! 🤹🏻‍♀️

--

--

Anna Prykhodko
Anna Prykhodko

Written by Anna Prykhodko

Front-end developer · Kyiv 🇺🇦

No responses yet