Functioneel programmeren met React-componenten

Waarschuwing: Deze blog is gericht op mensen die enigszins programmeertalen kunnen lezen 🙂. Ik hoop dat je zonder diepgaande kennis al wat kunt opsteken.

Het doel is om je te laten zien hoe je componenten kunt maken waarbij componenten niet altijd direct van elkaar afhankelijk zijn. Als ze dat wel zijn, noemen we dit ’tight coupling’. Dit is een term die je vaak tegenkomt in de softwareontwikkeling.

Onze applicaties (zoals Clientbox) zijn flink aan het groeien. Het is daarom belangrijk dat we componenten kunnen hergebruiken, maar dat ze nog steeds goed aan te passen zijn. Door componenten los te trekken zijn die later makkelijker in delen te vervangen of aan te passen en blijft de code overzichtelijk en up-to-date.

FP FTW! (Functional Programming For The Win)

React heeft de overstap gemaakt van class-componenten naar functionele componenten. Dit heeft vele voordelen, zoals simpelere en beter leesbare code. Lees hier meer over de overstap naar functionele componenten.

Hoewel de overstap nu niet direct relevant is, hier een klein voorbeeld van een functioneel component en JSX-syntax:

import { FC } from "react";

interface HalloProps {
    naam: string;
}

const Hallo: FC<HalloProps> = ({ naam }) => <h1>Hello, {naam}!</h1>;

const App = () => <Hallo naam="Wereld" />;

Hier is ‘naam’ een prop die je meegeeft aan het ‘Hallo’-component. Dit is een van de manieren om data door te geven aan een component. Het blijft simpelweg een functie die een stukje HTML (of eigenlijk JSX) teruggeeft. Je zou de functie ook direct kunnen aanroepen:

const App = () => Hallo({ naam: "Wereld" });
Echter, je wil een functioneel component niet als een gewone functie aanroepen, omdat je dan geen gebruik kunt maken van de voordelen van React:
  • Memoization: React zal de output van het component cachen en alleen opnieuw renderen als de input verandert.
  • Hooks: React heeft een aantal hooks die je kunt gebruiken om state en lifecycle te beheren.

Een stap terug: Wat is functioneel programmeren?

Nu een beetje off-topic, maar wel belangrijk om te weten. Ik heb het over functioneel programmeren, maar wat is dat precies?

Functioneel programmeren is een programmeerparadigma waarbij je voornamelijk met functies werkt. Dit betekent dat je functies kunt doorgeven als argumenten aan andere functies en functies kunt teruggeven als resultaat. Een voorbeeld in JavaScript:

const add = (a, b) => a + b;

const multiply = (a, b) => a * b;

const calculate = (a, b, operation) => operation(a, b);

console.log(calculate(2, 3, add)); // 5

console.log(calculate(2, 3, multiply)); // 6

Hier zie je dat de functie ‘calculate’ een ‘operation’ verwacht. Dit kan een van de twee functies zijn die we hebben gedefinieerd. ‘Calculate’ noemen we hier een hogere-orde functie, omdat het een functie verwacht als argument. De ‘add’ en ‘multiply’functies zijn first-class functies, omdat je ze kunt doorgeven als argument.

We kunnen first-class functies ook gebruiken om een functie te maken die een andere functie teruggeeft:

const add = (a, b) => a + b;

const createAdder = (a) => (b) => add(a, b);

const add2 = createAdder(2);

console.log(add2(3)); // 5

De weg naar functioneel programmeren met React-componenten

Stel, we willen een knop maken waarbij je ook een icoon kunt toevoegen. We gaan verschillende stappen doorlopen om te zien hoe we dit op een functionele en ontkoppelde manier kunnen doen.

Stap 1: Tight coupling (Niet gewenst)

import { FC } from "react";

interface IconProps {
  icon: string;
}

const Icon: FC<IconProps> = ({ icon }) => <i className={icon} />;

const Button = ({ icon, text }) => (
  <button>
    <Icon icon={icon} />
    {text}
  </button>
);

const App = () => <Button icon="home" text="Home" />;
In dit voorbeeld weet ‘Button’ dat het een ‘Icon’ moet renderen. Dit is een voorbeeld van tight coupling. ‘Button’ is afhankelijk van ‘Icon’. We kunnen ook geen extra props aan ‘Icon’ toevoegen zonder ‘Button’ aan te passen.

Stap 2: Inheritance (Niet gewenst)

import { FC } from "react";

interface IconProps {
  icon: string;
  loading?: boolean;
}

const Icon: FC<IconProps> = ({ icon, loading }) =>
  loading ? <i className="loading" /> : <i className={icon} />;

interface ButtonProps extends IconProps {
  text: string;
}

const Button: FC<ButtonProps> = ({ text, ...iconProps }) => (
  <button>
    <Icon {...iconProps} />
    {text}
  </button>
);

const App = () => <Button icon="home" text="Home" />;

Hier hebben we ‘IconProps’ gemaakt en ‘ButtonProps’ die ‘IconProps’ uitbreidt. Dit is een voorbeeld van inheritance. ‘ButtonProps’ erft van ‘IconProps’. We kunnen nu extra props aan ‘Icon’ doorgeven zonder ‘Button’ aan te passen.

Dit is echter niet gewenst, omdat ‘ButtonProps’ nu ook de ‘loading’ prop heeft. Dit is niet nodig voor ‘Button’ zelf en kan verwarrend zijn. Nu is het alleen ‘Icon’, straks is het misschien nog een andere component en welke props moet je dan allemaal doorgeven aan wie?

