From b2348b4c91eb09063e159dfd77453259a59309e7 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 22 Oct 2018 23:55:22 +0300 Subject: [PATCH] Documentation for routing and loading data --- docs/README.md | 1 + docs/routing.md | 266 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 docs/routing.md diff --git a/docs/README.md b/docs/README.md index 25244273..ff06f0fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,6 +40,7 @@ Documentation for specific topics can be found in the following files: * [Styling a marketplace](styling.md) * [Static pages](static-pages.md) * [Terms of Service and Privacy Policy](terms-of-service-and-privacy-policy.md) +* [Routing](routing.md) * [Redux and duck files](redux.md) * [Extended data](extended-data.md) * [Extend the listing data model](extend-listing.md) diff --git a/docs/routing.md b/docs/routing.md new file mode 100644 index 00000000..89d17114 --- /dev/null +++ b/docs/routing.md @@ -0,0 +1,266 @@ +# Routing + +Flex Template for Web (FTW) uses [React Router](https://reacttraining.com/react-router/web) for +creating routes to different pages. React Router is a collection of navigational components that +allow single page apps to create routing as a part of normal rendering flow of the React app. So, +instead of defining on server-side what gets rendered when user goes to URL +`somemarketplace.com/about`, we just catch all the path combinations and let the app to define what +page gets rendered. + +* [React Router setup](#react-router-setup) +* [Linking](#linking) +* [Loading data](#loading-data) +* [Analytics](#analytics) +* [A brief introduction to server-side rendering](#a-brief-introduction-to-ssr) + +## React Router setup + +### Route configuration + +FTW has a quite straightforward routing setup - there's just one file you need to check before you +link to existing routes or start creating new routes to static pages: `src/routeConfiguration.js`. + +There we have imported and configured all the page-level components that are currently used within +FTW: + +```js +import { + AboutPage, + AuthenticationPage, + //... +} from './containers'; + +// Our routes are exact by default. +// See behaviour from Routes.js where Route is created. +const routeConfiguration = () => { + return [ + { + path: '/about', + name: 'AboutPage', + component: AboutPage, + }, + { + path: '/login', + name: 'LoginPage', + component: props => , + }, + { + path: '/signup', + name: 'SignupPage', + component: props => , + }, + //... + ]; +}; + +export default routeConfiguration; +``` + +In the example, path `/login` renders `AuthenticationPage` component with prop 'tab' set to 'login'. +In addition, this route configuration has a name 'LoginPage'. + +> Routes use exact path matches in FTW. We felt that this makes it easier to understand the +> connection between a path and its routed view aka related page component. +> [Read more.](https://reacttraining.com/react-router/web/api/Route/exact-bool) + +There are a couple of extra configurations you can set. For example `/listings` path leads to a page +that lists all the listings provided by the current user: + +```js + { + path: '/listings', + name: 'ManageListingsPage', + auth: true, + authPage: 'LoginPage', // default is 'SingupPage' + component: props => , + loadData: ManageListingsPage.loadData, + }, +``` + +Here we have set this route to be available only for authenticated user (`auth: true`), because we +need to know whose listings we should fetch. If a user is unauthenticated, he/she is redirected to +LoginPage (`authPage: 'LoginPage'`) before he/she can see the content of `ManageListingsPage` page. + +There's also a `loadData` function defined. It is a special function that gets called if a page +needs to fetch more data (e.g. from Flex API) after redirecting to that route. We'll open up this +concept [later in this document](#loading-data). + +In addition to these configurations, there's also a rarely used `setInitialValues` function that +could be defined and passed to a route: + +```js + { + path: '/l/:slug/:id/checkout', + name: 'CheckoutPage', + auth: true, + component: props => , + setInitialValues: CheckoutPage.setInitialValues, + }, +``` + +This function gets called when some page wants to pass forward some extra data before redirecting +user to that page. For example we could ask booking dates on ListingPage and initialize CheckoutPage +state with that data before buyer is redirected to CheckoutPage. + +### How FTW renders a router with routeConfiguration.js + +Aforementioned route configuration is used in `src/app.js`. For example, `ClientApp` defines +`BrowserRouter` and gives it a child component (`Routes`) that gets the configuration as `routes` +property. + +Simplified `app.js` code that renders client-side FTW app: + +```js +import { BrowserRouter } from 'react-router-dom'; +import Routes from './Routes'; +import routeConfiguration from './routeConfiguration'; +//... +export const ClientApp = props => { + return ( + + + + ); +}; +``` + +`src/Routes.js` renders the `Route` navigational components (`Switch` renders the first `Route` that +matches the location): + +```js +import { Switch, Route } from 'react-router-dom'; +//... + +const Routes = (props, context) => { + //... + return ( + + {routes.map(toRouteComponent)} + + + ); +``` + +Inside `src/Routes.js`, we also have a component called `RouteComponentRenderer`, which has three +important jobs: + +* Calling loadData function, if those have been defined in `src/routeConfiguration.js`. This is an + asynchronous call, a page needs to define what gets rendered before data is complete. +* Reset scroll position after location change. +* Dispatch location changed actions to Redux store. This makes it possible for + [analytics Redux middleware](analytics.md) to listen location changes. + +## Linking + +Linking is a special case in SPA. Using HTML `` tags will cause browser to redirect to given +`href` location. That will cause all the resources to be fetched again, which is a slow and +unnecessary step for SPA. Instead, we just need to tell our router to render a different page by +adding or modifying browser's history entries. + +### NamedLink and NamedRedirect + +React Router exports a couple of +[navigational components](https://reacttraining.com/react-router/web/api/Link) (e.g. +`About`) that could be used for linking to different internal paths. Since +FTW is a template app, we want all the paths to be customizable too. That means that we can't use +paths directly when redirecting user to another Route. For example marketplace for German customer +might want to customize the LoginPage path to be `/anmelden` instead of `/login` - and that would +mean that all the _Links_ to it would need to be updated. + +This is the reason why we have created names to different routes in `src/routeConfiguration.js`. We +have a component called `` and its _name_ property creates a link to +the correct Route even if the path is changed in routeConfiguration. Needless to say that those +names should only be used for internal route mapping. + +More complex example of `NamedLink` + +```js +// Link to LoginPage: +log in +// Link to ListingPage with path `l///`: +', slug: '' }}>some listing +// Link to SearchPage with query parameter: bounds +Turku city +``` + +`NamedLink` is widely used in FTW, but there are some cases when we have made redirection to another +page if some data is missing (e.g. CheckoutPage redirects to ListingPage, if some data is missing or +it is old). This can be done with rendering component called `NamedRedirect`, which is a similar +wrapper for [Redirect component](https://reacttraining.com/react-router/web/api/Redirect). + +### ExternalLink + +There's also a component for external links. The reason why it exists is that there's a +[security issue](https://mathiasbynens.github.io/rel-noopener/) that can be exploited when a site is +linking to external resources. `ExternalLink` component has some safety measures to prevent those. +We recommend that all the external links are created using `ExternalLink`component instead of +directly writing anchors like `External site`. (You can just change +the JSX element accordinly: `External site`.) + +## Loading data + +If a page component needs to fetch data, it can be done as a part of navigation. A page component +needs to define a static function called `loadData`, which needs to return a Promise, which is +resolved when all the asynchronous Redux Thunk calls are completed. + +For example here's a bit simplified version of `ListingPage.loadData` function: + +```js +export const loadData = (params, search) => dispatch => { + const listingId = new UUID(params.id); + + return Promise.all([ + dispatch(showListing(listingId)), // fetch listing data + dispatch(fetchTimeSlots(listingId)), // fetch timeslots for booking calendar + dispatch(fetchReviews(listingId)), // fetch reviews related to this listing + ]); +}; +``` + +> Unfortunately, `loadData` function needs to be separately mapped in routeConfiguration.js atm. +> There has been a problem with module initialization order and functional components have been used +> in routeConfiguration.js as wrappers to prevent a premature call to these static functions. + +## Analytics + +It is possible to track page views to gather information about navigation behaviour. Tracking is +tied to routing through `src/Routes.js` where `RouteRendererComponent` dispatches `LOCATION_CHANGED` +actions. These actions are handled by a global reducer (`src/ducks/Routing.duck.js`), but more +importantly, `src/analytics/analytics.js` (a Redux middleware) listens to these changes and sends +tracking events to configured services. [Read more.](analytics.md) + +## A brief introduction to SSR + +Server-side rendering needs a better documentation at some point, but this routing setup is the key +to render any page on server-side without duplicating routing logic. We just need to fetch data if +`loadData` is defined on page component and then use `ReactDOMServer.renderToString` to render the +app to string (requested URL is a parameter for this render function). + +So, instead of having something like this on Express server: + +```js +app.get('/about', handleAbout); +``` + +We basically catch every path call using `*` on `server/index.js`: + +```js +app.get('*', (req, res) => { +``` + +and then we ask our React app to + +1. load data based on current URL (and return this preloaded state from Redux store) +2. render the correct page with this preloaded state (renderer also attaches preloadedState to + HTML-string to hydrate the app on the client-side) +3. send rendered HTML string as a response to the client browser + +```js + dataLoader + .loadData(req.url, sdk) + .then(preloadedState => { + const html = renderer.render(req.url, context, preloadedState); + //... + res.send(html); + } +```