Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document how to detect SW updates and alert users #9087

Closed
Knaackee opened this issue Oct 13, 2018 · 29 comments
Closed

Document how to detect SW updates and alert users #9087

Knaackee opened this issue Oct 13, 2018 · 29 comments
Assignees
Labels
type: question or discussion Issue discussing or asking a question about Gatsby

Comments

@Knaackee
Copy link

Knaackee commented Oct 13, 2018

I am using the gatsby-plugin-offline plugin and would like to know if there is a way to handle updates.

I would like to display a message if a new version is available like
https://medium.com/progressive-web-apps/pwa-create-a-new-update-available-notification-using-service-workers-18be9168d717

At the moment I need to hard reload the site in order to see changes.

How to handle this?

Thanks in advance!

@Knaackee Knaackee changed the title gatsby-plugin-offline: new update available gatsby-plugin-offline: how to handle updates? Oct 13, 2018
@smakosh
Copy link
Contributor

smakosh commented Oct 15, 2018

Same here

@pieh pieh added the type: question or discussion Issue discussing or asking a question about Gatsby label Oct 15, 2018
@pieh
Copy link
Contributor

pieh commented Oct 15, 2018

Right now workflow is like that - when there is SW update, gatsby will just set flag to reload page on next navigation (so it doesn't refresh itself which would cause weird UX)

You can hook into onServiceWorkerUpdateFound browser API hook in gatsby-browser.js if you want to display custom message, so users are aware of it (or even force refresh if you would want that)

@rwieruch
Copy link
Contributor

@pieh do I understand correctly, that with every new gatsby build the service worker gets the update and I don't need to worry about my users getting an old version of the webiste? I couldn't find anything about it in the gatsby-offline-plugin docs.

@pojntfx
Copy link

pojntfx commented Oct 28, 2018

@rwieruch Same problem here. On libresat.space (sources here), the old content still gets served (deployed using Netlify). Some docs would be awesome.

@pojntfx
Copy link

pojntfx commented Dec 8, 2018

Well, found it out. It's very simple actually!
Just add the following to gatsby-browser.js in the root of the repository (no further config necessary):

// gatsby-browser.js
exports.onServiceWorkerUpdateFound = () => {
  if (
    window.confirm(
      "This site has been updated with new data. Do you wish to reload the site to get the new data?"
    )
  ) {
    window.location.reload(true);
  }
};

This shows a little modal that reloads the site if the user pressed "Yes". See learn-chinese.tk (sources here) for a demo.

@smakosh
Copy link
Contributor

smakosh commented Dec 8, 2018

bad UX in my opinion, the user will keep seeing modals every time a new content is available

@rwieruch
Copy link
Contributor

rwieruch commented Dec 8, 2018

I think I will just disable the offline plugin before launching a new website/application. Heard about too many launches that went wrong or website still serving old content due to service workers. Not necessarily related to Gatsby, but maybe just avoiding service workers for now is a better option.

@pojntfx
Copy link

pojntfx commented Dec 9, 2018

Well, another option would be to simply reload the page without any confirm:

// gatsby-browser.js
exports.onServiceWorkerUpdateFound = () => window.location.reload(true);

I prefer the modal though. I've seen this a lot on other sites as well - on a slow mobile connection you don't want to reload the whole page, after all that's what serviceworkers are for!

@vtenfys vtenfys self-assigned this Dec 9, 2018
@vtenfys vtenfys changed the title gatsby-plugin-offline: how to handle updates? Document how to detect SW updates and alert users Dec 9, 2018
DSchau pushed a commit that referenced this issue Dec 11, 2018
Closes #9087

I also deleted `/docs/add-offline-support/` since it just redirects to `/docs/add-offline-support-with-a-service-worker/` anyway, and hasn't been updated in a while.
@laradevitt
Copy link
Contributor

The discussion in this thread has been helpful - thanks!

Using the method in the previous comment, I'm seeing the following behaviour after service worker update:

  1. Manual refresh; content not updated
  2. An automatic refresh is triggered; content still not updated
  3. Manual refresh again; content is updated

I would expect the content to be updated in step 2. Am I missing something?

My code (from @pojntfx's comment above):

// gatsby-browser.js
exports.onServiceWorkerUpdateFound = () => window.location.reload(true);

I would actually prefer the user alert method, but this turned out to be problematic because of #10432 (comment).

@vtenfys
Copy link
Contributor

vtenfys commented Jan 9, 2019

Hi @laradevitt, I believe the problems you've described should be fixed in #10432 - hopefully this should be merged soon so please could you have another look once this is ready? Thanks!

@laradevitt
Copy link
Contributor

@davidbailey00 - Thanks! Yes, I mentioned that PR above. Was looking for a quick fix in the meantime but I guess I should just be more patient. :) If there is anything I can do to help test, let me know.

gpetrioli pushed a commit to gpetrioli/gatsby that referenced this issue Jan 22, 2019
…#10417)

Closes gatsbyjs#9087

I also deleted `/docs/add-offline-support/` since it just redirects to `/docs/add-offline-support-with-a-service-worker/` anyway, and hasn't been updated in a while.
@laradevitt
Copy link
Contributor

Hey, @davidbailey00 - Noting that #10432 had been merged I updated all packages and the problem as described in my previous comment no longer exists.

I'll note here, in case somebody else stumbles upon this issue, that this:

exports.onServiceWorkerUpdateFound = () => window.location.reload(true);

should now be:

exports.onServiceWorkerUpdateReady= () => window.location.reload(true);

Works a charm. Thx. :)

