React Routing with redux-little-router is part 5 of a 5 parts React with ASP.NET Core series. This series will show you the setup necessary for building React single page application with routing and server-side rendering.
For traditional web site that server is responsible to render all web pages, each page change requires browser to request and re-run the page from the server. That means the current state/data of the page is lost and need to re-establish when the new page loads.
Single Page Application (SPA) shifts the routing job to the browser side. Browser can pre-fetch pages with the initial page load, or could load the required page on demand later. The main difference is the current state/data of the page is never lost. Although it’s still the same page, the javascript framework already knows how to assemble different pages and present them to the user within the same memory space. This results in better performance and is able to achieve app like experience.
So we need a routing module. One of the most popular routers available for React is react-router. It has a lot of bells and whistles. But for my project, I only need a simple and slim routing module that works well with redux. There are many other simple and basic routers and the alternative I ended up using is redux-little-router. Serve my needs and small in size.
React Routing with redux-little-router
Goal: To enable react routing with redux-little-router.
Base project code: Example code from React with Server Side Rendering, ASP.NET Core serves as starting point. (Rendering a simple HelloWorld component)
Base project structure:
Npm packages
Add the following package inside “dependencies” in file package.json
1 |
"redux-little-router": "15.1.1" |
Setup Routes
Let’s add a couple more pages and define routes for each page. Create 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 |
import * as React from 'react'; import { Fragment, Link } from 'redux-little-router'; import { HelloWorld } from './HelloWorld'; export const routes = { '/': {}, '/support/About': {}, '/support/Contact': {} } export const App = () => ( <div> <div><Link href='/'>Home</Link> <Link href='/support/About'>About</Link> <Link href='/support/Contact'>Contact</Link></div> <hr /> <Fragment forRoute='/' withConditions={location => location.pathname === '/'}><HelloWorld /></Fragment> <Fragment forRoute='/support/About'><div>About page</div></Fragment> <Fragment forRoute='/support/Contact'><div>Contact page</div></Fragment> </div> ); |
routes defines all the routes with their URL maps to a custom value object (which becomes available through Redux state.router.result for the route you are at) and redux-little-router uses this information to perform routing.
We also added two more pages: simple pages that just display ‘About page’ and ‘Contact page’.
App is a React functional component that defines the SPA.
redux-little-router provides a react component Fragment to serve like a switch statement and only Fragment with a matching route will display its content.
Link is another React component that lets you change route through hyperlink. Or if you like to change routes through javascript, you can use the provided action creators push, replace, go, goBack, goForward.
Add the 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 |
import * as React from 'react'; import { hydrate } from 'react-dom'; import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { helloWorld_reducers } from './redux-reducers'; import { routerForBrowser } from 'redux-little-router'; import { routes, App } from './routes'; const { reducer, middleware, enhancer } = routerForBrowser({ routes }); var Store = createStore( combineReducers({ router: reducer, 'helloWorld': helloWorld_reducers }), (window as any).ReduxInitialState, compose(enhancer, applyMiddleware(middleware, thunk)) ); hydrate(<Provider store={Store}><App /></Provider>, document.querySelector('#content')); |
routerForBrowser from redux-little-router will return an object that contains its reducer, middleware and enhancer for use in creating the redux store.
combineReducers() is a function to group multiple reducers and merge them into a single reducing function. Similarly, compose() is a function to group multiple enhancers/middleware.
Add the 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 41 42 43 |
import * as React from 'react'; import * as prerendering from 'aspnet-prerendering'; import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { helloWorld_reducers } from './redux-reducers'; import { renderToString } from 'react-dom/server'; import { routerForHapi } from 'redux-little-router'; import { routes, App } from './routes'; export var SR = prerendering.createServerRenderer((params: prerendering.BootFuncParams): Promise<prerendering.RenderToStringResult> => { return new Promise((resolve, reject) => { var request = params.location; const { reducer, middleware, enhancer } = routerForHapi( { routes, request: { // pass in adjusted request from javascriptServices's params.location to work using routerForHapi path: request.pathname, url: null, query: request.query } }); var Store = createStore(combineReducers({ router: reducer, 'helloWorld': helloWorld_reducers }), compose(enhancer, applyMiddleware(middleware, thunk))); var app = <Provider store={Store}><App /></Provider>; renderToString(app); // This kick off any async tasks started by React components // 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 that routerForHapi is used instead of routerForBrowser (see the highlighted lines).
Update HelloWorld component
Since we are now using multiple reducers, we shall access HelloWorld component’s redux data using stat['helloWorld'] instead.
Change connect() from
1 |
export var HelloWorld: React.ComponentClass<{}> = connect(state => { return { ...state }; })(Comp); |
to
1 |
export var HelloWorld: React.ComponentClass<{}> = connect(state => { return { ...state['helloWorld'] }; })(Comp); |
ASP.NET Core SPA Fallback
At this point, you can build and run and the SPA shall run as expected. Clicking on the hyperlinks will switch to the corresponding pages without need to go back to the server.
However, if you enter /support/About or /support/Contact at the address bar, or reload the page while at them, you will receive a HTTP 404 error. That is because ASP.NET controller is not defined to handle these URLs and we are not suppose to. Instead, we tell ASP.NET Core to load the SPA page when these URLs are requested by using MapSpaFallbackRoute().
Replace app.UseMvc() method call at Startup.cs with the following:
1 2 3 4 5 6 7 8 9 |
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute(name: "spa", templatePrefix: "support", defaults: new { controller = "Home", action = "Index" }); }); |
This is a fallback when ASP.NET MVC can’t find a match and the path starts with /support, load the page /Home/Index instead.
Now, reloading any page will have the page fires up correctly.
Thanks for reaching here, especially if you have followed through this React with ASP.NET Core series. You now have the basic setup for SPA using React/Redux/ASP.NET Core with Typescript, SSR and client side routing using redux-little-router. This series should give you a good understanding of how these many different modules and libraries work together to build a SPA.
Example code for Part 5: React Routing with redux-little-router is available at Github.
More Read
React with ASP.NET Core series – Part 4: React with Server Side Rendering, Get started with ASP.NET Core
Enable css route page transition for redux-little-router: Route transition for redux-little-router, react ASP.NET Core