SSR React with Symfonyfor the Strong of Spirit Workshop

Routing

Theory
Let's talk about React Router.

Set up

Let's start by installing React Router:
npm i react-router-dom
Now, let's create a single entry point for our app that handles the routes in app/js/MoviesApp.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React from "react";
import { BrowserRouter, StaticRouter, Route } from "react-router-dom";
import MovieDetail from "./MovieDetail";
import MovieList from "./MovieList";

const MainApp = initialProps => (
  <div>
    <Route
      path={"/movie/:slug"}
      render={props => <MovieDetail {...props} {...initialProps} />}
    />
  </div>
);

export default (initialProps, context) => {
  if (context.serverSide) {
    return (
      <StaticRouter
        basename={context.base}
        location={context.location}
        context={{}}
      >
        <MainApp {...initialProps} />
      </StaticRouter>
    );
  } else {
    return (
      <BrowserRouter basename={context.base}>
        <MainApp {...initialProps} />
      </BrowserRouter>
    );
  }
};
Tip
If you want to do console.log(context)
inside of this component, you will see that it provides information about the location of the page.
React router has two different routers, one for SSR and another for in-browser rendering.
Let's change the registration point of our app to export this root component in assets/js/app.js
1
2
3
4
import ReactOnRails from "react-on-rails";
import MoviesApp from "./MoviesApp";

ReactOnRails.register({ MoviesApp });
And adapt the code in templates/movies/detail.html.twig
1
2
3
4
5
{% extends 'base.html.twig' %}

{% block body %}
{{ react_component('MoviesApp', {'props': props}) }}
{% endblock %}
With this we should be able to visit the page http://localhost:8080/movie/dr-no. The listing needs more work. Can you do it?
Exercise
Create a new route "/" in templates/js/MoviesApp.js that renders MovieList
...And make use of MoviesApp istead of Movielist in templates/movies/list.html.twig
It is very likely that you make "/" work and, in the process, you break the route for the movie detail (unless you copy the solution :D). Don't worry, we'll get there when we discuss the solution.
Reveal the solution
Got lost?
The code up to this point is in the tag 05-routing-setup of the repository https://github.com/Limenius/workshop-symfony-react.git
This means:
git reset HEAD --hard
(To discard your changes)
And then:
git checkout 05-routing-setup

Linking

Let's add a link to app/js/MovieDetail.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import MovieComponent from "./MovieComponent";
import { Link } from "react-router-dom";

export default class MovieDetail extends React.Component {
  render() {
    return (
      <div>
        <Link to="/">Back to list</Link>
        <MovieComponent movie={this.props.movie} />
      </div>
    );
  }
}
And add a link from the list to the detail pages:
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import MovieComponent from "./MovieComponent";
import { Link } from "react-router-dom";

export default class Movies extends React.Component {
  render() {
    return this.props.movies.map(movie => (
      <Link to={`movie/${movie.slug}`} key={movie.slug}>
        <MovieComponent movie={movie} />
      </Link>
    ));
  }
}
Discuss
The link works, but if we click on it, nothing will render. However if we reload the page, it will work. What is happening?
Got lost?
The code up to this point is in the tag 05-linking of the repository https://github.com/Limenius/workshop-symfony-react.git
This means:
git reset HEAD --hard
(To discard your changes)
And then:
git checkout 05-linking

Loading data from API

Let's modify app/js/MovieDetail.js so it can load the data it needs if it is not preloaded:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React from "react";
import MovieComponent from "./MovieComponent";
import { Link } from "react-router-dom";

export default class MovieDetail extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      movie:
        props.movie && props.match.params.slug == props.movie.slug
          ? props.movie
          : undefined
    };
  }

  componentDidMount() {
    if (!this.state.movie) {
      fetch(`/api/movies/${this.props.match.params.slug}`)
        .then(response => response.json())
        .then(movie =>
          this.setState({
            movie
          })
        );
    }
  }

  render() {
    if (!this.state.movie) {
      return "Loading...";
    }
    
    return (
      <div>
        <Link to="/">Back to list</Link>
        <MovieComponent movie={this.state.movie} />
      </div>
    );
  }
}
Now we can travel starting from the list to a movie detail by clicking on it and it will work. And we can click back to the list, because the state is preloaded. However, if we start from the movie detail (because the user landed there or did a full reload) and we go to the list, it won't work. Can you make the list load its data asyncronously from the API?
Exercise
Adapt the code in assets/js/MovieList.js so it loads its data asyncronously from the API"api/movies"In this case it will be enough with a constructor like
1
2
3
4
5
6
constructor(props) {
  super(props);
  this.state = {
    movies: props.movies
  };
}
templates/movies/list.html.twig
Reveal the solution
Got lost?
The code up to this point is in the tag 05-end-routing of the repository https://github.com/Limenius/workshop-symfony-react.git
This means:
git reset HEAD --hard
(To discard your changes)
And then:
git checkout 05-end-routing