Continuous Migration: Keeping Users Uninterrupted

Cindy Manel
Cindy ManelJanuary 18, 2024
#react-admin#tutorial#architecture#js

Upgrading an application without disrupting end users is challenging. We've achieved this several times at Marmelab, including during lengthy migrations that can't be deployed all at once. We've developed a method for maintaining a seamless experience for end users throughout the transition. We call this the Continuous Migration approach.

This blog post explains the Continuous Migration method in practice, using a concrete example: transitioning from react-admin v3 to v4 in a CMS. The following screencast shows the state of an application midway through migration. Some resources use the previous react-admin v3 engine (posts and todos, with a blue app bar), while others use the new react-admin v4 engine (users and comments, with a green app bar).

In reality, the user experience is seamless, as the two apps can share the same theme. The only indicator of the application version is the URL:

This article is inspired by our experience migrating the backend for the ARTE editorial team, maintained by Marmelab.

Challenges Of A Long Migration

Upgrading a large application is an extensive task and can't be done all at once. The migration had to be deployed in stages over several weeks. To enable gradual migration, we decided to migrate resource by resource. A resource, in react-admin, is a set of CRUD pages related to a given domain (e.g., the List, Edition, Creation, and Show pages for a post form one Resource). Resources are mostly independent, making them suitable domain boundaries.

However, we couldn't migrate a given resource within the original app. This is because react-admin v3 relies on react-router V5, while react-admin v4 uses react-router V6, and these two versions are incompatible. Thus, migrated resources had to live in a second application, served from a different URL.

Users often navigate between resources when using the application. This meant they would need to frequently switch between two applications. We had to design a method to make this switch as seamless as possible from a UI standpoint.

An additional challenge was sharing authentication between the two apps. We aimed to ensure users wouldn't need to log in again when moving back and forth between apps.

A common challenge in migrations is migrating the data. Fortunately, in our case, both apps were Single-Page Apps relying on the same REST API, so we didn't have to worry about data synchronization.

Sharing Authentication Between Apps

React-admin apps delegate authentication to a service called authProvider. Our authProvider in the original app stored an authentication token in localStorage. To share this token between the two apps, we chose to have both applications share the same origin. This allowed the new app to access the token written by the old app, and vice versa.

To achieve this, we set up an Express server to serve both versions behind a subpath:

import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

var app = express();

app.use('/v3', express.static(path.join(__dirname, '../../admin-v3/dist')));
app.use('/v4', express.static(path.join(__dirname, '../../admin-v4/dist')));

app.listen(4000, () =>
    console.log('Express Server Now Running On localhost:4000/'),
);

These are technically two Single-Page Apps, but they are served by the same server:

Sharing A Common Navigation UI

End users constantly cross domain boundaries - they navigate from one resource to another. If each application only displayed a navigation menu for its resources, it would lead to UI inconsistencies.

For example, suppose the v3 app serves posts and todos, and the v4 app serves users and comments. Both apps show a navigation menu for their resources:

But when using the v3 app, users can't see the menu items for users and comments, and vice versa. And if they manage to switch to the v4 app (e.g., by using a bookmarks), they lose access to the posts and todos resources. This isn't practical.

So, we had to display the same navigation menu in both apps, regardless of the migration state. This ensured a seamless and visually coherent user experience.

However, the links for each menu item differed in each app. For instance:

To facilitate linking between apps, we used migration flags.

Using Migration Flags

We wanted to gradually mark resources as migrated: the old app always contained the code for all resources, but for those already migrated, users would be redirect to the new app. This meant we had to regularly change the app configuration to use v3 or v4 for a given resource. As we use feature flags to enable or disable features, we applied the same concept to migrations, hence the term migration flags.

Knowing each change in app migration required a production deployment, we realized our migration flags only needed to be read at build time. We opted for environment variables.

Each app had an environment variable listing the resources it managed. So, in the midst of migration, the two variables looked like:

V3_AVAILABLE_RESOURCES="posts,todos"
V4_AVAILABLE_RESOURCES="users,comments"

But because the two apps use a different build engine (create-react-app for v3, vite for v4), these variables needed a different prefix:

VITE_V3_AVAILABLE_RESOURCES="posts,todos"
REACT_APP_V4_AVAILABLE_RESOURCES="users,comments"

After migrating a resource, we simply updated the two flags:

VITE_V3_AVAILABLE_RESOURCES="posts"
REACT_APP_V4_AVAILABLE_RESOURCES="users,comments,posts"

Now, let's see in detail how each app uses these variables.

Admin V3 Implementation

We modified the old version of the app to redirect users to v4 for the migrated resources using the <Admin customRoutes> prop (since customRoutes execute before the routes defined by <Resource>):

import React from 'react';
import { Admin, Resource } from 'react-admin';
import { dataProvider } from './dataProvider';
+import { getV4Routes } from './getV4Routes';
import users from './users;
import posts from './posts;
import comments from './comments;
import todos from './todos;

export const AdminV3 = () => (
-   <Admin dataProvider={dataProvider}>
+   <Admin dataProvider={dataProvider} customRoutes={getV4Routes()}>
        <Resource name="users" {...users} />
        <Resource name="posts" {...posts} />
        <Resource name="comments" {...comments} />
        <Resource name="todos" {...todos} />
    </Admin>
);

