Szybki sposób na create-react-app z SSR oraz styled-components

Niedawno pojawiła się druga wersja create-react-app, czyli najpopularniejszego sposobu na wystartowanie nowej aplikacji z użyciem Reacta. Pomimo dodania wielu nowych ficzerów jak np. wsparcie dla CSS Modules, ciągle może istnieć potrzeba dodania do CRA czegoś więcej, jak np. obsługi SSR. I dzisiaj właśnie o tym.

Jakiś czas temu dostałem do zrobienia pewne zadanie rekrutacyjne, które polegało na stworzeniu prostej apki w React z użyciem Google Maps. Głównym celem tego zadania było sprawdzenie pomysłu na architekturę i to czy poradzę sobie z nim wykorzystując wszelkie popularne wzorce i narzędzia dla ekosystemu Reacta. Myślałem więc nad poszczególnymi klockami architektury takimi jak redux, react-router czy styled-components. Ale zamarzyło mi się też aby dodać do tego projektu server side rendering. Po kilku artykułach doszedłem do wniosku, że połączenie tych wszystkich klocków może i skomplikowane nie jest, ale z pewnością zajmie mi trochę czasu. A ten kurczył mi się z każdą chwilą, ponieważ na wykonanie zadania miałem tydzień (i brak dni wolnych od pracy). Na szczęście znalazłem pewną paczkę, która sporo mi zaoszczędziła nerwów, ale też wniosła swoje problemy, o czym za chwilę opowiem.



Paczka cra-universal na ratunek!

Istnieje wiele odmian CRA (create-react-app), które wprowadzają możliwość stworzenia aplikacji izomorficznej/uniwersalnej czy po prostu z obsługą SSR. Wszystkie te trzy hasła sprowadzają się oczywiście do jednego – do uruchomienia naszej aplikacji po stronie serwera. Jedną z takich paczek jest cra-universal, która nie jest może kosmicznie popularna, ale obiecuje nam uzyskanie apki izomorficznej małym nakładem pracy. No to spróbujmy upichcić prosty projekt, zaczynając oczywiście od utworzenia go za pomocą create-react-app:

npx create-react-app test-app

Polecenie npx pozwala nam na wywołanie innej paczki z npm’a, nawet jeżeli nie została ona wcześniej zainstalowana. Jeżeli nie mamy npx’a zainstalowanego globalnie to wpierw trzeba to uczynić, albo zainstalować create-react-app globalnie. Do wyboru do koloru. Następnie przechodzimy do nowo utworzonego projektu i instalujemy wspomniany przeze mnie cra-universal:

cd test-app
yarn add --dev cra-universal

Praktycznie mamy już aplikację izomorficzną! Ale zanim ją uruchomimy, musimy jeszcze zmienić w pliku index.js sposób renderowania. W tym celu zmieniamy:

ReactDOM.render(<App />, document.getElementById('root'));

na:

ReactDOM.hydrate(<App />, document.getElementById('root'));

Dodajmy teraz jeszcze odpowiednie polecenia do sekcji scripts w pliku package.json w celu łatwiejszego uruchomienia:

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "ssr-start": "cra-universal start",
  "ssr-build": "cra-universal build"
}

Polecenie yarn ssr-start pozwoli nam uruchomić aplikację po stronie serwera,
natomiast yarn ssr-build utworzy wersję produkcyjną. Uruchamiając aplikację po stronie serwera za pomocą cra-universal musimy pamiętać żeby w osobnym oknie terminala uruchomić także „standardową” wersję, ponieważ obsługa SSR jest tutaj tworzona za pomocą proxy. Aby o tym nie zapominać, warto zaopatrzyć się w pakiet, który pozwoli uruchomić nam obie wersje jednocześnie, np. taki jak concurrently. Zainstalujmy tą paczkę i zmodyfikujmy nieco nasz skrypt:

yarn add --dev concurrently

następnie w package.json:

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
  "ssr-start": "concurrently \"cra-universal start\" \"react-scripts start\"",
  "ssr-build": "cra-universal build"
},

Teraz wystarczy, że uruchomimy w terminalu polecenie:

yarn ssr-start

i już możemy odwiedzić localhost:3001 aby zobaczyć aplikację uruchomioną po stronie serwera.

A może by tak styled-components?

Od wersji drugiej create-react-app uprościł znacząco podłączenie CSS modules i preprocesora SASS, natomiast w kierunku technik CSS-in-JS nie zostało wykonane nic, co oczywiście nie oznacza, że nie są one możliwe do dodania. Są i nie stanowią żadnego problemu. Inaczej ma się nieco kwestia SSR, gdzie o style również musimy się sami zatroszczyć i może to być nieco skomplikowane gdy robimy to po raz pierwszy.

W kilku ostatnich projektach w których miałem przyjemność uczestniczyć opierałem stylowanie od podstaw na styled-components. Moim zdaniem jest to już na tyle dojrzała biblioteka i przyjemna w stosowaniu, że nie ma się co wahać przy wyborze. I tutaj niespodzianka! Paczka cra-universal działa ze styled-components od razu, bez żadnej ingerencji po stronie serwera! W tym miejscu mógłbym więc zakończyć ten punkt, ale chciałbym pokazać coś innego. Styled-components oferują także plugin dla babela o nazwie babel-plugin-styled-components, który pozwala nam na konfigurację kilku ciekawych elementów, takich jak np. dodawanie do dynamicznie tworzonych klas przedrostka stanowiącego nazwę pliku. Nie chciałbym się w tym miejscu zagłębiać we wszystkie możliwości tej wtyczki, więcej można poczytać tutaj. Chciałbym natomiast pokazać jak można osiągnąć tą samą konfigurację po stronie serwera.

Po pierwsze, będziemy musieli zrobić coś w rodzaju ejecta dla cra-universal, a odbywa się to za pomocą polecenia init:

npx cra-universal init

W ten sposób w strukturze naszej aplikacji zostanie utworzony folder server. Zawiera on dwa pliki: app.js, który odpowiada za tworzenie aplikacji po stronie serwera, oraz index.js, który odpowiada za podstawową konfigurację w express.js i którego możemy wykorzystać także do serwowania innych rzeczy. Nie musimy zmieniać nic w tych plikach, wystarczy tylko, że obok nich utworzymy plik .babelrc z taką samą konfiguracją jaką mamy po stronie klienta. Przykładowa konfiguracja babela, ustawiająca przedrostek z nazwą pliku dla styled-components:

"babel": {
  "presets": [
    "react-app"
  ],
  "plugins": [
    ["babel-plugin-styled-components", {
      "fileName": true
    }]
  ]
}

I to wszystko 😉

Podsumowanie

W podstawowym zakresie paczka cra-universal spisuje się doskonale i nie trzeba nawet robić ejecta po utworzeniu projektu w create-react-app. Jeżeli jednak potrzebujemy dość mocno zmodyfikować tą bazę, np. poprzez dodanie styled-components to można nieco pobłądzić (głównie ze względu na słabą dokumentację cra-universal). Jest jeszcze kilka innych rzeczy, które chciałbym poruszyć jak np. podpinanie reduxa czy react-routera do cra-universal, ale to zostawię sobie na osobny wpis.
Mam nadzieję, że ten post przyczyni się do powstania jeszcze większej ilości fajnych aplikacji, które na start będą wyposażone we wsparcie dla server side renderingu. Mnie zabawa z ustawianiem tego wszystkiego z jednej strony frustrowała, z drugiej zaś dała sporą satysfakcję gdy już wszystko zaczęło pięknie śmigać. I tego drugiego wszystkim życzę!