@diegodorado
Copy link

Hey, @davidbailey00 - Noting that #10432 had been merged I updated all packages and the problem as described in my previous comment no longer exists.

I'll note here, in case somebody else stumbles upon this issue, that this:

exports.onServiceWorkerUpdateFound = () => window.location.reload(true);

should now be:

exports.onServiceWorkerUpdateReady= () => window.location.reload(true);

Works a charm. Thx. :)

I dont get it... do I need both handlers?

@laradevitt
Copy link
Contributor

@diegodorado No, the second replaces the first.

@codingimages
Copy link

@rwieruch Same problem here. On libresat.space (sources here), the old content still gets served (deployed using Netlify). Some docs would be awesome.

That is the same issue I am having, cache is not updating new content, annoying as hell.

@cxspxr
Copy link
Contributor

cxspxr commented Aug 26, 2020

it's interesting since when I do npm run build it shows for any of the solutions above

ERROR #95312 
"window" is not available during server side rendering

@laradevitt
Copy link
Contributor

@cxspxr - AFAIK code in gatsby-browser.js shouldn't be applied during build time. Is that where you're putting it?

@cxspxr
Copy link
Contributor

cxspxr commented Aug 26, 2020

@laradevitt yes. It's strange but problem resolved on freshly cloned project. No tracked changes in old folder

@canadianeagle
Copy link

A good habit that saves me a lot of headache on Gatsby projects is to always use typeof window !== "undefined" && window instead of just window

@nibtime
Copy link
Contributor

nibtime commented Nov 6, 2020

