De innovatieve tech achter Create.nl

Gepubliceerd: 3 september 2019

Alex Lisenkov

Developer

Bij Create staat innovatie voorop! Dus, tijd voor een nieuwe website waarin we de laatste technieken hebben gebruikt. We hebben een React Hook geschreven die de componenten uit de API kan lezen.

Dit hebben we bereikt door de API blokken door te sturen. Van deze blokken is de data te beheren in het CMS en te gebruiken door de React App. Een pagina gebruikt zelf de blokken die nodig zijn, je kunt dus geen extra blokken toevoegen en de volgorde niet veranderen. Hierdoor kunnen we de intentie van het design gegarandeerd behouden. Hoe wij dit hebben gedaan? Dat lees je in dit artikel.

De technieken die wij hebben gebruikt

Niet elk project is hetzelfde, daarom verschilt het welke technieken er zijn gebruikt. We houden voor de front-end wel React aan, maar er hoeft niet altijd redux gebruikt te zijn of geschreven in TypeScript.

Voor de create website hebben we gekozen voor:

  • ReactJS
  • TypeScript
  • NodeJS
  • Sass imports
  • BEM
  • Laravel Nova

Waarom deze stack? React is iets waar we goed in zijn en wat mooi resultaat oplevert. Sass imports omdat we graag iets wilde wat dicht bij vanilla CSS ligt, en toch de voordelen van Sass behouden. TypeScript omdat static typing fouten kan voorkomen en je code voorspelbaar maakt. NodeJS, omdat je er gemakkelijk een SSR server mee op kan zetten. Laravel Nova omdat het ons een basis biedt waar we alle kanten mee op kunnen.

De structuur

Structuur van de Create website
Een afbeelding die de structuur van de create website weergeeft.
  1. Gebruik usePageData

    Gebruik op de pagina de usePageData hook en geef een endpoint mee, deze zal je voorzien van een methode om blokken op te halen

  2. Fetch een endpoint

    De usePageData hook zal asynchoon een fetch doen om de data op te halen en de response gebruiken voor zijn functionaliteit

  3. Het renderen van componenten

    De pagina zelf bepaald welke componenten gebruikt worden. Daarom levert onze hook een getComponent functie, deze geeft aan de hand van de type de juiste data. Die je vervolgens mee kan geven aan het component.

De API response

Voor de Create website vonden we het belangrijk dat de intentie van de designs intact bleef terwijl de content door iedereen te beheren is. Daarom hebben wij er voor gekozen dat componenten vast staan en alleen de content in het component dynamisch is.

JSON

1{
2  page: {
3    name: 'home',
4    slug: '/home',
5    seo: {
6      general: {
7        title: 'Create - Creative online applications',
8        description: 'Wij maken innovatieve applicaties met impact',
9        tags: ['Innovatief', 'Impact'],
10      },
11      // opengraph etc...
12    },
13  },
14  blocks: [
15    {
16      type: 'HeroBlock',
17      data: {
18        title: 'WIJ MAKEN INNOVATIEVE APPLICATIES MET IMPACT',
19        backgroundVideo: '...',
20        // etc
21      },
22    },
23    {
24      type: 'QuoteBlock',
25      data: {
26        quote: "Opportunities don't happen. You create them.",
27      },
28    },
29    // etc...
30  ],
31};

Hierboven zie je een voorbeeld van een API response. Deze bestaat uit twee properties, deze zijn “page” en “blocks”. Page bevat alle meta informatie, denk aan pagina titel en OpenGraph. Blocks is wat interessanter, deze bevat de blokken (of componenten) waar de pagina uit bestaat.

Een block bestaat uit een type en data, de type definieert om welk component het gaat en de data is de data die het component nodig heeft. Als ontwikkelaar zie je de bui al hangen; Types mappen aan React componenten en interfaces maken voor de data. Correct. Maar in de praktijk valt het wel mee. Als je een component schrijft, beschrijf je de data stiekem al via PropTypes of, als je TypeScript gebruikt, als generic.

TypeScript

1import React, { FC, ReactElement } from 'react';
2import './Quote.scss';
3
4export interface QuoteData {
5  quote: string;
6}
7
8interface QuoteProps {
9  data?: QuoteData;
10  className?: string;
11}
12
13
14const Quote: FC<QuoteProps> = ({
15  data,
16  className = '',
17}): ReactElement => {
18  const { quote } = data;
19
20  return (
21    <blockquote className={`quote ${className}`}>
22      {`"${quote}"`}
23    </blockquote>
24  );
25};
26
27
28export default Quote;
29

Het fetchen en renderen

