3 totaal verschillende design patterns voor React

Jeroen Groenendijk
Design patterns voor React

Ben jij PHP developer? Dan ben je waarschijnlijk vooral objectgeoriënteerd aan het programmeren (OOP). Logica en gegevens organiseer jij rondom objecten. Gebruik je daarbij frameworks als Symfony of Laravel, dan verandert er niets aan je manier van werken. Gebruik je echter React om gebruikersinterfaces mee te bouwen, dan is de aanpak anders. Functioneel georiënteerd programmeren (FP) is sinds een paar jaar* hun standaard.

Door deze andere aanpak bij het programmeren moet je ook anders kijken naar het ontwerp van je software. Design patterns zoals repositories, facades en value objects zijn niet of lastig toe te passen als je je strikt aan FP wilt houden. Maar dat wil niet zeggen dat een React codebase een hoop spaghetti moet zijn! In dit blog licht ik drie design patterns toe die je goed kunt gebruiken met React.

1. Container pattern

Als alternatief voor, of in combinatie met, het container pattern lees je ook wel ‘presentational components’, ‘controlled components’ of ‘stateless components’. In de basis houdt het in dat je een onderscheid maakt tussen het component dat verantwoordelijk is voor de presentatie en de container die zich druk maakt over state management en side effects.

Voorbeeld: je wilt een slideshow maken. De afbeeldingen voor de slideshow moeten via een API opgehaald worden. Die logica stop je in een container en vervolgens geef je het slideshow component alleen de links naar de afbeeldingen mee. Als je elders nu nogmaals een slideshow moet tonen maar dan met afbeeldingen uit een andere API dan kun je het component hergebruiken! Hier is dit voorbeeld in code:

// Slideshow.tsx


type Image = {
   uri: string;
   alt: string;
};


type Props = {
   slides: Image[];
};


export function Slideshow({ slides }: Props): React.ReactElement {
   return (
       <div>
           {slides.map((slide, index) => (
               <img key={index} src={slide.uri} alt={slide.alt} />
           ))}
       </div>
   );
}


// SlideshowContainer.tsx


export function SlideshowContainer(): React.ReactElement {
   const { data } = useApi('/slides');
   const slides = data?.slides ?? [];


   return <Slideshow slides={slides} />;
}

Voordelen

  • Het component is makkelijk te testen
  • Je kan makkelijker varianten van het component tonen in Storybook
  • Je componenten blijven kleiner en eventueel herbruikbaar omdat specifieke side effects in de container zitten.

Nadelen

  • Je logica staat op meer dan één plek.

2. Higher Order Component (HOC) pattern

Dit patroon zie je ook wel terug in styled components. Je gebruikt het als je bepaalde logica, zoals specifieke styling, los wilt houden van de rest van het component. Misschien ondersteunt je applicatie light en dark mode of misschien is je knop component soms groot en soms klein. In plaats van `isLarge = false` of `isLarge = true` continu mee te moeten geven kun je een `<LargeButton>` en een `<SmallButton>` maken met erg weinig overhead.

Hier is dit voorbeeld in code

function withStyles(Component: React.ReactElement, style: React.CSSProperties) {
   return (props: JSX.IntrinsicAttributes) => {
       return <Component style={style} {...props} />
   }
}


function Button(props: JSX.IntrinsicAttributes) {
   return <button {...props} />
}


export const LargeButton = withStyles(Button, { fontSize: 24, padding: 16 });
export const SmallButton = withStyles(Button, { fontSize: 12, padding: 8 });

Voordelen

  • Componenten krijgen niet teveel props en conditionele logica
  • Andere developers zien sneller het verschil tussen twee componenten
  • Weinig overhead om variaties van hetzelfde component te maken

Nadelen

  • Het wordt makkelijker om inconsistente variaties van eenzelfde component te maken

3. Layered frontend

Als je in je React codebase ook Typescript gebruikt, dan zul je als je applicatie groter wordt, merken dat sommige types of logica op meerdere plekken terugkomen. 

Je hebt een; 

  • hook die uit de API het adres ophaalt, 
  • componenten die (delen uit) het adres tonen en een 
  • container die het allemaal aan elkaar knoopt en waarschijnlijk ook nog valideert. 

In een layered frontend ga je eigenlijk een stap verder met hetzelfde idee dat ook achter het container component pattern zit: het scheiden van verschillende verantwoordelijkheden. Bij een van onze projecten passen we dit toe. Doorgaand op het voorbeeld van adressen hebben we naast lagen componenten en containers een losse service laag waarin je de Typescript types vindt, maar ook functies voor validatie en formatting. Eigenlijk zijn de services het kerndomein van de applicatie. Deze services zijn puur Typescript functies. Dit houdt ze makkelijk testbaar en breed inzetbaar. De bekende developer en schrijver Martin Fowler tekent dit design pattern als volgt uit (bron):

Voordelen

  • Duidelijkere algemene structuur, meer eenheid tussen functionaliteiten in de applicatie.
  • Domein logica is goed terug te vinden en automatische tests voor te schrijven.
  • Je focust steeds maar op één stukje (de UI, of de validatie, of de API service, etc.) in plaats van dat je het hele systeem in je hoofd moet hebben.

Nadelen

  • Als het team niet op één lijn zit over de aanpak verdwijnt de structuur ook weer snel.

Conclusie

Verschillende manieren van programmeren lossen andere problemen op, maar vragen ook om nieuwe oplossingen en denkrichtingen. Bij Enrise vinden we zelfontplooiing daarom erg belangrijk. Met interne en externe meetups, conferenties, demodagen en andere bijeenkomsten stimuleren we kennisontwikkeling. We raden iedereen aan om dat ook te doen!