Move Redux and routing docs to Flex Docs

This commit is contained in:
Kimmo Puputti 2019-01-29 13:29:30 +02:00
parent e2dc22f38f
commit 5d2601000b
3 changed files with 8 additions and 399 deletions

View file

@ -156,6 +156,8 @@ Flex Docs:
- [How to change FTW icons](https://www.sharetribe.com/docs/guides/how-to-change-ftw-icons/)
- [FTW customization checklist](https://www.sharetribe.com/docs/guides/ftw-customization-checklist/)
- [How to deploy FTW to production](https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/)
- [How routing works in FTW](https://www.sharetribe.com/docs/background/ftw-routing/)
- [How the Redux setup works in FTW](https://www.sharetribe.com/docs/background/ftw-redux/)
See also the following articles:
@ -169,11 +171,8 @@ See also the following articles:
- [How to use CI with FTW](https://www.sharetribe.com/docs/guides/how-to-use-ci-with-ftw/)
- [How to improve performance](https://www.sharetribe.com/docs/guides/how-to-improve-performance/)
Extra documentation for specific topics can be found in the following files:
- [Routing](routing.md)
- [Redux and duck files](redux.md)
- [Original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md)
See also the
[original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md).
The application was bootstrapped with a forked version of
[create-react-app](https://github.com/facebookincubator/create-react-app). While most of the

View file

@ -1,130 +1,5 @@
# Redux configuration
## What is Redux
Documentation moved to the Flex Docs site:
Flex Template for Web (FTW) is JavaScript single-page application (SPA). This means that the app
needs to be able to render several different layouts (pages) depending on user interaction. State
management is essential for this process. FTW needs to know if a user has been authenticated, if it
has received relevant data for the current page, and so on.
We use [Redux](https://redux.js.org/introduction) for state management on the application level. You
should read more about Redux before you start modifying queries to Flex API or creating new Page
level elements (unless you are modifying [a static page](static-pages.md)).
In the following subtopics, we assume that you know the
[basics of Redux](https://redux.js.org/basics) already.
- [Containers: Pages + TopbarContainer](#containers-pages--topbarcontainer)
- [Duck files](#duck-files)
- [Setting up Redux](#setting-up-redux)
- [Advanced Redux concepts: thunks](#advanced-redux-concepts-thunks)
## Containers: Pages + TopbarContainer
We have set up FTW so that pages are aware of Redux state store, but other components and forms are
purely
[presentational components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0).
This makes it easier to customize UI components - you don't need to be aware of the complexity
related to Redux setup when you just want to touch the behavior of some visual component. In those
cases, you can just head to `scr/components/` or `src/forms/` directory and you can see from there
what props are available for each component when they are rendered.
Naturally, there is a need for higher level components which fetch new data and define what props
are passed down to presentational components. In Redux terminology, those components are called
_Containers_. FTW has defined all the containers inside a directory called `src/containers/`. It
contains all the pages and a special container for top bar (TopbarContainer) since that is a very
complex component and it is shared with almost every page. You can read more about differences
between presentational and container components from an
[article written by Dan Abramov](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
The actual container setup of a page level component can be found from the bottom of the component
file. For example, `src/containers/TransactionPage/TransactionPage.js` connects itself to Redux
store with `mapStateToProps` and `mapDispatchToProps` functions:
```js
const TransactionPage = compose(
connect(
mapStateToProps,
mapDispatchToProps
),
injectIntl
)(TransactionPageComponent);
```
## Duck files
Inside `src/containers/<ComponentName>` directory, we have also a special `<ComponentName>.duck.js`
file. These files wrap all the other page-specific Redux concepts into a single file. Instead of
writing action types, action creators and reducers in separate files (and spread them around our
directory structure), FTW tries to keep pages encapsulated. Page specific actions, store updates,
and data fetches happen inside their respective directory - just look for a file which name follows
a pattern: `<ComponentName>.duck.js`. This pattern is just a simple concept of adding related things
into a single file. Read more from
[author's repository](https://github.com/erikras/ducks-modular-redux).
### Global reducers
Some reducers are needed in several pages. These global reducers we have defined inside `src/ducks/`
directory with their respective `*.duck.js` files. Most important global duck files are
`user.duck.js` and `marketplaceData.duck.js`.
## Setting up Redux
Container specific reducers are gathered and exported inside `src/containers/reducers.js` file and
global reducers are exported respectively in a file `src/ducks/index.js`.
With those exports, we are able to create appReducer (`src/reducers.js`):
```js
import { combineReducers } from 'redux';
import * as globalReducers from './ducks';
import * as pageReducers from './containers/reducers';
const appReducer = combineReducers({ ...globalReducers, ...pageReducers });
```
`appReducer` is called by `createReducer` function, which is called inside of `configureStore`
function (in `src/store.js`).
This setup creates a store structure that separates container specific state as well as global data
by their reducer names. Together with Ducks module naming schema, this means that:
- the state of the `ListingPage` can be found from `state.ListingPage` and
- the state of the global `user` object can be found from `state.user`.
## Advanced Redux concepts: thunks
One essential part of state management in FTW, is filling the Redux store with data fetched from the
Flex API. This is done with
[Redux Thunks](https://redux.js.org/advanced/asyncactions#async-action-creators), which is a Redux
middleware to create asynchronous action creators.
As with every other Redux store actions, we have defined Thunks inside `*.duck.js` file. For
example, fetching listing reviews can be done with a following thunk function:
```js
export const fetchReviews = listingId => (dispatch, getState, sdk) => {
// Make store aware of a request to fetch reviews
dispatch(fetchReviewsRequest);
// Fetch reviews using Flex SDK
return sdk.reviews
.query({
listingId,
state: 'public',
include: ['author', 'author.profileImage'],
'fields.image': ['variants.square-small'],
})
.then(response => {
const reviews = denormalisedResponseEntities(response);
// If fetch succeeds, make store aware of fetched data
dispatch(fetchReviewsSuccess(reviews));
})
.catch(e => {
// If fetch throws an error, save the error to the store (so that UI can react to it)
dispatch(fetchReviewsError(storableError(e)));
});
};
```
> Note: `sdk` parameter is also provided by Redux Thunk. We pass it to middleware in store.js:
> `thunk.withExtraArgument(sdk)`
https://www.sharetribe.com/docs/background/ftw-redux/

View file

@ -1,270 +1,5 @@
# 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.
Documentation moved to the Flex Docs site:
- [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 => <AuthenticationPage {...props} tab="login" />,
},
{
path: '/signup',
name: 'SignupPage',
component: props => <AuthenticationPage {...props} tab="signup" />,
},
//...
];
};
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 => <ManageListingsPage {...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 => <CheckoutPage {...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 (
<BrowserRouter>
<Routes routes={routeConfiguration()} />
</BrowserRouter>
);
};
```
`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 (
<Switch>
{routes.map(toRouteComponent)}
<Route component={NotFoundPage} />
</Switch>
);
```
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 to listen location changes. For more information, see the
[How to set up Analytics for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-analytics-for-ftw/)
guide in Flex Docs.
## Linking
Linking is a special case in SPA. Using HTML `<a>` 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.
`<Link to="/about">About</Link>`) 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 `<NamedLink name="LoginPage" />` 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:
<NamedLink name="LoginPage" />log in</NamedLink>
// Link to ListingPage with path `l/<listing-uuid>/<listing-title-as-url-slug>/`:
<NamedLink name="ListingPage" params={{ id: '<listing-uuid>', slug: '<listing-title-as-url-slug>' }}>some listing</NamedLink>
// Link to SearchPage with query parameter: bounds
<NamedLink name="SearchPage" to={{ search: '?bounds=60.53,22.38,60.33,22.06' }}>Turku city</NamedLink>
```
`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 `<a href="externalsite.com">External site</a>`. (You can just change
the JSX element accordinly: `<ExternalLink href="externalsite.com">External site</ExternalLink>`.)
## 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. For more information, see the
[How to set up Analytics for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-analytics-for-ftw/)
guide in Flex Docs.
## 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);
}
```
https://www.sharetribe.com/docs/background/ftw-routing/