Ostatnio dało się odczuć trochę mniejszą moją aktywność na blogu ale to nie znaczy, że z nim kończę (zresztą pisałem już o tym nieco w poście noworocznym). Mam przecież rozpoczętą serię o Server Side Rendering w React… A skoro, podobno, mężczyznę poznaje się nie po tym jak zaczyna ale jak kończy, to wydaje mi się, że moim obowiązkiem jest dokończyć to co zacząłem! Dlatego też dziś przedstawiam czwartą cześć moich wpisów na temat SSR, w której pokażę, jak zaprząc do działania bibliotekę react-router zarówno po stronie klienta jak i serwera. Zapraszam do lektury!

Niezbędne pakiety

Aby móc korzystać z biblioteki react-router po stronie klienta jak i serwera, muszę doinstalować do naszej aplikacji pakiet react-router-dom:

yarn add react-router-dom

Powyższe polecenie, oprócz instalacji pakietu react-router-dom zainstaluje też pakiet react-router (oraz kilka innych), który wykorzystam w naszej przykładowej aplikacji.

Zmiany w strukturze komponentów i konfiguracja routingu

W dzisiejszym wpisie przekształcę kod, który powstał jako przykład dla poprzedniego wpisu tej serii, w którym dodałem obsługę Reduxa. Utworzony dotychczas komponent App przekształcę w komponent Home, który wyświetlany będzie jako strona domowa. Dodam też nowy komponent About, który przedstawiać będzie podstronę “o stronie”. Następnie utworzę główny komponent aplikacji, w którym skonfiguję routing. Na koniec zajmę się modyfikacją kodu odpowiedzialnego za renderowanie naszej aplikacji po stronie serwera oraz klienta.

Komponenty Home oraz About

Na początek zajmę się komponentem App, który przekształcę w komponent Home. Tak na prawdę wystarczy tylko zmienić nazwę komponentu (oraz pliku, w którym się on znajduje):

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

class Home extends React.Component {
  static propTypes = {
    initialText: PropTypes.string.isRequired,
    changeText: PropTypes.func.isRequired
  }

  onButtonClick(event) {
    event.preventDefault();

    this.props.changeText();
  }

