Коли ви починаєте писати компонент, зазвичай ви визначаєте всі функції та інші утиліти в одному файлі, але з часом, коли код зростає, ви захочете перемістити їх у окремі файли, щоб зберегти код чистим.
Але що робити, якщо одна з ваших функцій приймає параметр, який ви не зможете імпортувати? Це може бути стан, який ви визначили, або дані, які ви отримали з API.
Це можна вирішити за допомогою Function Currying.
Що таке Function Currying?
У двох словах, це спосіб перетворити функцію на функції з одним аргументом, класичний приклад — це метод множення:
// оригінальна функція
const multiply = (a, b) => {
return a * b
}
multiply(2, 4) // повертає 8
// Function Currying
const curryMultiply = (a) => {
const multiply = (b) => {
return a * b
}
return multiply
}
curryMultiply(2)(4) // повертає 8
Параметри діляться для кожної функції, і це буде корисно для нашого наступного прикладу.
Компонент
Кілька тижнів тому мені потрібно було реалізувати побудовувач запитів і я вирішив використати популярну бібліотеку react-query-builder, дуже настроюваний інструмент для побудови запитів з різними функціями, включаючи групування, комбінацію різних правил і перевірку полів.
Що мені потрібно було побудувати — це модуль, де користувачі з певними ролями в додатку могли б фільтрувати профілі клієнтів, використовуючи різні критерії, визначені під час виконання. Ці критерії стосувалися не тільки інформації про профіль клієнта, але й даних в інших пов’язаних таблицях.
Архітектурно додаток розділений на 2 частини: React фронтенд і Ruby on Rails бекенд (тільки API додаток на Ruby on Rails). Бекенд виконував би запит, створений на фронтенді, але також повертав би на фронтенд колекцію attributes
, з яких користувач міг би вибирати з відповідними types
і operators
. Описуючи це словами, бекенд міг би сказати: «Ви можете запитати дату народження, яка є датою, а оператори будуть більше ніж, дорівнює, менше ніж тощо».
Метою React Query Builder є створення запиту користувача в форматі, який бекенд зможе зрозуміти. Після обробки запиту бекенд повертає відфільтровані дані.
Проблема
Незважаючи на те, що був список операторів для кожного атрибуту, я міг мати тільки один тип значення на атрибут. Наприклад, поле з назвою reservation_date
завжди матиме тип значення date
, і в цьому випадку я маю оператори equals
, greater than
, less than
тощо. Але для того самого атрибуту інколи хочеться запитувати, чи є reservation_date
«до x днів тому», або «дні з того часу». В такому випадку ми не хочемо вводити дату, а хочемо ввести число.
React Query Builder підготовлений для таких ситуацій, оскільки надає функцію під назвою getInputType
, яка визначає, який інпут має бути відображений, коли вибрано певне поле. Однак проблема була в тому, що вона працює на основі атрибута, а не оператора, тому мені довелося внести деякі зміни в код.
export default function App() {
const [fields, setFields] = useState(searchFields)
const getInputType = (field, operator) => {
// Робимо щось, коли вибрано поле і оператор...
}
return (
) }
Ця функція також вимагає два параметри: field
і operator
.
Щоб зберегти компонент чистим та організованим, я захотів визначити свою кастомну функцію в окремому файлі, але було дві проблеми, які треба було вирішити:
- Мені потрібен був доступ до списку полів всередині цієї функції, щоб визначити відповідний тип вводу.
- Я не міг імпортувати поля з компонента в цей файл, оскільки вони зберігалися в стані.
Ось тут Function Currying став у пригоді.
Рішення
Розпочнемо з полів, формат кожного параметра виглядатиме наступним чином:
const fields = [
{
name: "reservation_date",
label: "Reservation",
operators: [
{
name: "eq",
label: "=",
valueType: "date",
},
{
name: "days_from_now",
label: "Days from now",
valueType: "number", // якщо дата відносна, ми хочемо, щоб це було число
}]
}
]
Як ви бачите, поле reservation_date
має масив operators
, і кожен оператор має свій різний valueType
. Тепер давайте подивимось на функцію getInputType
, яку ми створимо для обробки цих операторів:
// функція для встановлення типу пропса для вводу (date, text...)
const getInputType = (field, operator) => {
const value = fields.find((f) => f.name === field)
if (!value) return null const valueType = value.operators.find((op) => op.name === operator)?.valueType return valueType ?? null
}
Це виглядає чудово! Але, як я згадував раніше, є маленька проблема — я не можу отримати fields
, не порушивши логіку оригінальної функції. Ми можемо вирішити цю проблему, обгорнувши її в іншу функцію, яка братиме параметр, який мені потрібен, і повертатиме саме цю функцію.
export const defineGetInputType = (fields) => {
// функція для встановлення типу пропса для вводу (date, text...)
const getInputType = (field, operator) => {
const value = fields.find((f) => f.name === field)
if (!value) return null const valueType = value.operators.find((op) => op.name === operator)?.valueType return valueType ?? null
}
return getInputType
}
І ось і все, тепер я можу експортувати це і використовувати де завгодно. Ось код в дії:
Висновок
Function currying — це чудове рішення, коли потрібно обробляти параметри, які знаходяться поза лексичним контекстом. У моєму випадку, я зміг перемістити всі свої функції в файл utils.js
, щоб зберегти компонент більш організованим, це не тільки покращило читабельність, але й підвищило зрозумілість коду.
Тепер ваша черга спробувати!
Перекладено з: Function Currying: A real-life use example