We're currently refactoring a legacy React codebase. It's built with many class-based components and houses an over-engineered Redux Saga setup for an admin panel with straightforward CRUD forms.
A codebase of its size can't be overhauled in one take. An undertaking of that magnitude is risky and can hinder progress on features users are waiting for now. On the other hand, we want to avoid a never-ending race by coupling new features to legacy components.
As a contributor to the project it can be hard to determine whether a piece of code you don't have ownership of is "legacy, to be removed" or not. We wanted to make the distinction explicit, and give new or refactored code a new home. That allows us to slowly migrate the entire codebase to the new directories.
Since we weren't happy with the existing application structure, we were presented with the opportunity to have the old and new code live alongside each other in different directories. In this project, all code was previously organized by "type".
components/
admin/
front/
providers/
admin/
front/
redux/
admin/
front/
utils/
We wanted to organize per "section" instead. Split across admin
, front
, and shared
.
admin/
views/
front/
views/
shared/
components/
providers/
redux/
utils/
Because there are no collisions between the two setups, they can live alongside each other. Everything in admin
, front
, and shared
is considered non-legacy.
admin/
components/ Legacy!
front/
providers/ Legacy!
redux/ Legacy!
shared/
utils/ Legacy!
To make this distinction explicit, we configured a set of aliases in Vite.
export default {
resolve: {
alias: {
"@admin": path.resolve(__dirname, './src/admin'),
"@front": path.resolve(__dirname, './src/front'),
"@shared": path.resolve(__dirname, './src/shared'),
"@legacy-components": path.resolve(__dirname, './src/components'),
"@legacy-providers": path.resolve(__dirname, './src/providers'),
"@legacy-redux": path.resolve(__dirname, './src/redux'),
"@legacy-utils": path.resolve(__dirname, './src/utils'),
},
},
}
All legacy folders are aliased as @legacy-*
. For example the top-level components
folder is aliased to @legacy-components
.
import Dashboard from '@legacy-components/admin/Dashboard';
import Badge from '@shared/components/Badge';
We also installed and configured the excellent prettier-plugin-sort-imports to organize our imports across multiple sections: third party libraries, legacy code, and new code.
import { useState } from 'react';
import Tile from '@legacy-components/admin/Tile';
import UserAvatar from '@admin/components/UserAvatar';
import Dropdown from '@shared/components/Dropdown';
This is achieved with the following configuration:
"importOrder": [
"^w",
"^@legacy",
"^[./]"
],
Finally, if you're using an editor that supports file scopes, you can configure custom scopes and colors for legacy and new code.

Having legacy imports stand out almost gamifies the process of writing new code. When a new feature is added, we carefully consider whether it's worth using a @legacy
import versus taking the time to modernize the code behind it. This strategy promotes progressing with the ongoing refactor instead of having to take a few steps back every time a new feature is added.