Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Implementing Update Logic in React

Welcome back. In the previous lesson, you took control of the deployment side of Over-the-Air updates by learning how to publish new web bundles to different channels using the Capgo CLI and bun scripts. Your application can receive automatic updates, and you can send them. Now, we'll bridge that gap by giving you, and by extension your users, more granular control over the update process itself.

So far, the updater plugin operates silently in the background. While efficient, this isn't always the ideal user experience. For significant changes, or simply to provide a smoother transition, you might want to inform the user that an update is ready and let them choose when to apply it.

This lesson focuses on implementing that client-side logic within your React application. We will shift from a fully automatic process to a developer-controlled, manual one. You will learn how to use the Capgo plugin's API to check for, download, and apply updates, laying the groundwork for a user-facing update notification system.

This diagram illustrates different update strategies. We are moving from the "Silent Updates" model to the "User-Choice Updates" model, where the application logic actively manages the update process.

By the end of this lesson, you will have implemented the core React logic to programmatically manage the OTA update lifecycle.

1. Switching to Manual Update Mode

To take control of the update process, the first step is to disable the plugin's default automatic behavior. This is a simple configuration change in your capacitor.config.ts file. By setting autoUpdate to false, you tell the plugin to stop its automatic checks, downloads, and reloads, handing over all responsibility to your application code.

In your apps/mobile/capacitor.config.ts, find the CapacitorUpdater plugin configuration and modify it as follows:

// apps/mobile/capacitor.config.ts

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  // ... other config
  plugins: {
    CapacitorUpdater: {
      autoUpdate: false, // <-- Change this from true to false
    },
    // ... other plugins
  },
};

export default config;

This single change is the key that unlocks manual control. However, with great power comes great responsibility.

The Importance of notifyAppReady()

When autoUpdate is enabled, the plugin automatically handles a critical handshake: it confirms that a new update has loaded successfully. If it doesn't get this confirmation after a reload, it assumes the update is broken and rolls back to the previous version to prevent a crash loop.

Now that we are in manual mode, we must perform this handshake ourselves. On every single application startup, you must call CapacitorUpdater.notifyAppReady(). This tells the native layer, "The JavaScript bundle has loaded and executed successfully. Don't roll back."

Cap-go/capacitor-updater - GitHub

The plugin's GitHub documentation provides the most detailed technical explanation of its API. It's crucial to understand the contract you're entering into when disabling auto-updates.

Focus on the section for the notifyAppReady() method. Read the description carefully to understand its purpose and the consequences of not calling it. Pay attention to the best practice advice to call it early in your app's lifecycle.

The best place for this call is at the root of your application, such as in your main App.tsx component, before any other complex logic or data fetching.

2. Implementing the Update Logic in React

A clean way to manage the update logic is by encapsulating it within a custom React hook. This aligns with modern React patterns and keeps your component logic clean. Let's create a useAppUpdater hook that will handle checking for, downloading, and applying updates.

Create a new file, for example, useAppUpdater.ts in your web app's lib or hooks directory.

The overall flow we'll implement is:

  1. Check: On app load, call CapacitorUpdater.getLatest() to see if a new bundle is available on the Capgo server.
  2. Download: If an update is available, use CapacitorUpdater.download() to fetch it in the background.
  3. Apply: Once downloaded, we'll have a function ready to apply the update using CapacitorUpdater.reload().

Let's build this step by step.

Getting Started - Capacitor Plugin Docs & Examples | Capgo

The official Capgo docs provide excellent examples for the manual update flow. We will be using the API methods described here to build our hook.

First, review the "Basic API Usage" section to familiarize yourself with the core functions: download through current. Then, look at the "Manual Update Flow" section for a complete example. We will adapt this logic into a more robust React hook.

3. Building the useAppUpdater Hook

Here is a complete implementation for a useAppUpdater hook. We will break down how it works below.

// In your web app, e.g., packages/web/src/hooks/useAppUpdater.ts

import { useEffect, useState, useCallback } from 'react';
import { CapacitorUpdater, type BundleInfo } from '@capgo/capacitor-updater';

