Adding PWA Components to Legacy Pages

When porting an existing site to a PWA, you may decide to deliver only a subset of the site's overall functionality as a PWA so you can launch sooner. React Storefront's proxyUpstream route handler allows you to reuse content from the existing site and injecting certain components from the PWA, such as the header, main menu, and footer, to provide a uniform experience across the entire site.

Example

The sections that follow reference code from React Storefront Boilerplate, which is a "hybrid" PWA that contains both PWA and traditional pages.

Creating a Shared Header Component

React Storefront Boilerplate uses the Header component in both PWA and legacy pages. There isn't anything special you need to do to a component to share it in both parts of the site. It's just a generic React component.

Injecting the PWA's Header into Legacy Pages

In React Storefront Boilerplate, when a URL doesn't match a route, content is proxied in from the upstream site. This is configured in src/routes.js:

// src/routes.js

new Router()
  
  // ... other routes here
  
  .fallback(
    // when no route matches, pull in content from the upstream site
    proxyUpstream('./proxy/proxy-handler')
  )

Here is the handler for all proxied pages:

// src/proxy/proxy-handler.js

import renderHeader from './renderHeader'
import getStats from 'react-storefront-stats'

export default async function proxyHandler(params, request, response) {
  try {
    const stats = await getStats()
    fns.init$(body)
    renderHeader(stats) // reuse the PWA header in legacy pages
    response.send($.html())
  } catch (e) {
    response.send(e.stack)
  }
}

The renderHeader function injects the PWA's header into proxied pages. It uses the render function from react-storefront/renderers to generate HTML for the Header component.

// src/proxy/renderHeader.js

/**
 * Inserts the PWA header into proxy pages.
 * @param {Object} stats Webpack build stats object for the client build
 */
export default function renderHeader(stats) {
  const { html } = render({
    component: <Header/>,   // the same Header component used by the PWA
    state: createState(),   // an instance of AppModel
    theme,                  // the PWA theme
    stats,                  // from react-storefront-stats/getStats()
    clientChunk: 'header',  // the name of the entry injected into config/web.dev.*.js
  })

  // remove the existing header
  $body.find('header').remove()

  // add the new header and supporting resources to the document
  const $header = $(tag('div', { class: 'mw-header' })).append(html)
  $body.find('#page-container').attr('id', null).prepend($header)
}

Hydrating on the Client

In order for the Header to be interactive (for example, to display the slide-in app menu when the user taps the menu button), we need to hydrate it once the page loads in the browser. This is done in src/proxy/hydrateHeader.js:

// src/proxy/hydrateHeader.js

/*
This is the webpack client entry point for the header chunk that we'll inject into legacy pages.
See config/webpack/webpack.*.client.js
*/

import React from 'react'
import { hydrate } from 'react-storefront/renderers'
import Header from '../header/Header'
import model from '../AppModel'
import theme from '../theme'

hydrate({
  component: <Header/>,
  model,
  theme,
  target: document.querySelector('.mw-header')
})

Webpack Configuration

In order to hydrate the shared header on the client, we need to build a bundle corresponding to the clientChunk option passed into the render function of react-storefront/renderers. See "Injecting the PWA's Header into Legacy Pages" above.

// config/webpack.dev.client.js

const { dev } = require('react-storefront/webpack/client')
const path = require('path')

module.exports = dev(path.join(__dirname, '..', '..'), {
  workboxConfig: require('./workbox.config'),
  entries: {
    header: './proxy/hydrateHeader' // add the clientChunk entry referenced in renderHeader.js here
  }
})

Remember to add the entry for the shared header to both webpack.dev.client.js and webpack.prod.client.js.