  render() {
    return (
      <div>
        <p>{this.props.initialText}</p>
        <button onClick={this.onButtonClick.bind(this)}>change text!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return { ...state };
};

const mapDispatchToProps = (dispatch) => {
  return {
    changeText: () => dispatch({ type: 'CHANGE_TEXT' })
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Home);

Kolejna rzecz, którą zrobię to dodanie nowego komponentu, przedstawiającego informacje “o stronie” - dla niepoznaki nazwę go About. Oto jak on wygląda:

import React from 'react';

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>Some information about the page</p>
    </div>
  );
};

export default About;

Powyżej widać, że jest to prosty komponent prezentacyjny (funkcyjny). Na potrzeby tego przykładu nie potrzebuję aby był on bardziej skomplikowany.

Konfiguracja routingu

Komponenty Home oraz About będą wyświetlane odpowiednio dla adresów / oraz /about. Routing ten skonfiguruję w głównym komponencie App, który teraz dodam (stary komponent App został przed chwilą przemianowany na Home):

import React from 'react';
import { Route, Link, Switch } from 'react-router-dom';
import Home from './Home';
import About from './About';

const App = (props) => {
  return (
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
      </ul>

      <hr />

      <Switch>
        <Route path="/about" component={About} />
        <Route path="/" component={Home} />
      </Switch>
    </div>
  );
};

export default App;

Jak widzisz, nie ma tutaj niczego niezwykłego: korzystam ze standardowych komponentów Route i Switch dostępnych w pakiecie react-router-dom. Oprócz tego, dodałem też “menu” nawigacyjne aby łatwiej było się poruszać po aplikacji. Korzystam tutaj z komponentu Link, który również dostępny jest we wspomnianym pakiecie.

Uwaga! Jeśli powyższy przykład nie jest dla Ciebie zrozumiały, proponuję abyś zajrzał do mojego wpisu na temat react-router w wersji 4. To powinno Ci trochę rozjaśnić temat.

I to wszystko jeśli chodzi o zmiany w strukturze komponentów oraz konfiguracji samego routingu. Teraz czas na…

Zmiany po stronie serwera

Na początek zajmę się renderowaniem naszej aplikacji po stronie serwera. Biblioteka react-router jest na to przygotowana i dostarcza odpowiednich narzędzi do tego celu. Zresztą spójrz na przykład:

import express from 'express';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducers from './reducers';
import { StaticRouter } from 'react-router';

import Html from './components/Html';
import App from './components/App';

const app = express();

app.use(express.static(path.join(__dirname)));

app.get('*', async (req, res) => {
  const scripts = ['vendor.js', 'client.js'];

  const initialState = { initialText: "rendered on the server" };
  const context = {};

  const store = createStore(reducers, initialState);

  const appMarkup = ReactDOMServer.renderToString((
    <StaticRouter location={req.url} context={context}>
      <Provider store={store}>
        <App />
      </Provider>
    </StaticRouter>
  ));
  const html = ReactDOMServer.renderToStaticMarkup(<Html children={appMarkup} scripts={scripts} initialState={initialState} />);

  res.send(`<!doctype html>${html}`);
});

app.listen(3000, () => console.log('Listening on localhost:3000'));

W sumie niewiele się tutaj zmieniło w stosunku do tego co zostało stworzone w poprzednim wpisie tej serii. Jedyne co doszło to import komponentu StaticRouter z pakietu react-router (nie mylić z react-router-dom). Komponentem tym “owijam” pozostałe komponenty przekazywane do metody renderToString oraz przekazuję do niego dwa atrybuty: location oraz context.

Do pierwszego z nich przekazuję wartość url żądania do serwera - w ten sposób react-router będzie umiał powiązać adres wpisany w pasku adresu przeglądarki z odpowiednim komponentem i go wyrenderować. Czyli jeśli wpiszemy “z palca” adres http://example.com/about to już na serwerze ścieżka ta zostanie rozwiązana i naszym oczom ukaże się komponent About (bez dodatkowych czynności po stronie klienta).

Drugim atrybutem, który przekazuję do komponentu StaticRouter jest context. Jest to zwykły obiekt, który zostanie dodany do “propsów” komponentu renderowanego dla danej ścieżki. Może on zostać zmodyfikowany podczas renderowania pasującego komponentu. Dla przykładu: jeśli dany adres zostanie powiązany z komponentem, który renderuje komponent Redirect to do obiektu context zostanie dodana właściwość url wskazująca, gdzie należy przekierować; dzięki temu możemy to rozpoznać już podczas przetwarzania żądania do serwera i wykonać przekierowanie “ręcznie”.

Uwaga! Więcej na temat obiektu context (i ogólnie SSR w react-router) przeczytasz na tej stronie dokumentacji.

Zmiany po stronie klienta

Ok, konfigurację po stronie serwera mamy już “ogarniętą”. Przyszła więc pora na renderowanie po stronie klienta. Tutaj nie ma już w zasadzie niczego nowego:

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducers from './reducers';
import App from './components/App';
import { BrowserRouter } from 'react-router-dom';

const store = createStore(reducers, { ...window.APP_STATE });

ReactDOM.hydrate((
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>
), document.getElementById('app'));

Jak widzisz, tym razem wykorzystuję komponent BrowserRouter z pakietu react-router-dom, którym “owijam” komponenty renderowane po stronie przeglądarki. Jeśli pracowałeś już z biblioteką react-router to wiesz, że to wystarczy aby zadziałał routing, który skonfigurowałem w komponencie App (ponownie odsyłam do mojego wpisu na temat react-rotuter v4).

Podsumowanie

I to tyle na dziś. Powyższy przykład przedstawia najbardziej podstawową konfigurację routingu, który zadziała zarówno po stronie serwera jak i klienta. Poniżej podaję link do repozytorium na GitHubie, którym możesz się samodzielnie pobawić - gorąco do tego zachęcam!

To jednak jeszcze nie koniec serii na temat SSR w React! Planuję jeszcze conajmniej jeden wpis, w którym pokażę bibliotekę next.js

Uwaga! Kod przedstawionego przykładu jest dostępny jako repozytorium w serwisie GitHub - kliknij tutaj aby je zobaczyć.


P.S. Ten wpis jest częścią serii wpisów na temat Server Side Renderingu w React! Poniżej lista wszystkich wpisów tej serii: