mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-25 22:37:18 +10:00
Move Redux and routing docs to Flex Docs
This commit is contained in:
parent
e2dc22f38f
commit
5d2601000b
3 changed files with 8 additions and 399 deletions
|
|
@ -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
|
||||
|
|
|
|||
129
docs/redux.md
129
docs/redux.md
|
|
@ -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/
|
||||
|
|
|
|||
269
docs/routing.md
269
docs/routing.md
|
|
@ -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/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue