Adaptive Web Applications with React and Redux

Jailany Mohamed
Zolo Engineering
Published in
8 min readNov 13, 2019
Photo by William Iven on Unsplash

What is an Adaptive Web Application?

An Adaptive web application, as opposed to a responsive web application use separate UI templates for different devices the user may use to access the application from. Adaptive web applications is not a new thing, I am sure you will remember the mobile domains (m.domain.com) to which the user will be redirected whenever he/she visited the site in a mobile device. Having a separate mobile site is also a form of an Adaptive Application, but it is not optimal as it comes along with its own problems like infrastructure, separate domain names etc.,

With an adaptive setup, you can render different template and content to different devices. The devices included can be Desktop, Mobile, Tablets, TV Devices and Bots.

Why do we need Adaptive Web Applications?

With the increased usage of mobile phones, it is essential for any transactional website to keep their design and UX mobile-first. Adaptive Development, lets you do that without compromising anything on the desktop counterpart.

Google has also started crawling and indexing web-pages using a mobile-first approach. This means that your website should be highly optimised for mobile with respect to content served and its speed. It is a lot of pain and work to get a higher page speed score for a responsive site which is not optimised for mobile. Adaptive Web Applications can help you tackle this problem as well.

The following are some of the advantages of AWA’s:

  1. Serve template and content as per the device of the user
  2. Provide flows and UX tailor-made for different devices
  3. Optimise your application by controlling what gets sent to the end-user device (HTML, Styles, Scripts, Assets, etc)

How was it done before SPAs?

Before the implementation of Single Page Applications, the content for the page was rendered on the server and sent to the browser. In the older implementations, the mobile version of the site was hosted under a different subdomain, usually a mobile domain. The web server will find the device type from the user-agent header of the request and redirect the user to the mobile domain. The problem here is having a separate domain and an application for different device types.

It is also possible to do this without a separate mobile domain. We can set up adaptive rendering with Express JS using Handlebars as a templating engine. In this setup, express finds the device type of the request and pick the template from different folders. Even though we can solve the different domain problem in this approach, the pages are still server-rendered individually.

How can it be done with client rendered SPAs?

In the client-side world, the app loads the shell from the server and the rest of the application is mounted once the script is executed. To load a template or component conditionally, we can either use the width of the device or evaluate the userAgent property in the navigator object. The former approach is much lesser fool-proof compared to the latter.

This setup goes for a toss when we implement server-side rendering for faster-perceived speed and SEO purposes. This is because the device detection has to happen in the server and has to be propagated into the UI components. We cannot employ the methods discussed above since the `window` and `navigator` objects are not available in the server.

How did we do it in a Server Rendered SPA?

At Zolo, We have a Server Rendered adaptive web application which is built with the help of React, Redux and couple of other libraries. The adaptive nature of our application helps us in providing separate UI for desktop and mobile users. We created our mobile UI to look and feel like a mobile app since ours is also an installable Progressive Web Application.

Desktop and Mobile Templates of a Property Details Page

Based on our implementation, we have created a starter kit for anyone who wishes to learn or implement Adaptive Web Design in their application.

Here is a rough outline of what we will be doing to implement Adaptive Web Design in a React-Redux application.

  1. Create an App Shell
  2. Create Page Components for Desktop and Mobile Devices
  3. Create an Express Server with feature to detect the device type
  4. Create a shared Redux store between the Client and the Server
  5. Set the device type in the store and pass it down to the browser
  6. Use the value in the store to conditionally render the components

Note: We will not be going through the details of the webpack and other configurations of the project.

We will be going through the above steps one by one with an example in the section below.

Project Structure

Our project will follow the folder structure as shown below.

Client and Server logic are kept in separate folders.

In the client folder, we have an App Shell file (App.jsx) which holds the routing and shell logic. The pages folder will have all the page templates of different devices identified by the file extension.

We have designed the app in such a way that the actions, reducers, and services for the devices are kept the same. We will see that structure further down the article.

Create an App Shell

First, we create an App shell which will be common for desktop and mobile templates. You can have different App Shells for different device types. We keep it the same for simplicity.

We add the following code in the App.jsx file for creating the App Shell.

App Shell (common for desktop and mobile)

In the App Shell, we use styled-components to create a header which will be common for desktop and mobile.

Create Page Components

After creating the App Shell, we create two files in the pages folder, `home.desktop.jsx` and `home.mobile.jsx` which are the page files of the Home page. We have the files segregated with the device name as the extension.