Voor het fetchen van een pagina hebben we een hook geschreven, usePageData, hier in staat ook de map. Deze ziet er ongeveer zo uit:

TypeScript

1import React, { useContext, useEffect, useState } from 'react';
2import { QuoteData } from '../components/Quote/Quote';
3
4// Mapping
5export interface Components {
6  QuoteBlock: QuoteData;
7  // etc
8}
9
10export interface PageInfo {
11  name: string;
12  slug: string;
13  seo: SEOData;
14}
15
16export interface BlockData {
17  type: string;
18  data: Components;
19}
20
21export interface PageData {
22  page?: PageInfo;
23  blocks?: BlockData[];
24}
25
26const usePageData = (
27  endPoint: string,
28  args: RequestInit = { method: 'get' },
29) => {
30  const [status, setStatus] = useState<number | null>(null);
31  const [loading, setLoading] = useState(false);
32  const [pageData, setPageData] = useState<PageData | undefined>(undefined);
33
34  useEffect((): void => {
35    setLoading(true);
36
37    get(API_ENDPOINT + endPoint, args).then((response): void => {
38      setLoading(false);
39      setStatus(response.status);
40
41      if (response.ok) {
42        response.json().then((jsonBody): void => {
43          setPageData(jsonBody);
44        });
45      }
46    }).catch((): void => {
47      setLoading(false);
48    });
49  }, [endPoint]);
50
51  const getComponent = <T extends keyof Components>(component: T): Components[T] | undefined => {
52    if (!pageData || !pageData.blocks) {
53      return undefined;
54    }
55
56    let componentData;
57
58    pageData.blocks.forEach((block: BlockData): void => {
59      if (block.type === component) {
60        componentData = block.data;
61      }
62    });
63
64    return componentData && componentData as Components[T];
65  };
66
67  const hasComponent = (component: keyof Components): boolean => getComponent(component) !== undefined;
68
69  return {
70    getComponent,
71    hasComponent,
72    pageData,
73    loading,
74    status,
75  };
76};
77
78export default usePageData;
79

Op deze manier kunnen we heel makkelijk de usePageData hook gebruiken door een endpoint mee te geven. De containers zijn clean, componenten kunnen hergebruikt worden en componenten kunnen met weinig moeite worden toegevoegd. Dit is hoe ons contact-pagina er uit ziet:

TypeScript

1import React, { FC, ReactElement } from 'react';
2import { RouteComponentProps } from 'react-router';
3
4import usePageData from '../../hooks/usePage';
5import { NotFound } from '..';
6
7import {
8  ContactForm,
9  ContactInformation,
10  Map,
11} from '../../components';
12import './Contact.scss';
13
14type ContactProps = {} & RouteComponentProps;
15
16const Contact: FC<ContactProps> = (props): ReactElement => {
17  const { getComponent, status } = usePageData('/page/contact');
18
19  if (status && status !== 200) {
20    return <NotFound {...props} />;
21  }
22
23  const contactFormData = getComponent('ContactBlock');
24  const contactInformationData = getComponent('ContactInformationBlock');
25  const mapData = getComponent('MapBlock');
26
27  return (
28    <main className="contact-page">
29      <ContactForm data={contactFormData} />
30      <ContactInformation data={contactInformationData} />
31      <Map data={mapData} />
32    </main>
33  );
34};
35
36
37export default Contact;
38

In de praktijk verzorgt de usePageData hook ook voor het afhandelen van de data die via de SSR wordt geladen, het aanmaken van een Helmet react element, het onthouden van responses voor die sessie en het initiëren van de image observer.

Waarom wil je deze structuur gebruiken?

Om een structuur aan te houden waar alles is opgebouwd uit blokken met een type en de data die er bij hoort, waarbij de type direct correleert aan een component, zorgt voor een duidelijke structuur. Dit in combinatie met TypeScript zorgt voor schaalbaarheid en onderhoudbaarheid van de Create website. Een nadeel is dat de benamingen in de api en de react applicatie hetzelfde moet zijn. Dit zorgt er voor dat dit vooraf bepaald moet worden. We hebben een aantal keer meegemaakt dat we achteraf benamingen hebben aangepast, wat een verspilling van tijd is. Over het algemeen ben ik tevreden met de keuze en was het interessant om te werken aan een publieke website in tegenstelling tot maatwerk enterprise web apps waar ik normaal aan werk bij Create.

PS: Je kan onze oude website nog bezoeken op https://old.create.nl

Opmerkingen

Er zijn momenteel nog geen opmerkingen geplaatst onder deze blog.
Wil jij als eerste een bericht achter laten?

Laat een bericht achter
Aantal tekens:0 / 500

Beveiligd met reCAPTCHA.PrivacyVoorwaarden