This is a simple tutorial showing how to switch from redux-little-router to redux-first-router. The settings are pretty similar so the changes should be easy to understand.
For more information about comparison of the 2 router packages, check out my other post redux-first-router vs redux-little-router for React/Redux SPA
Get started
Goal: To use redux-first-router.
Base project code: Example code from React Routing with redux-little-router, ASP.NET Core SPA will be used as starting point. (Rendering a simple HelloWorld component)
Base project structure:
Npm packages
Add the following package inside “dependencies” in file package.json
1 2 3 4 |
"redux-first-router": "0.0.16-next", "redux-first-router-link": "1.4.2", "history": "4.7.2", "query-string": "6.1.0" |
Setup Routes
Replace routes.tsx in folder Scripts as follow:
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 |
import * as React from 'react'; import { HelloWorld } from './HelloWorld'; import { connect } from 'react-redux'; import Link from 'redux-first-router-link'; import { retrieveData } from './async-thunks'; // Mapping of route action type to component const Components = { 'HOME': <HelloWorld />, 'ABOUT': <div>About page</div>, 'CONTACT': <div>Contact page</div> }; // This component shows the correct page based on the route action type defined at state.location.type class Switcher extends React.PureComponent<any, any> { render() { return Components[this.props.page] || <div>Something wrong</div>; } } var Page = connect((state: any) => ({ page: state.location.type }))(Switcher); export const App = () => ( <div> <div><Link to={{ type: 'HOME' }}>Home</Link> <Link to={{ type: 'ABOUT' }}>About</Link> <Link to={{ type: 'CONTACT' }}>Contact</Link></div> <hr /> <Page /> </div> ); // route action types map to URLs export var routes = { HOME: { path: '/', thunk: retrieveData() // optional thunk }, ABOUT: '/support/About', CONTACT: '/support/Contact' }; |
routes defines all the routes with name (redux action type) maps to their URL. It is used by redux-first-router to perform the routing.
App is a React functional component that defines the SPA.
Page is a connected React component provided to display the corresponding page based on the route name defined at state.location.type.
Link is another React component that lets you change route through hyperlink.
Update HelloWorld component
Simply removed the componentWillMount() method of HelloWorld component.
Data fetching for HelloWorld component ( retrieveData()) is declared as an optional thunk for the HOME route. Therefore, when HOME page is shown, retrieveData() is automatically called at client side. As a result, componentWillMount() is no longer needed.
Boilerplate for client side rendering
Replace the entry point entry.tsx as follow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import * as React from 'react'; import { hydrate } from 'react-dom'; import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import reduxThunk from 'redux-thunk'; import { helloWorld_reducers } from './redux-reducers'; import { routes, App } from './routes'; import { connectRoutes } from 'redux-first-router'; import createHistory from 'history/createBrowserHistory'; import * as queryString from 'query-string'; const { reducer, middleware, enhancer } = connectRoutes(createHistory(), routes, { querySerializer: queryString }); var Store = createStore( combineReducers({ location: reducer, 'helloWorld': helloWorld_reducers }), (window as any).ReduxInitialState, compose(enhancer, applyMiddleware(middleware, reduxThunk)) ); hydrate(<Provider store={Store}><App /></Provider>, document.querySelector('#content')); |
By calling connectRoutes() from redux-first-router, we get a reducer, middleware and enhancer for use to configure the Redux store.
Boilerplate for server side rendering
Replace the entry point entry-server.tsx as follow:
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 * as React from 'react'; import * as prerendering from 'aspnet-prerendering'; import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { Provider } from 'react-redux'; import reduxThunk from 'redux-thunk'; import { helloWorld_reducers } from './redux-reducers'; import { renderToString } from 'react-dom/server'; import createHistory from 'history/createMemoryHistory'; import { connectRoutes } from 'redux-first-router'; import { routes, App } from './routes'; import { addTask } from 'domain-task'; import * as queryString from 'query-string'; export var SR = prerendering.createServerRenderer((params: prerendering.BootFuncParams): Promise<prerendering.RenderToStringResult> => { return new Promise((resolve, reject) => { var request = params.location; var path = request.pathname + (request.query ? '?' + queryString.stringify(request.query) : ''); const { reducer, middleware, enhancer, thunk } = connectRoutes(createHistory({ initialEntries: [ path ]}), routes, { querySerializer: queryString }); var Store = createStore( combineReducers({ location: reducer, 'helloWorld': helloWorld_reducers }), compose(enhancer, applyMiddleware(middleware, reduxThunk)) ); addTask(thunk(Store)); var app = <Provider store={Store}><App /></Provider>; // any async task (has a Promise) should call addTask() to add to domainTasks. // only do the actual rendering when all async tasks are done. params.domainTasks.then(() => { resolve({ html: renderToString(app), globals: { ReduxInitialState: Store.getState() } }); }, reject); // Also propagate any errors back into the host application }); }); |
The change is similar to that of client side except history is created using createMemoryHistory() instead. Note that connectRoutes() also returns a thunk (the optional thunk associated with the route, if any). At server side, we need to call it manually by passing the redux Store as an argument. addTask() and params.domainTasks are used to ensure any async operations (data fetching) are completed before calling renderToString() to render the page.
Note that in this example, the data fetching for HelloWorld inside component is no longer needed. As a result, renderToString() is only called once.
Example code is available at Github.