Let’s create a simple page component that displays a line of text along with the device type.

Desktop Component

Desktop Page Component (home.desktop.js)

Mobile Component

Mobile Page Component (home.mobile.js)

In the above files, we created two simple page components. One for Desktop and One for Mobile.

Create an Express Server

We use express as our web server which will receive the first request and send the HTML response back to the browser.

In our case, only the first request will be served by Express, all the other navigation will be handled on the client-side by React.

In the server folder, we create the file `index.js` with the following code.

Express Server (index.js)

Once we have the server up and running, we use express-device package to find the device type.

const device = require(‘express-device’);
app.use(device.capture());

Now if you try to print req.device in the console, you will see the device object containing the following data.

{ parser: 
DeviceParser {
options:
{ emptyUserAgentDeviceType: 'desktop',
unknownUserAgentDeviceType: 'phone',
botUserAgentDeviceType: 'bot',
carUserAgentDeviceType: 'car',
consoleUserAgentDeviceType: 'tv',
tvUserAgentDeviceType: 'tv',
parseUserAgent: false
},
make_sure_parser_was_executed: [Function],
get_model: [Function],
get_type: [Function]
},
type: 'desktop',
name: ''
}

As you can see, we can get the type of device from req.device

Create Redux Store

The next step is to create a redux store for storing the device type and other data as necessary.

We create a reducer file called core.reducer.js and set the initialState as mentioned below and export it for combining it with other reducers in the index reducer file.

const initialState = {
config: {
deviceType: ''
}
};

In the index reducer file, we import the core reducer and combine it using combineReducer.

Note: In the example, we have only one reducer to explain the concept.

Once the reducer is set up, we create the store using a function that will be shared by both the client and the server.

configureStore method (store.js)

In the client folder, we import the configureStore function in the index.js file to create the store and add it to the Provider. We do the same in the server part as well.

Client Store Creation

import { configureStore } from '../server/framework/store';
const initialState = {};
const store = configureStore(initialState);

Server Store Creation

import { configureStore } from './store';
const store = configureStore({});

Set the device type on the Server State and pass to Client

Once the store is created in the server, we extract that state using the getState() function.

let state = store.getState();

After that, we set the device type which we got from req.device.type in the state object.

state.core.config.deviceType = req.device.type;

Now, we have the device type stored in the state object which can be passed down to the client by setting it in the WINDOW object.

To do this, we take the HTML text which we got from reading the main.html file. We append the stringified state object to the end of the HTML and pass it on to the client as response.

const store = configureStore({});
const state = store.getState();
state.core.config.deviceType = req.device.type;
const htmlPath = path.resolve(__dirname, '../../build/main.html');
fs.readFile(htmlPath, 'utf8', (err, data) => {
const html = data.replace('<script></script>',
`<script>window.__PRELOADED_STATE__=${JSON.stringify(state)}
</script>`
)
res.send(html);
});

Conditionally render the components

Earlier, we set the device type in the state on the server-side and pass it to the client-side.

In the client-side, while creating the store, we pass the object we got from the server as the initial state to the configureStore function. This will create the store on the client-side with the same state object from the server.

import { configureStore } from '../server/framework/store';
const initialState = window.__PRELOADED_STATE__ ? window.__PRELOADED_STATE__ : {};
const store = configureStore(initialState);

Now that we have the device type in the store, next step would be to connect to the store from the App shell and use the value to conditionally render the pages/components.

To connect to redux store from a react component, we use the connect method.

Once we get the value from the state as a prop in the component, we can do the conditional rendering of components as mentioned below.

{
this.props.config.deviceType === ‘desktop’ ? <HomePageDesktop/> : <HomePageMobile/>
}

As you can see, the content of the page changes based on the device from which the site was accessed.

Upcoming Work

You might wonder why we have Redux here even though the usage of it has been very minimal in the example.

The above example can be implemented without the help of Redux. We can easily set the device type as a variable in the window object and use it in the components. This method will work fine as long as we are rendering only on the client. The same will be an issue if we are rendering on the server which can be solved by using Redux.

We will get into the implementation of the same in detail in the upcoming articles related to Server Side Rendering, Server Side Data loading. For now, the above method and the architecture help you create Adaptive Web Applications in React.

The codebase of the Adaptive Web Starter Kit is available here.

This article was originally posted here on the Official Blog of Zolostays.

--

--