Skip to content

Conversation

@simonadomnisoru
Copy link
Contributor

@simonadomnisoru simonadomnisoru commented Sep 10, 2025

DHIS2-18801

This pull request migrates the project from Create React App to Vite, following the official migration guide

Key changes introduced during the migration:

  1. Upgrade @dhis2/cli-app-scripts, @dhis2/ui and @dhis2/app-runtime
  2. Remove environment variables deprecated prefix
  3. Replace deprecated ReactDOM.render with createRoot (e.g. DeleteControl.component.tsx)
  4. Use static imports for the date-fns locale
  5. Replace Material-UI with Emotion:
  • Added custom withStyles and withTheme HOCs that replicate the logic of Material-UI’s ones. The HOCs implementation takes a pragmatic approach, with the goal of removing Material-UI and migrating to Emotion as smoothly as possible.
  • Introduced a new theme object (leverage the theme object already done in Replace the HOCs #3811)
  • Removed the classnames library and replaced it with Emotion’s cx helper.
  • The following functionality was tested to confirm compatibility with Emotion:
    • Using className={class.name} on both plain HTML elements and UI library components
    • Styles imported from our own *.module.css files or external css files (e.g. leaflet, virtualized-select, etc.)
    • Classes passed down from the parent component (e.g. IconButton.component.tsx)
    • Combining multiple class names (e.g. IndicatorsSection.component.tsx)
    • Applying conditional class names (e.g. BreadcrumbItem.tsx)
  1. Removed the leaflet CDN dependency and imported the styles (chore: Remove leaflet CDN dependencies #4001)
  2. Vite/Rollup handle module resolution differently from Create React App/Webpack. Using both alias and relative imports can cause the same module to be included multiple times in the bundle, leading to issues with async code and shared state (e.g 8443af9). This is the intended Rollup behavior. To address this issue, I ensured consistent use of absolute imports and add an ESlint rule to enforce this convention in 16580d3.
  3. Upgraded react-query to v4, the first version compatible with React 18. One notable change affecting Capture app is the useage of isInitialLoading for disabled queries.
  4. In dev mode, the app is slower to completely load the first time. This is expected due to Vite’s architecture as files aren’t pre-bundled to enable fast startup and Hot Module Replacement. The HMR allows for instant updates without a full reload, but it results in a longer initial load, as the browser requests many small modules. I’ve added some scripts to serve the app in prod mode locally (yarn build:standalone and yarn serve), useful for running Cypress tests or inspecting the production bundle.

@github-actions
Copy link

github-actions bot commented Sep 17, 2025

@simonadomnisoru
Copy link
Contributor Author

simonadomnisoru commented Nov 5, 2025

Amazing work, @simonadomnisoru! You’ve upgraded the app and migrated it to use Vite really smoothly. I added two comments with some minor points I’m a bit uncertain about.

In addition, I noticed we’re still using some lifecycle methods with the UNSAFE_ prefix, such as UNSAFE_componentWillReceiveProps. It works fine for now, but perhaps this would be a good time to replace those methods to ensure full React 18 compatibility going forward.

@henrikmv Thank you for your review! I’ve now removed and refactored most of the deprecated UNSAFE_ lifecycle methods. Only two remain in FormBuilder.component and OrgUnitTree.component, as their logic is more complex and requires a deeper dive to ensure all edge cases are handled correctly. There is already an open ticket for this DHIS2-16685, and I think it’s safer to address the remaining two components separately under that task.

Let me know if you have any additional feedback. Thanks!

Copy link
Contributor

@eirikhaugstulen eirikhaugstulen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @simonadomnisoru - really great work on this! 🎉

Left some small comments, mostly NITs, but have a look and let me know if you disagree!


const allEnvVariables = {
...envFromCypressFiles,
...(process.env || {}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tried to run this myself, so it might work - but should this be import.meta.env instead?

https://vite.dev/guide/env-and-mode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.env still works correctly and Cypress runs successfully both locally and in GitHub workflows. Feel free to try it in your local environment as well if you’d like to confirm. Thanks!

@@ -17,7 +17,7 @@ const computeServerCacheVersion = ({ minor, patch, tag }) =>
computeTagVersionPart(tag);

const computeAppCacheVersion = () => {
const appCacheVersionAsString = process.env.REACT_APP_CACHE_VERSION;
const appCacheVersionAsString = process.env.DHIS2_CACHE_VERSION;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be import.meta.env.DHIS2_CACHE_VERSION or is the app-shell doing some magic here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app-shell is doing some magic, and according to the migration guide, process.env is well supported and a more widely-used pattern. Refactoring to import.meta.env would require extra effort, as Jest doesn’t support it by default. Therefore, continuing to use process.env seems reasonable to me.

Copy link
Contributor

@eirikhaugstulen eirikhaugstulen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @simonadomnisoru - this is a big improvement!

Copy link
Contributor

@henrikmv henrikmv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brilliant work!

Copy link
Member

@JoakimSM JoakimSM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks quite good! Let me spend some more time on this tomorrow, just wanted to give some initial comments.

Comment on lines 29 to 37
const rawStyles = stylesOrCreator && typeof stylesOrCreator === 'function'
? stylesOrCreator(theme as T)
: stylesOrCreator;

// Transform the raw style object with Emotion’s css() so that className={classes.label} continues to work as before
const classes = Object.keys(rawStyles).reduce((acc, key) => {
acc[key] = css(rawStyles[key] as any);
return acc;
}, {} as any);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Given we have this code in a "centralized" place, maybe we can wrap in useMemo for a minor perf. improvement?

Suggested change
const rawStyles = stylesOrCreator && typeof stylesOrCreator === 'function'
? stylesOrCreator(theme as T)
: stylesOrCreator;
// Transform the raw style object with Emotion’s css() so that className={classes.label} continues to work as before
const classes = Object.keys(rawStyles).reduce((acc, key) => {
acc[key] = css(rawStyles[key] as any);
return acc;
}, {} as any);
const classes = useMemo(() => {
const rawStyles = stylesOrCreator && typeof stylesOrCreator === 'function'
? stylesOrCreator(theme as T)
: stylesOrCreator;
// Transform the raw style object with Emotion’s css() so that className={classes.label} continues to work as before
return Object.keys(rawStyles).reduce((acc, key) => {
acc[key] = css(rawStyles[key] as any);
return acc;
}, {} as any);
}, []);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! I added it, thanks.

"workspaces": [
"packages/rules-engine"
],
"dependencies": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I do think we should explicitly list the dependencies we use directly. if not, the depedency graph becomes ambiguous. Not sure we should make an exception here? I understand we will have to make sure we use the same version as app-runtime, but at least another dep update won't suddenly change the version we are using.


return new Promise<void>((resolve) => {
import(`moment/locale/${locale}`)
import(`moment/dist/locale/${locale}`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked at your solution for dateFns and it seems like we have a problem with moment as well. The moment locale is not being properly set from what I can see (have a look at the weekdays variable in the setLocaleDataAsync function in this file, still resolves to english even if the ui-language is something else).

These dynamic imports are quite confusing, not going to lie. Tried the import.meta.glob thing a bit, wasn't able to make it work. Let's talk about this tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoakimSM I’ve removed the dynamic import and use the switch statement for moment as well. Could you please take another look? Thanks!

</MuiThemeProvider>
</JSSProviderShell>
</React.Fragment>
<QueryClientProvider client={queryClient}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? I think this is already inserted by the App shell / runtime?

Copy link
Contributor Author

@simonadomnisoru simonadomnisoru Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we do need it. When I remove it and restart the server, the app breaks with the error No QueryClient set, use QueryClientProvider to set one useQueryClient.


Screenshot 2025-11-12 at 08 13 17

Copy link
Member

@JoakimSM JoakimSM Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a look and the problem is that we are adding multiple versions of react-query to the bundle and the app and app-runtime are using different versions (if you have a look in yarn.lock, you will see react-query listed twice). I fixed it by doing this:

yarn add @tanstack/react-query@^4.42.0 -W
npx yarn-deduplicate --packages @tanstack/react-query

you should end up with something like this in yarn.lock:

"@tanstack/react-query@^4.42.0", "@tanstack/react-query@^4.40.0":
  version "4.42.0"
  resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.42.0.tgz#a4d2527713e841c71a4a4b3f9412bb98ea5eec59"
  integrity sha512-j0tiofkzE3CSrYKmVRaKuwGgvCE+P2OOEDlhmfjeZf5ufcuFHwYwwgw3j08n4WYPVZ+OpsHblcFYezhKA3jDwg==
  dependencies:
    "@tanstack/query-core" "4.41.0"
    use-sync-external-store "^1.2.0"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, yes running the deduplicate fixed the issue. Thanks for the suggestion! 🙌

Comment on lines 8 to 10
export type WithStyles<S extends Record<string, any> | ((t: typeof theme) => S)> = {
classes: Record<string, any>;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we have a job to do on the types in this file, ok to leave that for later though. But can we change the WithStyles to the following suggestion:

Suggested change
export type WithStyles<S extends Record<string, any> | ((t: typeof theme) => S)> = {
classes: Record<string, any>;
};
type Style = Record<string, string | number>
export type WithStyles<T extends Record<string, Style> | ((t: typeof theme) => Record<string, Style>)> = {
classes: { [K in keyof (T extends (args: unknown) => unknown ? ReturnType<T> : T)]: string }
}

Let me know if something breaks, but I think it should be an improvement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the suggested types causes errors because string | number is too restrictive and doesn’t account for the media queries or the pseudo-selectors (e.g. &:focus, &:hover, &.isLastItem etc.). To fix the errors, I extended on your suggestion so that type Style also supports nested objects. Let me know if you agree. Thank you!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, that makes sense 👍

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
17 New issues
1 New Critical Issues (required ≤ 0)
17 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants