How to force update Single Page Application (SPA) pages?
Asked Answered
L

7

96

In fully server side based rendering (non Web 2.0), deploying server side code would directly update client side pages upon page reload. In contrast, in React based Single Page Application, even after React components were updated, there would be still some clients using old version of the components (they only get the new version upon browser reload, which should rarely happen) -> If the pages are fully SPA, it's possible that some clients only refresh the pages after a few hours.

What techniques should be employed to make sure the old components versions are not used anymore by any clients?

Update: the API doesn't changed, only React Component is updated with newer version.

Lillian answered 21/12, 2015 at 3:38 Comment(2)
Good question. I can think of couple of ways. Either using SSE(server sent events) or websockets to notify the client which has new update, hence they could update it when ready(to make sure they're not in the middle of something where automatic update may possibly make some disappointments).Smythe
leaving a socket open just for code updates is probably overkill, and not needed anyway, since a pure-client can run forever. It's server communication that would be at-risk. send a version stamp with each server request, and if your server gets something old, respond with an error message that will cause the page to reload (maybe after asking the user first). if you can support old and new at once, until old dries up, that's ideal...Jasik
A
86

You can have a React component make an ajax request to your server, when the application loads, to fetch "interface version". In the server API, you can maintain an incremental value for the client version. The React component can store this value on the client (cookie/local storage/etc). When it detects a change, it can invoke window.location.reload(true); which should force the browser to discard client cache and reload the SPA. Or better still, inform the end-user that a new version will be loaded and ask them if they wish to save the work and then reload etc. Depends on what you wanna do.

Awildaawkward answered 21/12, 2015 at 11:44 Comment(9)
@dotslash in short: you need to send the api version with the request. The server can compare and sends back there has been an update. If you're using redux, it will be easy to save the state (one reason why it's important that redux values are serializable) in web storage (developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API). reload page. check if anything is stored for redux. hydrate redux if so. and load your components with the necessary data (if you aren't using redux/react-redux you'll have to do this manually for a component's state)Compander
I don't understand why the update of the client app should be tied to a server request. Let's say clients are running v.42 of the SPA, and a v.43 just became available. This means clients will be forced to keep using v.42 until they make an HTTP request. But you can use a SPA for a long time without making such request... This SPA update should happen just after the v.43 availability IMHO.Vivianne
@DavidD. You'd need the client to initiate some communication with the server to know about the new version. It either comes to polling or push. Checkout Andy's response in this thread (https://mcmap.net/q/218194/-how-to-force-update-single-page-application-spa-pages) for tying a version check with every request. IMO a push approach just for keeping client version in sync would be an overkill.Awildaawkward
I prefer to serve manifest.json that is already packed with React application and use its description field to store version.Rolanda
Nowadays, reload with a boolean parameter is deprecated. Is a newer way to do so?Sclerosis
5 yeas later I still can't find a satisfied answer. I asked my question here with some new information softwareengineering.stackexchange.com/questions/423068/…Crider
Isn't reloading the page a Bad user experience ? Imagine, you are browsing a site or filling up a form on the site and it reloads in the middle of it.Roye
That's correct, you should add more checks corresponding to your specific use cases.Awildaawkward
!!!!warn!!!! window.location.reload(true) is depreciated. !!!!don't try this solution!!!Rettke
I
22

Similar to Steve Taylor's answer but instead of versioning API endpoints I would version the client app, in the following way.

With each HTTP request send a custom header, such as:

X-Client-Version: 1.0.0

The server would then be able to intercept such header and respond accordingly.

If the server is aware that the client's version is stale, for example if the current version is 1.1.0, respond with an HTTP status code that will be appropriately handled by the client, such as:

418 - I'm a Teapot

The client can then be programmed to react to such a response by refreshing the app with:

window.location.reload(true)

The underlying premise is that the server is aware of the latest client version.

EDIT:

A similar answer is given here.

Immensurable answered 2/12, 2017 at 13:11 Comment(1)
Good answer. I think it's better just to send the latest version from the server back to the client as a header, instead of using (abusing) HTTP status. Then client can decide whatever they want to do.Yarvis
B
9

What techniques should be employed to make sure the old components versions are not used anymore by any clients?

today (2018), many front apps use service workers. With it, it's possible to manage your app lifecycle by several means.

Here is a first example, by using a ui notification, asking your visitors to refresh webpage in order to get latest application version.

import * as SnackBar from 'node-snackbar';

// ....

// Service Worker
// https://github.com/GoogleChrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js

const offlineMsg = 'Vous êtes passé(e) en mode déconnecté.';
const onlineMsg = 'Vous êtes de nouveau connecté(e).';
const redundantMsg = 'SW : The installing service worker became redundant.';
const errorMsg = 'SW : Error during service worker registration : ';
const refreshMsg = 'Du nouveau contenu est disponible sur le site, vous pouvez y accéder en rafraichissant cette page.';
const availableMsg = 'SW : Content is now available offline.';
const close = 'Fermer';
const refresh = 'Rafraîchir';

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    function updateOnlineStatus() {
      SnackBar.show({
        text: navigator.onLine ? onlineMsg : offlineMsg,
        backgroundColor: '#000000',
        actionText: close,
      });
    }
    window.addEventListener('online', updateOnlineStatus);
    window.addEventListener('offline', updateOnlineStatus);
    navigator.serviceWorker.register('sw.js').then((reg) => {
      reg.onupdatefound = () => {
        const installingWorker = reg.installing;
        installingWorker.onstatechange = () => {
          switch (installingWorker.state) {
            case 'installed':
              if (navigator.serviceWorker.controller) {
                SnackBar.show({
                  text: refreshMsg,
                  backgroundColor: '#000000',
                  actionText: refresh,
                  onActionClick: () => { location.reload(); },
                });
              } else {
                console.info(availableMsg);
              }
              break;
            case 'redundant':
              console.info(redundantMsg);
              break;
            default:
              break;
          }
        };
      };
    }).catch((e) => {
      console.error(errorMsg, e);
    });
  });
}

// ....

There's also an elegant way to check for upgrades in background and then silently upgrade app when user clicks an internal link. This method is presented on zach.codes and discussed on this thread as well.

Biracial answered 11/9, 2018 at 12:3 Comment(0)
M
8

You can send app’s version with every response from any endpoint of your API. So that when the app makes any API request you can easily check there’s a new version and you need a hard reload. If the version in the API response is newer than the one stored in localStorage, set window.updateRequired = true. And you can have the following react component that wraps react-router's Link:

import React from 'react';
import { Link, browserHistory } from 'react-router';

const CustomLink = ({ to, onClick, ...otherProps }) => (
  <Link
    to={to}
    onClick={e => {
      e.preventDefault();
      if (window.updateRequired) return (window.location = to);
      return browserHistory.push(to);
    }}
    {...otherProps}
  />
);

export default CustomLink;

And use it instead of react-router's Link throughout the app. So whenever there's an update and the user navigates to another page, there will be a hard reload and the user will get the latest version of the app.

Also you can show a popup saying: "There's an update, click [here] to enable it." if you have only one page or your users navigate very rarely. Or just reload the app without asking. It depends on you app and users.

Mayman answered 10/8, 2017 at 20:2 Comment(0)
B
2

I know this is an old thread, and service workers are probably the best answer. But I have a simple approach that appears to work:

I added a meta tag to my "index.html" file :

<meta name="version" content="0.0.3"/>

I then have a very simple php scrip in the same folder as the index.html that responds to a simple REST request. The PHP script parses the server copy of the index.html file, extracts the version number and returns it. In my SPA code, every time a new page is rendered I make an ajax call to the PHP script, extract the version from the local meta tag and compare the two. If different I trigger an alert to the user.

PHP script:

<?php
include_once('simplehtmldom_1_9/simple_html_dom.php');
header("Content-Type:application/json");
/*
    blantly stolen from: https://shareurcodes.com/blog/creating%20a%20simple%20rest%20api%20in%20php
*/