I frequently observed troubles with gatsby-plugin-offline after site updates (white pages with errors or stale content (some parts missing or mixed up). A force reload solves those problems, so I wanted to do it regardless of any user input, but at the same time, I wanted to have some feedback mechanism in place to inform the user about the site update.

I used the Notification API so that it is opt-in and non-invasive for the user (unlike alert and confirm, which are modal).

looks like this on Windows 10

2020-11-06 21_40_45

The implementation looks as follows, in case anyone is interested:

import { ServiceWorkerArgs } from "gatsby"

export const onServiceWorkerUpdateReady = async (args: ServiceWorkerArgs) => {
  const permissionResponse = await Notification.requestPermission()
  if (permissionResponse === "granted") {
    await args.serviceWorker.showNotification("Website update", {
      body:
        "Our website just got a little bit better. We reloaded the site with the update to ensure a smooth experience for you."
    })
  }
  window.location.reload(true)
}

You can find information about adapting the behavior of notifications here. In case you wonder how to use TypeScript for Gatsby APIs, the most comprehensive real-world example I know is this website.

@pwnts
Copy link

pwnts commented Nov 9, 2020

I have the same issue. I need to perform a hard reload on the page to get new content.

I've tried adding this to gatsby-browser.js:
export const onServiceWorkerUpdateReady = () => window.location.reload();
...but with no success. Still need to perform a hard reload for new content to get displayed. What to do? I do want the PWA with offline functionality but not with the cost of the site not working when online.

@nibtime
Copy link
Contributor

nibtime commented Nov 10, 2020

@pwnts

in window.location.reload(true) the true is important. It stands for force refresh. If I omit it, things don't work on my side either.

@ali4heydari
Copy link

@nibtime
location.reload(true) is deprecated. See this stackoverflow question.

@nibtime
Copy link
Contributor

nibtime commented Nov 13, 2020

@ali4heydari
thank you for the info, I did not know that it is deprecated.

However, I think that browsers do not perform the necessary Shift + F5 refresh if the forcedReload flag is omitted and just do an ordinary F5 reload instead. A F5 reload does not solve the white page errors of gatsby-plugin-offline.

When calling window.location.reload() without any parameter, there can only be one kind of behavior, so it is either F5 or Shift + F5. The decision to ignore (not just deprecate) the forceReload flag in the future would effectively rule out the possibility to programmatically trigger a Shift + F5 refresh in an easy and convenient way, assuming browsers would consistently perform F5 behavior on parameterless window.location.reload(). Inconsistent behavior, i.e. every browser comes with its very own interpretation of window.location.reload() would be a nightmare and actually make the function itself unusable.

Removing the forcedReload feature would be too bad, since those kinds of refreshes are a very reliable "last resort" cleanup mechanism, not just for gatsby-plugin-offline. Browsers might then offer a new explicit function like window.forceReload() instead, but no such thing exists at the moment. Therefore I can't really understand the reasons for deprecating the forceReload flag at this point.

This problem is mentioned in an answer to the same stackoverflow question you posted. It suggests an alternative solution involving a POST request. I am not sure if I like this solution and not sure if this is feasible in a good way with onServiceWorkerUpdateReady and gatsby-plugin-offline.

Therefore, I keep using the forcedReload flag since it's better to be safe than sorry. The force-reload is a hack in the first place to fix problems of gatsby-plugin-offline that are seriously detrimental to the user experience. Eventually, gatsby-plugin-offline should solve them so that such hacks are no longer necessary.

@majg0
Copy link

majg0 commented Feb 4, 2021

@nibtime I agree, but I don't fully understand the last part: is this issue specific to gatsby-plugin-offline? What is it doing that it shouldn't (or vice versa)?

@sandren
Copy link

sandren commented Feb 7, 2021

What is it doing that it shouldn't (or vice versa)?

@KyleAMathews @wardpeet

Problem

The problem with gatsby-plugin-offline is that it is fundamentally broken by default.

Using it almost guarantees that repeat visitors will get "stuck" on old builds of the site and won't be able to get the latest version without the help of someone walking them through developer tools to manually clear out cache and remove registered service workers.

I've personally had to replace gatsby-plugin-offline with gatsby-plugin-remove-serviceworker on every single website I've ever deployed gatsby-plugin-offline on after repeated complaints of visitors getting stuck on older versions of the site, sometimes versions as old as six months. At this point the website owners rightfully decide that the slight benefit of instantly serving everyone a cached version of the website is not worth many users being stuck on an old version of the site indefinitely.

Workarounds

Currently the only way for developers to resolve this issue is to add hacks in gatsby-browser.js to implement very basic functionality—like automatically updating the website to the latest version on navigation events—which I suspect most developers mistakenly expect is the default behavior.

I see this hack in gatsby-browser.js used a lot (sometimes with success) to force a refresh to the latest version:

export const onRouteUpdate = () => {
  navigator.serviceWorker.register('/sw.js').then((reg) => {
    reg.update();
  });
};

export const onServiceWorkerUpdateReady = () => {
  window.location.reload(true)
};

Others use a combination of a state update in gatsby-browser.js with a custom modal/prompt asking the user to basically do the same thing via clicking a button:

export const onRouteUpdate = () => {
  navigator.serviceWorker.register('/sw.js').then((reg) => {
    reg.update();
  });
};

export const onServiceWorkerUpdateReady = () => {
  document.getElementById('___gatsby').setAttribute('data-update-available', 'true');
  console.info('PWA update available.');
};
export const UpdateButton = () => {
  const [isUpdateAvailable, setIsUpdateAvailable] = useState(false)
  
  useEffect(() => {
    const interval = setInterval(() => {
      const isUpdateAvailable =
        document.getElementById('___gatsby').dataset.updateAvailable === 'true';
  
      if (isUpdateAvailable) {
        setIsUpdateAvailable(true);
      }
    }, 1000);
  
    return () => {
      clearInterval(interval);
    };
  }, []);
  
  return isUpdateAvailable ? <button onClick={() => window.location.reload(true)}>An update is available. Click to update the website.</button> : null
}

Conclusion

Everyone that uses gatsby-plugin-offline, but does not implement one of these hacks is effectively serving a fundamentally broken website to a large number of users, which is precisely the problem: none of these workarounds should be necessary in the first place; they should be options provided by the plugin itself.

@nibtime
Copy link
Contributor

nibtime commented Feb 11, 2021

@martingronlund
originally, I got to this issue by chance and then shared a solution to inform users about SW updates via the Browser Notification API (#9087 (comment)). My specific use case included the force-reload hack so that gatsby-plugin-offline doesn't mess up everything. Then somebody told me the forceReload flag of window.location.reload is deprecated and then I explained why it can't be omitted in this case even though it's deprecated and then it was suddenly all about gatsby-plugin-offline 😄

@sandren
great writeup! I agree with every single thing. I am currently getting into writing some custom Gatsby themes, maybe I will write one for that. It should have sensible default options for the most critical fixes (everything you listed is in it) and the theme should also provide a low-friction way to interact with SW state and functionality from the Gatsby Browser API in React code. zustand looks highly appropriate to me for implementing this.

@sandren
Copy link

sandren commented Feb 11, 2021

@sandren
great writeup! I agree with every single thing. I am currently getting into writing some custom Gatsby themes, maybe I will write one for that. It should have sensible default options for the most critical fixes (everything you listed is in it) and the theme should also provide a low-friction way to interact with SW state and functionality from the Gatsby Browser API in React code. zustand looks highly appropriate to me for implementing this.

@nibtime Thanks! I agree that Zustand seems like a great choice for implementing this. I look forward to seeing your solutions! Is there a repository I can follow? 😀

@nibtime
Copy link
Contributor

nibtime commented Feb 18, 2021

@nibtime Thanks! I agree that Zustand seems like a great choice for implementing this. I look forward to seeing your solutions! Is there a repository I can follow? 😀

@sandren Thanks for your interest! Not yet, I am still experimenting with Gatsby Themes in a private repo, and also another theme has currently a higher priority for me (to add a Next.JS getStaticProps-ish per-page level abstraction for data fetching and proper content i18n to Gatsby). I will eventually create a public monorepo with a single semantic release process for all my Gatsby Themes and other libraries I make. I am using TypeScript all the way and rely heavily on my fp-ts, io-ts, and ramda street knowledge for any kind of design and implementation. If you are interested in those kinds of things, you can follow me on GitHub. 😃

Gatsby with zustand PoC

The last two days I implemented a PoC for using Gatsby Browser with zustand. If those things go into themes, I think it would be best to create a theme gatsby-theme-zustand that exposes state changes triggered by Gatsby Browser through a zustand store to enable you to compose any patterns of effect or state involving Gatsby Browser with React hooks. Then you would build a theme gatsby-plugin-offline-fixes (that actually shouldn't exist) which implements React hooks with fixes on top of gatsby-theme-zustand and includes them in wrapRootElement. I don't really know the internals of gatsby-plugin-offline and all the fixes that would be necessary to be able to use it with true confidence on production websites. All this is meant as a blueprint for implementing and composing such fixes and to establish a better way for dealing with Gatsby Browser in general.

A zustand store for Gatsby Browser

This also makes Gatsby Browser easily observable in Redux Devtools

zustand_gatsby_devtools

Define the store

import create, { StateSelector } from "zustand"
import { devtools } from "zustand/middleware"
import type { RouteUpdateArgs, RouteUpdateDelayedArgs } from "gatsby"
import { pipe } from "ramda"

type GatsbyBrowserApiState = {
  hasSwUpdateReady?: boolean
  routeUpdate?: RouteUpdateArgs
  routeUpdateDelayed?: RouteUpdateDelayedArgs
}

const withDevtools = pipe(devtools, create)

export const useGatsbyBrowserApiStore = withDevtools<GatsbyBrowserApiState>(() => ({}))

export type Selector<U> = StateSelector<GatsbyBrowserApiState, U>

const hasSwUpdateReadySelector: Selector<boolean | undefined> = (store) => store.hasSwUpdateReady

export const useHasSwUpdateReady = () => useGatsbyBrowserApiStore(hasSwUpdateReadySelector)

Wire the store with Gatsby Browser

import type { GatsbyBrowser } from "gatsby"
import { useGatsbyBrowserApiStore } from "#gatsby/browser-api/zustand"

export const onServiceWorkerUpdateReady: Required<GatsbyBrowser>["onServiceWorkerUpdateReady"] = () => {
  useGatsbyBrowserApiStore.setState({
    hasSwUpdateReady: true
  })
}

export const onRouteUpdate: Required<GatsbyBrowser>["onRouteUpdate"] = (args) => {
  useGatsbyBrowserApiStore.setState({
    routeUpdate: args
  })
}

export const onRouteUpdateDelayed: Required<GatsbyBrowser>["onRouteUpdateDelayed"] = (args) => {
  useGatsbyBrowserApiStore.setState({
    routeUpdateDelayed: args
  })
}

Implement an offline-plugin fix as a React hook (update SW on route updates)

import type { GatsbyBrowser } from "gatsby"
import { useAsync } from "react-use"
import { useGatsbyBrowserApiStore, Selector } from "#gatsby/browser-api/zustand"

const routeUpdatePathSelector: Selector<string | undefined> = (store) => store.routeUpdate?.location.pathname

export const useUpdateSwOnRouteUpdates = () => {
  const routeUpdatePath = useGatsbyBrowserApiStore(routeUpdatePathSelector)

  useAsync(async () => {
    console.info("sw.update() for routeUpdate from Gatsby Browser", { routeUpdatePath })
    const sw = await navigator.serviceWorker.register("sw.js")
    sw.update()
  }, [routeUpdatePath])
}

const RootElementWrapper: React.FC = ({ children }) => {
  useUpdateSwOnRouteUpdates()
  return <>{children}</>
}

export const wrapRootElement: Required<GatsbyBrowser>["wrapRootElement"] = ({ element }) => {
  return <RootElementWrapper>{element as React.ReactNode}</RootElementWrapper>
}

Write an update alert component like it's 2021

I think it is also necessary to provide a concrete example to show why this is worth it. With this approach, you can easily compose a nice functional React component for service worker updates (or involving anything from Gatsby Browser in general) without any document hacks and with an arbitrary amount of fanciness. Then place it anywhere you like in your layouts. The following example uses Tailwind with twin.macro and the Transition component of @headlessui/react.

This will show when an SW update is available

sw_fancyalert

import "twin.macro"
import React from "react"
import { Transition } from "@headlessui/react"
import { useTimeout } from "react-use"
import { useHasSwUpdateReady } from "#gatsby/browser-api/zustand"

const ExclamationIcon = () => (
  <svg
    tw="h-6 w-6 text-secondary-600"
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
    aria-hidden="true"
  >
    <path
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="2"
      d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
    />
  </svg>
)

export const SwUpdateAlert = () => {
  const hasUpdateReady = useHasSwUpdateReady()
  const [timeoutElapsed] = useTimeout(5000)
  const show = hasUpdateReady && !timeoutElapsed()
  return (
    <Transition
      show={show ?? false}
      enter="ease-out duration-300"
      enterFrom="opacity-0"
      enterTo="opacity-100"
      leave="ease-in duration-200"
      leaveFrom="opacity-100"
      leaveTo="opacity-0"
    >
      <div tw="bg-secondary-100 px-4 pt-5 pb-4 sm:(p-6 pb-4)">
        <div tw="sm:(flex items-start)">
          <div tw="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:(mx-0 h-10 w-10)">
            <ExclamationIcon />
          </div>
          <div tw="mt-3 text-center sm:(mt-0 ml-4 text-left)">
            <h3 tw="text-lg leading-6 font-medium text-gray-900">Website update</h3>
            <div tw="mt-2">
              <p tw="text-sm text-gray-500">
                Our website has an update available. Please load the update to ensure the best possible experience.
              </p>
            </div>
          </div>
          <div tw="px-4 py-3 object-right justify-self-end sm:(px-6 flex flex-row-reverse)">
            <button
              onClick={() => window.location.reload(true)}
              type="button"
              tw="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-secondary-600 text-base font-medium text-white hover:bg-secondary-700 focus:(outline-none ring-2 ring-offset-2 ring-secondary-500) sm:(ml-3 w-auto text-sm)"
            >
              Load
            </button>
          </div>
        </div>
      </div>
    </Transition>
  )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question or discussion Issue discussing or asking a question about Gatsby
Projects
None yet
Development

No branches or pull requests