Компоненти от по-висок ред (част 1)

“A low-angle shot of skyscrapers in downtown Calgary” by Samson Duborg-Rankin on Unsplash

В тази статия ще разгледаме какво са компоненти от по-висок ред, какво ни предоставят като функционалност и как ни помагат да решим често срещани проблеми.

Първо нека си припомним, че функции от по-висок ред са такива, които приемат поне една функция като аргумент и връщат функция като резултат. Компонентите от по-висок ред или Higher Order Components (HOC) следват същия принцип, а именно приемат поне един компонент и връщат компонент.

Ще разгледаме и как този design pattern ни предоставя вариант за решение на проблем като conditional rendering в React. Това приложение на HOC е едно от многото, например библиотека като Redux разчита на този принцип, за да “навръзва” променливите вътре в компонентите и решава проблем като “prop drilling”, който ще разгледаме в друга статия.

Проблемът, който ще засегнем е свързан с визуализацията на елементи в списък. Тъй като в нашият офис ценим повече плодовете, а и не искаме да бъдем банални, вместо познатия ToDoList, ще направим приложението FruitList. Също така ще се абстрахираме от целия boilerplate код, който имаме в едно приложение и ще разгледаме само това, което ни касае.

function App(props) {
return (
<FruitList fruits=(props.fruits) />
);
}

function FruitList({ fruits }) {
return (
<div>
{fruits.map(fruit => <FruitItem key={fruit.id} fruit={fruit} />)}
</div>
)
}

На пръв поглед всичко изглежда добре. От някъде сме получили props и съдържащите се в тях плодове ги подаваме на FruitList, който от своя страна ще итерира и генерира n на брой плодове в списъка.

Какво се случва обаче, когато масива с плодове, които получаваме на 7-ми ред е празен или е null? В някои случаи нищо, а в други потребителите виждат червени екрани или неформатирани съобщения за грешки. За да се подсигурим трябва да добавим обичайните проверки на уязвимите места.

function FruitList({ fruits }) {
if (!fruits) {
return null;
}

if (!fruits.length) {
return (
<div>
<p>There aren't any fruits in the office today :(</p>
</div>
)
}

return (
<div>
{fruits.map(fruit => <FruitItem key={fruit.id} fruit={fruit} />)}
</div>
)
}

Какво ново имаме в кода? На 2-ри и 6-ти ред вече имаме проверки за гореспоменатите проблеми.

В повечето случаи списъкът с плодове няма да е част от нашето приложение, затова ще трябва да го заредим с асинхронна заявка. Това води до друг проблем, че по никакъв начин не уведомяваме потребителя за това изчакване.

Нашият FruitList компонент няма как да знае за тази заявка, защото той само консумира данните подадени му отгоре. За да се подсигурим, добавяме състоянието на заявката като втори аргумент.

function FruitList({ fruits }) {
if (isLoadingFruits) {
return (
<div>
<p>Loading fruit list...</p>
</div>
)
}

if (!fruits) {
return null;
}

if (!fruits.length) {
return (
<div>
<p>There aren't any fruits in the office today :(</p>
</div>
)
}

return (
<div>
{fruits.map(fruit => <FruitItem key={fruit.id} fruit={fruit} />)}
</div>
)
}

Новият аргумент на 1-ви ред и допълнителната проверка на 2-ри решават и последния проблем.

След като сме проверили за възможни проблеми и състояния, можем да приемем, че компонентът ни е готов.

Сега ще разгледаме как компоненти от по-висок ред могат да ни помогнат да имплементираме същата функционалност по по-чист и модуларен начин.

Нека създадем една функция, която приема компонент като параметър, която от своя страна връща друга функция. Правим това, за да можем да достъпим props аргумента от подадения компонент. След проверка дали има плодове в props, решаваме дали да върнем компонент с подадени като параметър вече “проверените” props или да прекратим операцията.

const withFruitsNull = (Component) => (props) =>
!props.fruits ? null : <Component { ...props }/>;

След написването на същата проверка като тази във FruitList компонента, можем да преминем към подменянето им.

function FruitList({fruits}) {
if (isLoadingFruits) {
return (
<div>
<p>Loading fruit list...</p>
</div>
);
}

if (!fruits.length) {
return (
<div>
<p>There aren't any fruits in the office today :(</p>
</div>
);
}

return (
<div>
{fruits.map(fruit => <FruitItem key={fruit.id} fruit={fruit}/>)}
</div>
);
}
const FruitListWithNull = withFruitsNull(FruitList);
function App(props) {
return (
<FruitListWithNull fruits=(props.fruits) />
);
}

С последното “разместване” сме постигнали същата функционалност, както преди, с две разлики:

1. null проверката е изнесена в higher order component.

2. подаваме на App компонента новия HOC, а не директно FruitList.

По същия начин можем да изнесем и другите две проверки от FruitList в два отделни компонента от по-висок ред.

const withFruitsEmpty = (Component) => (props) =>
!props.fruits.length
? <div><p>There aren't any fruits in the office today :(</p></div>
: <Component { ...props }/>;

const withLoadingIndicator = (Component) => (props) =>
props.isLoadingFruits
? <div><p>Loading fruit list...</p></div>
: <Component { ...props }/>;

Правейки това, нашият FruitList компонент изглежда както в самото начало. Той връща само един контейнер, съдържащ итерацията на плодове и няма никакви проверки. За да подсигурим нашия списък с плодове, ще трябва да използваме и трите нови higher order components, за да може да съберем желаната функционалност в едно. Подобен резултат можем да постигнем по няколко различни начина.

Един не толкова практичен начин би бил “да извикаме” тези HOCs и резултата на всеки да го подаваме на следващия.

function FruitList({fruits}) {
return (
<div>
{fruits.map(fruit =>
<FruitItem key={fruit.id} fruit={fruit}/>)}
</div>
);
}

const withFruitsNull = (Component) => (props) =>/* Code */
const withFruitsEmpty = (Component) => (props) =>/* Code */
const withLoadingIndicator = (Component) => ({isLoadingFruits}) =>/* Code */

const FruitListNotNull = withFruitsNull(FruitList);
const FruitListNotEmpty = withFruitsEmpty(FruitListNotNull);
const FruitListWithLoading = withLoadingIndicator(FruitListNotEmpty);

function App(props) {
return (
<FruitListWithLoading
fruits={props.fruits}
isLoadingFruits={props.isLoadingFruits}
/>
);
}

С този подход постигаме желаната функционалост, но се оказваме в ситуация с един не-чист код (ред 13, 14 и 15), който би било хубаво да го рефакторираме.

Един начин по който ние се справяме с подобни проблеми е като използваме библиотека наречена recompose. С нея можем да направим bulk composition от n на брой HOCs, която ще върне само един компонент. Подобно решение би изглеждало по следния начин:

import { compose } from 'recompose';

const withConditionalRenderings = compose(
withLoadingIndicator,
withFruitsNull,
withFruitsEmpty
);

withConditionalRenderings е компонент съдържащ всички поверки, който ще сложим в return метода на App компонента.

В следващата част на тази статия, ще разгледаме как да правим по-абстрактни компоненти от по-висок клас, така че да можем да ги преизползваме в различни контексти.