Stap 3: Component doorgeven als prop (Ontkoppeld, maar beperkt)

import { ReactNode, FC } from "react";

interface ButtonProps {
  text: string;
  icon: ReactNode;
}

const Button: FC<ButtonProps> = ({ text, icon }) => (
  <button>
    {icon}
    {text}
  </button>
);

const App = () => <Button icon={<Icon icon="home" />} text="Home" />;
Hier hebben we ‘ReactNode’ gebruikt als type voor ‘Icon’. Dit betekent dat je elk stukje JSX kunt doorgeven als ‘Icon’. Dit is een voorbeeld van ontkoppeling. ‘Button’ weet niet wat ‘Icon’ is en ‘Icon’ weet niet dat het in een ‘Button’ zit. Het nadeel is dat ‘Button’ geen props kan doorgeven aan ‘Icon’, omdat ‘Button’ niet weet van het bestaan van ‘Icon’. Dit beperkt de flexibiliteit.

Stap 4: Custom component (Ontkoppeld & met extra functionaliteit)

import { FC } from "react";

const HomeIcon: FC<{ loading: boolean }> = ({ loading }) => (
  <Icon icon="home" loading={loading} />
);

interface ButtonProps {
  text: string;
  IconComponent: FC<{ loading: boolean }>;
}

const Button: FC<ButtonProps> = ({ text, IconComponent }) => (
  <button>
    <IconComponent loading={true} />
    {text}
  </button>
);

const App = () => <Button IconComponent={HomeIcon} text="Home" />;
Nu hebben we een custom component gemaakt voor de ‘Icon’. Dit is een functioneel component dat een ‘loading’ prop verwacht. ‘Button’ verwacht nu een ‘IconComponent’ prop die een functioneel component is. Dit is een goede manier om componenten te maken die niet direct van elkaar afhankelijk zijn en toch data kunnen doorgeven. Bovendien kunnen we op deze manier extra functionaliteit of specifieke implementaties in ‘IconComponent’ stoppen, zonder dat ‘Button’ daar iets van hoeft te weten.

Stap 5: Render prop (Functioneel & ontkoppeld)

import { ReactNode, FC } from "react";

interface ButtonProps {
  text: string;
  renderIcon: ({ loading }: { loading: boolean }) => ReactNode;
}

const Button: FC<ButtonProps> = ({ text, renderIcon }) => (
  <button>
    {renderIcon({ loading: false })}
    {text}
  </button>
);

const App = () => (
  <Button
    renderIcon={({ loading }) => <Icon icon="home" loading={loading} />}
    text="Home"
  />
);
We hebben nu een ‘renderIcon’ prop toegevoegd aan ‘Button’. Dit is een functie die een object verwacht met een ‘loading’ prop. Dit is een voorbeeld van een render prop. ‘Button’ weet niet wat er gerenderd wordt, maar kan wel data doorgeven aan wat er gerenderd wordt. Dit is een goede manier om componenten te maken die niet direct van elkaar afhankelijk zijn en toch data kunnen uitwisselen.

Conclusie

Voor nu was dit een korte introductie in functioneel programmeren met React-componenten. Ik hoop dat je nu een idee hebt van hoe je componenten kunt maken die niet direct van elkaar afhankelijk zijn. In de praktijk zul je vaak een combinatie van deze methoden gebruiken, afhankelijk van de situatie. Als je properties simpelweg aan het doorgeven bent, is het goed om te kijken welke methode het beste past.

Referenties

Enkele aanbevolen blogs en artikelen:

Real life Clientbox voorbeeld

Drag and drop componenten: hierbij zie je dat type item niet uitmaakt voor de container. De container weet niet wat voor items erin zitten, maar weet wel hoe ze gesorteerd moeten worden. De weergave staat hier volledig los van, de container weet niet hoe de items of lijst er uit ziet.
import { FC, useCallback, useMemo } from "react";

export const FoodListListExample: FC<{
    horizontal?: boolean;
    items: FoodListInterface[];
    setItems: (food: FoodListInterface[]) => void;
}> = ({items, setItems, horizontal}) => {
    const setFoods = useCallback(
        (foods: Food[], containerId: string) => {
            setItems(items.map((i) => (i.id === containerId ? {...i, food: foods} : i)));
            return Promise.resolve();
        },
        [items, setItems]
    );

    const render = horizontal ? renderListHorizontal : renderListVertical;
    return (
        <DND.Container<FoodListType, FoodListInterface>
            renderDraggedItem={render}
            items={items}
            sortingStrategy={horizontal ? DND.horizontalListSortingStrategy : DND.verticalListSortingStrategy}
            onDrop={async ({placement, item}) => {
                setItems(
                    placeItemFromPlacement({
                        items,
                        placement,
                        item,
                    })
                );
            }}
            onRemove={async (item: FoodListInterface) => {
                setItems(items.filter((i) => i.id !== item.id));
            }}
        >
            {({items: draggedItems, ...rest}) => (
                <div
                    className={classNames(styles.listContainer, {
                        [styles.horizontal]: horizontal,
                    })}
                    {...rest}
                >
                    {draggedItems.map((item) => (
                        <DND.Item<FoodListType, FoodListInterface> item={item} key={item.id}>
                            {(props) =>
                                render({
                                    ...props,
                                    setItems: setFoods,
                                })
                            }
                        </DND.Item>
                    ))}
                </div>
            )}
        </DND.Container>
    );
};
Eric Pinxteren
Technologie