Because it has a <Resource> item for all resources, even those migrated, this app generates a menu with all four items.

The getV4Routes function returns an array of v4 routes to redirect to, using the react-router V5 API:

// in getV4Routes.js
import React from 'react';
import { Route } from 'react-router-dom';
import { Redirect } from './Redirect';

export const V4Resources =
    process.env.REACT_APP_V4_AVAILABLE_RESOURCES?.split(',') || '';

export const getV4Routes = () => {
    const v4Routes = V4Resources.map(resource => {
        return [
            <Route
                exact
                path={`/${resource}/`}
                key={`/${resource}/`}
                component={() => <Redirect />}
            />,
            <Route
                path={`/${resource}/:id/show`}
                key={`/${resource}/:id/show`}
                component={() => <Redirect />}
            />,
            <Route
                path={`/${resource}/:id`}
                key={`/${resource}/:id`}
                component={() => <Redirect />}
            />,
        ];
    });
    return v4Routes.flat();
};

To redirect to the v4 app, we couldn't use the <Redirect/> component from react-router v5, as it's restricted to navigation within the same app, and we had to redirect to another single-page app.

So, we implemented an alternative <Redirect/> component. It doesn't need any prop, as it reads the target pathname using react-router's useLocation() hook.

// in Redirect.jsx
import React, { useEffect } from 'react';
import CircularProgress from '@material-ui/core/CircularProgress';
import { useLocation } from 'react-router-dom';

const adminV4Url = `${process.env.REACT_APP_API_URL}/v4/#`;

export const Redirect = () => {
    const { search, pathname } = useLocation();
    useEffect(() => {
        window.location.href = `${adminV4Url}${pathname}${search}`;
    }, [pathname, search]);
    return <CircularProgress />;
};

With this setup, the v3 app properly redirects users to the v4 app for already migrated resources. The navigation menu rendered correctly:

  • users => v4
  • posts => internal
  • comments => v4
  • todos => internal

Now, let's see how it works on the other side.

Admin v4 Implementation

The new version of the app includes already migrated resources (e.g., users and comments) and placeholders for yet-to-be-migrated resources (posts and todos). This ensures the generated navigation menu will contain entries for all resources. In addition, the app uses the <CustomRoutes> component to redirect to the old app for the not-yet-migrated resources.

import React from 'react';
import { Admin, CustomRoutes, ListGuesser, Resource } from 'react-admin';
import { dataProvider } from './dataProvider';
import { getV3Routes } from './getV3Routes';
import users from './users';
import comments from './comments';

export const AdminV4 = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="users" {...users} />
        <Resource name="posts" list={ListGuesser} />
        <Resource name="comments" {...comments} />
        <Resource name="todos" list={ListGuesser} />
        <CustomRoutes>{getV3Routes()}</CustomRoutes>
    </Admin>
);

The getV3Routes() function returns react-router V6 <Route> components:

// in getV3Routes.tsx
import React from 'react';
import { Route } from 'react-router-dom';
import { Redirect } from './Redirect';

const V3Resources = import.meta.env.VITE_V3_AVAILABLE_RESOURCES.split(',');

export const getV3Routes = () =>
    V3Resources.map((resource: string) => (
        <Route
            key={`${resource}/`}
            path={`${resource}/`}
            element={<Redirect />}
        >
            <Route key={`${resource}/:id`} path=":id" element={<Redirect />} />
            <Route
                key={`${resource}/:id/show`}
                path=":id/show"
                element={<Redirect />}
            />
        </Route>
    ));

Just like for AdminV3, we needed a custom <Redirect> component, as the <Navigate> component in react-router v6 doesn't work for external routes. The implementation is similar to that in v3, with the only difference being the way to import environment variables.

// in Redirect.tsx
import React, { useEffect } from 'react';
import { CircularProgress } from '@mui/material';
import { useLocation } from 'react-router-dom';

const API_URL = import.meta.env.VITE_API_URL;
const adminV3Url = `${API_URL}/v3/#`;

export const Redirect = () => {
    const { search, pathname } = useLocation();
    useEffect(() => {
        window.location.href = `${adminV3Url}${pathname}${search}`;
    }, [pathname, search]);
    return <CircularProgress />;
};

This code made the navigation menu in AdminV4 work as expected:

  • users => internal
  • posts => v3
  • comments => internal
  • todos => v3

Conclusion

Our meticulous resource-by-resource migration strategy, combined with effective use of environment variables, has proven successful in migrating the Arte backend from react-admin v3 to v4. The flexibility of serving both versions simultaneously empowered us to navigate the migration seamlessly, minimizing disruptions and ensuring a smooth transition for our users.

We encourage you to explore similar strategies when confronted with version upgrades in your projects. The Continuous Migration approach not only simplifies the migration process but also allows you to maintain a responsive and efficient application throughout the transition.

As we conclude, we invite you to explore the GitHub repository associated with this example. There, you'll find detailed documentation, code snippets, and resources to assist you in your own version upgrades.

We look forward to hearing about your experiences and insights in dealing with version upgrades. Happy migration!

Did you like this article? Share it!