if(!empty($_GET['name']))
{
    $name=$_GET['name'];
    $price = get_meta($name);

    if(empty($price))
    {
        response(200,"META Not Found",NULL);
    }
    else
    {
        response(200,"META Found",$price);
    }   
}
else
{
    response(400,"Invalid Request",NULL);
}

function response($status,$status_message,$data)
{
    header("HTTP/1.1 ".$status);

    $response['status']=$status;
    $response['status_message']=$status_message;
    $response['content']=$data;

    $json_response = json_encode($response);
    echo $json_response;
}

function get_meta($name)
{
    $html = file_get_html('index.html');
    foreach($html->find('meta') as $e){
        if ( $e->name == $name){
            return $e->content ;
        }
    }
}
Bubalo answered 5/8, 2019 at 13:32 Comment(1)
Tried this approach, worked perfectly! Thank youFraunhofer
H
0

Yes in server side rendering if you need to update a small part of the page also you need to reload whole page. But in SPAs you update your stuffs using ajax, hence no need to reload the page. Seeing your problem I have some assumptions:

You see one of your component got updated but other components getting data from same API didn't update. Here comes Flux Architecture. where you have your data in store and your component listen to store's changes, whenever data in your store changes all your components listening to it's change will be updated (no scene of caching).

Or

You need to control your component to be updated automatically. For that

  1. You can request your server for data in specific intervals
  2. Websockets can help you updating component data from server.
Harmonics answered 21/4, 2016 at 13:46 Comment(0)
A
0

Implemented the following solution using Axios, Redux and .Net Core REST API. enter image description here

During initial Axios configuration a custom header is added to the defaults. This results in all client GET requests including the client version number (defined in package.json) as a request header.

axios.defaults.headers.get["x-version"] = process.env.VERSION?.toString()??"0";

Within the REST API a middleware message handler intercepts all HTTP requests and checks if the request includes the x-version header. If so the x-header value is checked against a value defined in the appsettings.json. If these values do not match then the header x-client-incompatable is added to the response.

Middleware configuration in Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     ...
     app.UseMiddleware<VersionMessageHandler>();
     ...
 }

VersionMessageHandler.cs

public class VersionMessageHandler 
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;

    public VersionMessageHandler(RequestDelegate next, IConfiguration configuration)
    {
        _next = next;
        _configuration = configuration;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase)) 
        {
            var version = context.Request.Headers["x-version"].ToString();
            if (string.IsNullOrEmpty(version) == false)
            {
                context.Response.OnStarting(() =>
                {
                    var compatableClientVersion = _configuration.GetValue<string>("CompatableClientVersion");
                    if (string.IsNullOrEmpty(compatableClientVersion) == false && version != compatableClientVersion)
                    {
                        context.Response.Headers.Add("x-client-incompatable", "true");
                    }

                    return Task.FromResult(0);
                });
            }
        }

        await _next(context);
    }
}

When the response is received by the client an Axios interceptor (also defined as part of the initial Axios configuration) is used to check the response for the x-client-compatable header. If the header is found then a redux state update is dispatched.

Axios interceptor (Interceptors):

axios.interceptors.response.use(
    (response: AxiosResponse) => {

        // If we have the x-client-incompatable then the client is incompatable with the API.
        if(response.headers && response.headers["x-client-incompatable"]) {

            // Check if we already know before dispatching state update
            const appState: ApplicationState = store.getState();

            if (appState.compatability === undefined || appState.compatability.incompatable === false) {
                store.dispatch({ type: 'COMPATABILITY_CHECK', incompatable: response.headers["x-client-incompatable"] });
            }
        }

        return response;
    }
);

From here its just standard react functionality - the change in redux state causes a notification component to be displayed in the header of the app (user can still continue working). The notification has an onClick handler which calls window.location.reload() which results in the client being reloaded from the server.

Note. CORS may restrict response headers - if it does then you will need to configure CORS WithExposedHeaders in your API (startup.cs/ConfigureServices)

services.AddCors(options =>
            {
                options.AddPolicy("AllowedDomains",
                    policy => policy
                        .WithExposedHeaders("x-client-incompatable")
                );
            });
Alfonse answered 18/4, 2023 at 23:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.