|
1 | 1 | # react-playground |
2 | 2 | React application with React Router v4, async components etc |
| 3 | + |
| 4 | +## What is this for? |
| 5 | +Demo app to show react router v4 beta with async component, code split and async reducer registration. |
| 6 | + |
| 7 | +Bases on excellent work of great people |
| 8 | + - [Dan Abramov on inject reducers](http://stackoverflow.com/questions/32968016/how-to-dynamically-load-reducers-for-code-splitting-in-a-redux-application/33045558) |
| 9 | + - [React async component](https://github.com/ctrlplusb/react-async-component) |
| 10 | + - [Create react app scripts](https://github.com/facebookincubator/create-react-app/tree/master/packages/react-scripts) |
| 11 | + - [React Universally](https://github.com/ctrlplusb/react-universally) |
| 12 | + - [React Router v4](https://github.com/ReactTraining/react-router/tree/v4) |
| 13 | + - [webpack 2](https://github.com/webpack/webpack/) |
| 14 | + |
| 15 | + |
| 16 | +## Client Side setup |
| 17 | + |
| 18 | +1. Open `Root.jsx`. There you can see that we have pretty basic RR4 routing. |
| 19 | +```jsx |
| 20 | +<ul> |
| 21 | + <li> <Link to="/">Home</Link> </li> |
| 22 | + <li> <Link to="/about">About</Link> </li> |
| 23 | + <li> <Link to="/topics">Topics</Link> </li> |
| 24 | + <li> <Link to="/legal">Legal</Link> </li> |
| 25 | +</ul> |
| 26 | +``` |
| 27 | + |
| 28 | +2. Open `client-entry.js`. Here we have Routing set up for React Router and Redux store. |
| 29 | +```jsx |
| 30 | +// create render function |
| 31 | +const render = RootEl => { |
| 32 | + const app = ( |
| 33 | + <Provider store={store}> |
| 34 | + <ReactHotLoader> |
| 35 | + <Router><RootEl /></Router> |
| 36 | + </ReactHotLoader> |
| 37 | + </Provider> |
| 38 | + ); |
| 39 | +``` |
| 40 | +
|
| 41 | +and set up for async components |
| 42 | +```jsx |
| 43 | +withAsyncComponents(app).then(({appWithAsyncComponents}) => { |
| 44 | + ReactDOM.render(appWithAsyncComponents, rootEl); |
| 45 | +}); |
| 46 | +``` |
| 47 | +
|
| 48 | +3. Open `About/index.jsx`. It has a bit more then you need to code split. And we will get back to it later. |
| 49 | +Below is full code you need to code split of components using RR4 and `react-async-component`. |
| 50 | +
|
| 51 | +```jsx |
| 52 | +import { createAsyncComponent } from 'react-async-component'; |
| 53 | + |
| 54 | +const AsyncAbout = createAsyncComponent({ |
| 55 | + name: 'about', |
| 56 | + resolve: () => new Promise(resolve => |
| 57 | + require.ensure([ |
| 58 | + './reducers/about' |
| 59 | + ], require => { |
| 60 | + const component = require('./containers/About').default; |
| 61 | + resolve({default: component}); |
| 62 | + }, 'about')) |
| 63 | +}); |
| 64 | + |
| 65 | +export default AsyncAbout; |
| 66 | +``` |
| 67 | +
|
| 68 | +### Server Side setup |
| 69 | +
|
| 70 | +Server side setup is done within `render-app.js` |
| 71 | +1. Redux Store and Router |
| 72 | +```jsx |
| 73 | +import {Provider as Redux} from 'react-redux'; |
| 74 | +import StaticRouter from 'react-router/StaticRouter'; |
| 75 | + |
| 76 | +const App = (store, req, routerContext) => ( |
| 77 | + <Redux store={store}> |
| 78 | + <StaticRouter location={req.url} context={routerContext}> |
| 79 | + <Root /> |
| 80 | + </StaticRouter> |
| 81 | + </Redux> |
| 82 | +); |
| 83 | + |
| 84 | +``` |
| 85 | +
|
| 86 | +2. rendering app with Router context and async components |
| 87 | +```jsx |
| 88 | +// create router context |
| 89 | +const routerContext = {}; |
| 90 | +// construct app component with async loaded chunks |
| 91 | +const asyncSplit = await withAsyncComponents(App(store, req, routerContext)); |
| 92 | +// getting async component after code split loaded |
| 93 | +const {appWithAsyncComponents} = asyncSplit; |
| 94 | +// actual component to string |
| 95 | +const body = renderToString(appWithAsyncComponents); |
| 96 | +``` |
| 97 | +
|
| 98 | +3. Rendering actual page is done in `Html.jsx`. For client to understand what content we rendered and do same we need to pass down async chunk state |
| 99 | +```jsx |
| 100 | +{asyncComponents && asyncComponents.state ? |
| 101 | + <script |
| 102 | + dangerouslySetInnerHTML={{ __html: ` |
| 103 | + window.${asyncComponents.STATE_IDENTIFIER} = ${serialize(asyncComponents.state, {isJSON: true})}; |
| 104 | + `}} /> : |
| 105 | + null} |
| 106 | +``` |
| 107 | +
|
| 108 | +And at this point you have SSR of React app using React router with Async Components. |
| 109 | +
|
| 110 | +## Handling 404 and redirects with React Router |
| 111 | +
|
| 112 | +1. View `Status.jsx`. All this component is doing really is just setting value on Static Router Context. |
| 113 | +
|
| 114 | +```jsx |
| 115 | +componentWillMount() { |
| 116 | + const { staticContext } = this.context.router; |
| 117 | + if (staticContext) { |
| 118 | + staticContext.status = this.props.code; |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | +
|
| 123 | +2. then we can handle this value in `render-app.js` for SSR |
| 124 | +
|
| 125 | +``` |
| 126 | +// checking is page is 404 |
| 127 | +let status = 200; |
| 128 | +if (routerContext.status === '404') { |
| 129 | + log('sending 404 for ', req.url); |
| 130 | + status = 404; |
| 131 | +} else { |
| 132 | + log('router resolved to actual page'); |
| 133 | +} |
| 134 | + |
| 135 | +// rendering result page |
| 136 | +const page = renderPage(body, head, initialState, config, assets, asyncSplit); |
| 137 | +res.status(status).send(page); |
| 138 | +``` |
| 139 | +3. This is basically same exact thing RR 4 is doing for redirect. |
| 140 | +
|
| 141 | +```jsx |
| 142 | +if (routerContext.url) { |
| 143 | + // we got URL - this is a signal that redirect happened |
| 144 | + res.status(301).setHeader('Location', routerContext.url); |
| 145 | +``` |
| 146 | +
|
| 147 | +4. If you try to navigate to `/legal` you will see that Not Found is returned and server is giving us 404 as expected. `/topic` will do 301 redirect. More details on how to use [Switch](https://reacttraining.com/react-router/examples/ambiguous-matches) |
| 148 | +
|
| 149 | +## Enabling async reducers |
| 150 | +
|
| 151 | +Coming back to About component. Full source |
| 152 | +
|
| 153 | +```jsx |
| 154 | +import { createAsyncComponent } from 'react-async-component'; |
| 155 | +import withAsyncReducers from '../store/withAsyncReducers'; |
| 156 | + |
| 157 | +const AsyncAbout = createAsyncComponent({ |
| 158 | + name: 'about', |
| 159 | + resolve: () => new Promise(resolve => |
| 160 | + require.ensure([ |
| 161 | + './reducers/about' |
| 162 | + ], require => { |
| 163 | + const reducer = require('./reducers/about').default; |
| 164 | + const component = require('./containers/About').default; |
| 165 | + const withReducer = withAsyncReducers('about', reducer)(component); |
| 166 | + resolve({default: withReducer}); |
| 167 | + }, 'about')) |
| 168 | +}); |
| 169 | + |
| 170 | +export default AsyncAbout; |
| 171 | +``` |
| 172 | +
|
| 173 | +`withAsyncReducers` is core function that we use here. It finds redux store from context and tries to register **top-level reducer** passed into it. |
| 174 | +
|
| 175 | +```jsx |
| 176 | + |
| 177 | +import {injectReducer} from './store'; |
| 178 | + |
| 179 | +//... |
| 180 | + |
| 181 | +componentWillMount() { |
| 182 | + this.attachReducers(); |
| 183 | +} |
| 184 | + |
| 185 | +attachReducers() { |
| 186 | + if (!reducer || !name) { return; } |
| 187 | + injectReducer(this.store, `${name}`, reducer, force); |
| 188 | +} |
| 189 | + |
| 190 | +``` |
| 191 | +
|
| 192 | +This may not be ideal for some scenarios and should be used with caution. Main risk is that some actions that happen before reducer is loaded and registered would be tracked. In case you need track of those you might look into more complex and robust solutions like [redux-persist](https://github.com/rt2zz/redux-persist) |
| 193 | +
|
| 194 | +`injectReducer` is a function that is responsible for |
| 195 | + - checking is async reducer was already injected into async registry |
| 196 | + - Creating new redux function and replacing state function with it. |
| 197 | + |
| 198 | + ###Caveats using async reducers### |
| 199 | + 1. **SSR.** we don't won't to loose initialState that was sent from server. Redux currently is checking that once we create store on client and will remove all state that does not have reducers yet. _And we don't have it since we have not loaded our components yet_. To fix that we use `dummyReducer` function that will be later replaced with real one. |
| 200 | + |
| 201 | +``` |
| 202 | +const initialReducers = createAsyncReducers({}, Object.keys(initialState)); |
| 203 | +// ... setting dummy |
| 204 | + persist.forEach(key => { |
| 205 | + if (!{}.hasOwnProperty.call(allReducers, key)) { |
| 206 | + allReducers[key] = dummyReducer; |
| 207 | + } |
| 208 | + }); |
| 209 | +//... replacing dummy |
| 210 | + if (!force && has(store.asyncReducers, name)) { |
| 211 | + const r = get(store.asyncReducers, name); |
| 212 | + if (r === dummyReducer) { return; } |
| 213 | + } |
| 214 | + |
| 215 | +``` |
| 216 | +2. All shared reducers should be registered outside of code split. See `core` folder and stuff. |
| 217 | + |
| 218 | +
|
| 219 | + |
0 commit comments