export const useAppUpdater = () => {
  const [updateAvailable, setUpdateAvailable] = useState<BundleInfo | null>(null);

  // 1. Initial check on component mount
  useEffect(() => {
    // CRITICAL: Notify the plugin that the app is ready.
    CapacitorUpdater.notifyAppReady();

    const check = async () => {
      try {
        console.log('Checking for updates...');
        const latest = await CapacitorUpdater.getLatest();
        
        // The `version` property is present when a new version is available.
        if (latest.version) {
          console.log(`Update available: ${latest.version}`);
          // Download the bundle in the background
          const bundle = await CapacitorUpdater.download(latest);
          // Store the downloaded bundle info in state
          setUpdateAvailable(bundle);
        } else {
          console.log('No new updates available.');
        }
      } catch (error) {
        console.error('Update check or download failed:', error);
      }
    };

    check();
  }, []);

  // 2. Function to apply the update
  const applyUpdate = useCallback(async () => {
    if (updateAvailable) {
      console.log('Applying update...');
      // Set the update and reload the app
      await CapacitorUpdater.set({ id: updateAvailable.id });
      // reload() is not strictly necessary if set() is used, 
      // as the update will apply on next launch. Calling it
      // provides an immediate update experience.
      await CapacitorUpdater.reload(); 
    }
  }, [updateAvailable]);

  return { 
    isUpdateAvailable: !!updateAvailable,
    applyUpdate,
  };
};

Breakdown of the Hook:

  1. Initial Check (useEffect):
    • The hook runs once when the component that uses it mounts.
    • The very first thing it does is call CapacitorUpdater.notifyAppReady(), fulfilling our critical requirement.
    • It then calls CapacitorUpdater.getLatest(). This method queries the Capgo backend for the channel the device is on. The documentation is key here.

Cap-go/capacitor-updater - GitHub

Let's look at the getLatest method signature again.

Review the documentation for getLatest. Notice that when no update is available, the promise still resolves successfully, but the response structure is different. Checking for the presence of the version field is a reliable way to know if an update is available to be downloaded.

*   If `latest.version` exists, we know an update is available. We immediately call `CapacitorUpdater.download(latest)`, which fetches the bundle and stores it on the device.
*   Once the download is complete, we update our React state (`setUpdateAvailable`) with the downloaded bundle's information. This will cause any component using our hook to re-render, now aware that an update is ready.

2. Applying the Update (applyUpdate):
* We expose a function, applyUpdate, wrapped in useCallback for performance.
* This function checks if updateAvailable state holds a downloaded bundle.
* If so, it first calls CapacitorUpdater.set(), which tells the plugin to mark this downloaded bundle as the one to use next.
* Then, it calls CapacitorUpdater.reload(). This forces an immediate refresh of the WebView, loading the new bundle. The user sees the app restart, and they are now on the latest version.

To use this hook, you would simply call it in your main application component, like App.tsx:

// e.g., packages/web/src/App.tsx
import { useAppUpdater } from './hooks/useAppUpdater';

function App() {
  const { isUpdateAvailable, applyUpdate } = useAppUpdater();

  // In the next lesson, we will use `isUpdateAvailable` to show a UI prompt.
  // For now, we can log it or use a simple button for testing.

  return (
    <div>
      {/* ... your app content ... */}
      {isUpdateAvailable && (
        <div style={{ position: 'fixed', bottom: 20, right: 20, background: 'lightgreen', padding: '10px', borderRadius: '5px' }}>
          <p>Update is ready!</p>
          <button onClick={applyUpdate}>Install & Restart</button>
        </div>
      )}
    </div>
  );
}

Conclusion

In this lesson, you've taken a significant step toward creating a professional and user-friendly update experience. You have moved from a "black box" automatic system to a transparent, manually controlled flow that you manage directly from your React code.

Let's summarize the key achievements:

  • You disabled the automatic update feature by setting autoUpdate: false in capacitor.config.ts.
  • You understood the critical importance of calling CapacitorUpdater.notifyAppReady() on every app launch to prevent unwanted rollbacks.
  • You implemented a complete React hook, useAppUpdater, that follows the check -> download -> apply pattern using the getLatest, download, and reload API methods.
  • You now have the logical foundation (isUpdateAvailable state and an applyUpdate function) to build a user interface around.

The current test implementation with a simple button works, but it's not the polished experience we're aiming for. In our next lesson, we will focus on exactly that. We'll leverage the useAppUpdater hook to create a user-facing component (like a toast or modal) to inform the user of a pending update and prompt them to restart the app, using your existing shadcn/ui library.

In the next lesson, we will choose and implement one of these patterns to create a seamless user experience for app updates.

Can't find a good explanation? Sign up and we'll make it for you

Sign up