-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: v12 migration guide [LIBS-689] (#884)
* docs: update js-to-jsx docs * docs: add known caveats to js-to-jsx * docs: add v12 migration guide * chore: deps * docs: add first draft of React 18 changes * docs: add links and examples for React 18 test changes * docs: improve link formatting * docs: punctuation change * docs: more React 18 examples * docs: space * docs: add some more react notes * docs: remove todo * docs: dashes * docs: mention expected snapshot changes * docs: mention fireEvent to userEvent change * docs: comment on typescript changes * docs: revision * docs: update breaking changes and deprecations * docs: comments about env vars * docs: intro revisions * docs: straight apostrophes * docs: suggestions in React 18 section Co-authored-by: Rene Pot <Topener@users.noreply.github.com> * docs: small revisions in react 18 section * docs: env var suggestions Co-authored-by: Rene Pot <Topener@users.noreply.github.com> * docs: last suggestions Co-authored-by: Rene Pot <Topener@users.noreply.github.com> * docs: remove "noticed in __ app" mentions * chore: formatting * docs: change bloggy stuff for technical language --------- Co-authored-by: Rene Pot <Topener@users.noreply.github.com>
- Loading branch information
1 parent
64d7ce9
commit 56bbfd4
Showing
4 changed files
with
973 additions
and
776 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,341 @@ | ||
--- | ||
title: v12: Migrate to Vite & React 18 | ||
--- | ||
|
||
# Platform v12 Migration Guide: Vite & React 18 | ||
|
||
Version 12.0.0 introduces Vite and React 18 to the platform, and introduces some great benefits including faster dev server startup, faster application builds, and better plugin UX. Check out the announcement blog post here (todo: link)! | ||
|
||
This guide describes the features that version 12 introduces, then some tips on how to easily upgrade to `@dhis2/cli-app-scripts` v12 and what to expect along the way. | ||
|
||
## Features | ||
|
||
Vite is now used to start and build apps, replacing Create React App (CRA). This confers several significant improvements: | ||
|
||
- It's fast! Starting up an app is nearly instant, and building an app is about 3-4 times faster compared to CRA | ||
- Plugins are handled better: | ||
- Start-up of both the app and plugin is nearly instant | ||
- The app and plugin are run on the same port | ||
- Support for a plugin without an app is improved | ||
- Code is shared between entrypoints, which makes bundles smaller | ||
- Hot Module Replacement (HMR) for code changes in the plugin will be as fast as in the app | ||
- There's a small suite of tools available in the CLI when running an app in dev mode: | ||
- With the dev server running, press `h + enter` to see the options | ||
- Options include exposing the server on LAN, opening the app in the browser, cleanly quitting the server, and restarting the dev server (which can be helpful if you're modifying libraries in `node_modules`) | ||
- The build output includes a summary to inspect chunks | ||
|
||
The React version is now increased to v18, which provides some performance benefits and new APIs. More details about React 18 can be found at the [React docs](https://react.dev/blog/2022/03/29/react-v18). | ||
|
||
Version 12 also includes some new features for TypeScript: | ||
|
||
- Bootstrapping an app with a TypeScript template is now supported: `d2-app-scripts init --typescript my-app`. See the [`init` docs](../scripts/init.md) for more about the command | ||
- Vite has native support for TypeScript | ||
- Although not in the App Platform package, with the latest `@dhis2/cli-style`, TypeScript type checking is performed when running `d2-style lint` | ||
- With `@dhis2/ui` version 9, the UI library is now typed as well | ||
|
||
## Getting started | ||
|
||
By running these steps, you should be able to run your app right away: | ||
|
||
1. `yarn add @dhis2/app-runtime @dhis2/ui -D @dhis2/cli-app-scripts` | ||
2. `npx yarn-deduplicate yarn.lock && yarn` | ||
3. Try out `yarn start --allowJsxInJs`, and your app should be running 🚀 | ||
|
||
There will be some other changes you will probably want to make, which are described in the following sections. Our goal is to make it easy to adopt the new changes, so we have some tools to facilitate the process. | ||
|
||
## Vite & JSX | ||
|
||
As suggested by the `--allowJsxInJs` flag, Vite does not allow JSX syntax in files without a .jsx or .tsx extension by default; attempting to run `start` or `build` in that case will throw an error like `✘ [ERROR] The JSX syntax extension is not currently enabled`. | ||
|
||
JSX in .js files is not supported for performance reasons: it saves needing to parse non-JSX files for JSX syntax. We can add complicated config to enable parsing of JSX in .js files, but that approach has several downsides: | ||
|
||
1. It's nonstandard — it is most correct to use the right file extensions | ||
2. It requires more config to maintain | ||
3. It has performance costs on startup and when parsing files | ||
The new default for `@dhis2/cli-app-scripts`, therefore, will be the same as Vite's. To make the transition easier, though, we provide some tools to 1) support JSX in .js files temporarily, and 2) easily migrate JS files to .jsx extensions if they contain JSX syntax. | ||
|
||
### Allow JSX in JS temporarily | ||
|
||
To make it as fast as possible to upgrade and get up and running with Vite and React 18, we added an `--allowJsxInJs` flag that you can pass to `start` and `build` scripts, so that you can test out the changes without any file renaming. In many cases, you should be able to upgrade your platform dependencies and run `start` with the flag and see your app running! Other times, you may need to add a few tweaks — see the troubleshooting and breaking changes sections below. | ||
|
||
Due to the downsides mentioned above, however, we will remove the `--allowJsxInJs` option in the next major version. When you are ready to rename your files, you can use the migration script we've made. | ||
|
||
### .js to .jsx migration script | ||
|
||
We made a codemod that can migrate your codebase to the right file extensions quickly! It checks .js files for JSX syntax, updates the extension if needed, then updates imports within the file tree to target the new filenames. | ||
|
||
```sh | ||
yarn d2-app-scripts migrate js-to-jsx | ||
``` | ||
|
||
Check out the [script docs](../scripts/migrate/js-to-jsx.md) for options, tips, and caveats. | ||
|
||
## React 18 | ||
|
||
With this update, we also upgraded from React 16 to React 18. Upgrading to `@dhis2/cli-app-scripts` version 12 should generally a smooth process for the consuming apps. Most of the [changes](https://react.dev/blog/2022/03/08/react-18-upgrade-guide) shouldn't affect most apps. | ||
|
||
### Default props | ||
|
||
The main change in React 18 that is expected to affect apps at runtime is that using `MyComponent.defaultProps = { ... }` is deprecated, and will be removed in the next major version. While non-breaking, this change comes along with warning messages in the console for every rendered component that uses them, which can be chatty and annoying. | ||
|
||
The fix is to move prop defaults -- either to default parameters for functional components, or to a static property for class components. This code mod can help with the migration, and was used for the `@dhis2/ui` library: [transform.js](https://gist.github.com/kabaros/45ed16660ac89dc48d22e03836b1e981) | ||
|
||
:::info Note | ||
Default params for functional components are computed on each render, so default values like arrays and objects as defaults may unintentionally retrigger `useEffect` and `useMemo` functions on each render. Instead, use a stable reference like so: | ||
|
||
```js | ||
const arr = [] | ||
const MyComponent = ({ options = arr }) => { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
::: | ||
|
||
### Tests | ||
|
||
React 18 has minimal effects on the operation of an app in a browser, but it has a more significant effect on tests, depending on what tools are used and how the tests are written. | ||
|
||
#### Snapshots | ||
|
||
When the tests are up and running, you can expect some small changes from React 18. You may see changed classnames and different whitespace; these shouldn't cause any functional changes, and they're safe to approve. | ||
|
||
#### React Testing Library | ||
|
||
Testing-library supports React 18 since v13, so the first step is to upgrade that library and the supporting libraries around it. These major versions bumps come with a few breaking changes that we will document here with some examples to make migration easier. | ||
|
||
Examples come from the migration for the Aggregate Data Entry app; you can check out the PR [here](https://github.com/dhis2/aggregate-data-entry-app/pull/396) for more examples in context. | ||
|
||
##### `userEvent` | ||
|
||
`userEvent` is async now in `@testing-library/user-event`. Check the [release notes](https://github.com/testing-library/user-event/releases/tag/v14.0.0) for an idea of the improvements and new API. In practice, this means mostly that you'd need to add an `await` for `userEvent` interactions like `userEvent.click()`. | ||
|
||
Actions using the `fireEvent` API should also be migrated to `userEvent`, e.g. `fireEvent.change(selector(), { target: { value: 'input text' } })` should be changed to `userEvent.type(selector(), 'input text')`. | ||
|
||
Here are some examples that illustrate the changes: | ||
|
||
```diff | ||
+ import userEvent from '@testing-library/user-event' | ||
|
||
- it('should allow re-running validation', () => { | ||
+ // Make the function async: | ||
+ it('should allow re-running validation', async () => { | ||
// ... | ||
|
||
- userEvent.click(getByText('Run validation again')) | ||
- // Or this other form: | ||
- fireEvent.click(getByText('Run validation again')) | ||
+ // Now that it's asynchronous, await the event: | ||
+ await userEvent.click(getByText('Run validation again')) | ||
|
||
await findByText('2 medium priority alerts') | ||
expect(queryByText('There was a problem running validation')).toBeNull() | ||
}) | ||
``` | ||
|
||
```diff | ||
- const login = async ({ user }) => { | ||
+ // Don't need `user`; will use `userEvent` later: | ||
+ const login = async () => { | ||
- // Remove fireEvent calls: | ||
- fireEvent.change(screen.getByLabelText('Username'), { | ||
- target: { value: 'Fl@klypa.no' }, | ||
- }) | ||
- fireEvent.change(screen.getByLabelText('Password'), { | ||
- target: { value: 'SolanOgLudvig' }, | ||
- }) | ||
+ // Replace with userEvent.type(): | ||
+ await userEvent.type(screen.getByLabelText('Username'), 'Fl@klypa.no') | ||
+ await userEvent.type(screen.getByLabelText('Password'), 'SolanOgLudvig') | ||
|
||
- await user.click(screen.getByRole('button', { name: /log in/i })) | ||
+ // Replace `user` with `userEvent`: | ||
+ await userEvent.click(screen.getByRole('button', { name: /log in/i })) | ||
} | ||
``` | ||
|
||
##### `renderHook` | ||
|
||
`renderHook` is not a separate library anymore; it is part of the `@testing-library/react`, and it has [quite a different API](https://testing-library.com/docs/react-testing-library/api/#renderhook). | ||
|
||
:::warning Note | ||
Some docs online still point to the old version, which can be confusing. | ||
::: | ||
|
||
- Use `import { renderHook } from '@testing-library/react'` instead of `import { renderHook } from '@testing-library/react-hooks` | ||
- The [return type](https://testing-library.com/docs/react-testing-library/api/#renderhook-result) for renderHook is different. One major difference is the absence of `waitForNextUpdate` returned from `renderHook`. Now it can be replaced with the `waitFor` from `@testing-library`, and adding an expectation to wait for, for example. | ||
- Testing hooks that throw errors is different. Use `expect(renderHook(() => { ... })).toThrow(....)` | ||
|
||
This example demonstrates some of the changes: | ||
|
||
```diff | ||
- import { renderHook } from '@testing-library/react-hooks' | ||
+ // Update import, and include waitFor: | ||
+ import { renderHook, waitFor } from '@testing-library/react' | ||
import React from 'react' | ||
import { useRootOrgData } from './use-root-org-data.js' | ||
|
||
it('should provide the org unit data', async () => { | ||
- const { result, waitForNextUpdate } = renderHook( | ||
- () => useRootOrgData(['A0000000000']), | ||
- { wrapper } | ||
- ) | ||
+ // waitForNextUpdate is no longer part of the result: | ||
+ const { result } = renderHook(() => useRootOrgData(['A0000000000']), { | ||
+ wrapper, | ||
+ }) | ||
|
||
- await waitForNextUpdate() | ||
+ // Instead, use waitFor: | ||
+ await waitFor(() => {}) | ||
// ... | ||
}) | ||
``` | ||
|
||
In some cases, `renderHookResult.waitFor` can just be removed and doesn't need replacing: | ||
|
||
```diff | ||
it('should update validationToRefresh', async () => { | ||
- const { result, waitFor } = renderHook(useValidationStore) | ||
+ const { result } = renderHook(useValidationStore) | ||
|
||
act(() => { | ||
result.current.setValidationToRefresh(true) | ||
}) | ||
|
||
- await waitFor(() => { | ||
- expect(result.current.getValidationToRefresh()).toBe(true) | ||
- }) | ||
+ expect(result.current.getValidationToRefresh()).toBe(true) | ||
}) | ||
``` | ||
|
||
And testing errors: | ||
|
||
```diff | ||
it('throws an error when passing both a client- and a serverDate', async () => { | ||
const clientDate = new Date('2022-10-13 10:00:00') | ||
const serverDate = new Date('2022-10-13 08:00:00') | ||
- const { result } = renderHook(() => | ||
- useClientServerDate({ clientDate, serverDate }) | ||
- ) | ||
- expect(result.error).toEqual( | ||
- new Error( | ||
- '`useClientServerDate` does not accept both a client and a server date' | ||
- ) | ||
|
||
+ expect(() => { | ||
+ renderHook(() => useClientServerDate({ clientDate, serverDate })) | ||
+ }).toThrow( | ||
+ '`useClientServerDate` does not accept both a client and a server date' | ||
+ ) | ||
}) | ||
``` | ||
|
||
##### Fake timers | ||
|
||
Faking timers can help with tests that seem to fail with concurrency issues: | ||
|
||
```diff | ||
describe('<Comment />', () => { | ||
+ // Set up fake timers: | ||
+ beforeEach(() => { | ||
+ jest.useFakeTimers | ||
+ }) | ||
|
||
afterEach(() => { | ||
// ... | ||
}) | ||
|
||
it('shows a loading indicator when submitting a comment change', async () => { | ||
// ... | ||
|
||
- const { getByRole, queryByRole } = render(<Comment item={item} />) | ||
+ const { getByRole, queryByRole, findByRole } = render( | ||
+ <Comment item={item} /> | ||
+ ) | ||
|
||
- userEvent.click(getByRole('button', { name: 'Edit comment' })) | ||
+ const editButton = await findByRole('button', { name: 'Edit comment' }) | ||
+ // Set up user event with fake timers: | ||
+ const user = userEvent.setup({ | ||
+ advanceTimers: jest.advanceTimersByTime, | ||
+ }) | ||
+ await user.click(editButton) | ||
|
||
// ... | ||
|
||
expect(queryByRole('progressbar')).not.toBeInTheDocument() | ||
- userEvent.click(getByRole('button', { name: 'Save comment' })) | ||
- expect(getByRole('progressbar')).toBeInTheDocument() | ||
+ // Use the setup from above: | ||
+ const btnSaveComment = await findByRole('button', { | ||
+ name: 'Save comment', | ||
+ }) | ||
+ await user.click(btnSaveComment) | ||
+ await findByRole('progressbar') | ||
}) | ||
}) | ||
``` | ||
|
||
#### Enzyme | ||
|
||
Enzyme is [considered dead](https://dev.to/wojtekmaj/enzyme-is-dead-now-what-ekl): there is no official adapter for React 18 (or 17), so it's best to [migrate to React Testing Library](https://testing-library.com/docs/react-testing-library/migrate-from-enzyme/) for tests, if possible. | ||
|
||
:::tip | ||
If it's not currently possible to migrate, there is an [unofficial React 18 adapter](https://github.com/cfaester/enzyme-adapter-react-18) that can help in the mean time. However, this should be considered a temporary solution, and migrating away from Enzyme should be scheduled. | ||
::: | ||
|
||
## Environment variables | ||
|
||
### `REACT_APP_` prefix no longer needed | ||
|
||
With the migration away from Create React App, the formatting for environment variable names has gotten simpler. | ||
|
||
As before, environment variables with names that start with `DHIS2_` are added to the app, and can be picked up from `.env` files or on the command line for example. Before, the platform had to add `REACT_APP_` before `DHIS2_` so that Create React App would add the variable to the app, so a variable like `DHIS2_MY_VAR` in the environment would become accessible on `process.env.REACT_APP_DHIS2_MY_VAR` in the app. Now, the `REACT_APP_` prefix isn't needed, so the variable can be accessed on just `process.env.DHIS2_MY_VAR`. | ||
|
||
The `REACT_APP_`-prefixed variables are now deprecated and will be removed in the next major version. However they will be kept around alongside the shorter `DHIS2_` variables in this version for backwards compatibility. | ||
|
||
For example, both `process.env.REACT_APP_DHIS2_MY_VAR` and `process.env.DHIS2_MY_VAR` will be available. | ||
|
||
:::tip | ||
The `REACT_APP_` prefixed variables will be removed in the next version, so make sure to migrate to the shorter `DHIS2_` variable names. | ||
::: | ||
|
||
### `import.meta.env` | ||
|
||
By default, Vite adds its environment variables to the [`import.meta.env` object](https://vite.dev/guide/env-and-mode#env-variables). The App Platform will add `DHIS2_` env vars to `import.meta.env` as well, and the configured [`envPrefix`](https://vite.dev/config/shared-options.html#envprefix) is `DHIS2_`. Emphasis is given to environment variables on the `process.env` object, however, since it is a more widely-used pattern. | ||
|
||
## Troubleshooting & tips | ||
|
||
Here are some edge cases that were encountered when upgrading some apps with the new platform version: | ||
|
||
- There is a bug in Vite: importing from a directory, e.g. `'./app/'` where we would assume to get `./app/index.[jt]s`, can incorrectly resolve to a file with a similar name to the directory, e.g. `./App.tsx`. I see this as a bug with Vite, but as a workaround, file and dir names can be changed so they don't conflict. | ||
- If you're using `identity-obj-proxy` in your test config, make sure you add it to the project's dependencies. Previously, some apps got away with using the proxy without an explicitid dependency because the package is a dependency of `react-scripts` | ||
- You may see some build warnings coming from the `mathjs` package. Upgrading to a recent version of `mathjs` will handle these warnings. | ||
|
||
## Full list of breaking changes and deprecations | ||
|
||
The most likely and impactful breaking changes have been described in detail above. However this doesn't cover all the breaking changes and deprecations introduced in this upgrade. Below is the exhausted list. | ||
|
||
### Breaking changes | ||
|
||
1. Node 18 or Node 20+ is required. Node 19 is not supported. | ||
2. JSX in files without .jsx or .tsx extensions is not supported by default | ||
3. React 18: Most significant testing changes are described above; the rest can be read about in the React [migration guide](https://react.dev/blog/2022/03/08/react-18-upgrade-guide) | ||
4. Flow types won't work, although this is a motivator for extensible config | ||
5. Import aliases will likely break (also a motivator for extensible config) | ||
6. `global.variableName` is no longer supported; use `window.variableName` instead | ||
7. In order to make the dev server available on LAN, the `--host` flag needs to be passed to the start script | ||
8. Workers imported in the “webpack” way will need to be imported in a new way: [docs](https://vitejs.dev/guide/features.html#web-workers) | ||
9. An `App.jsx` entrypoint is no longer quietly added if a plugin entrypoint is defined or for a defined but empty entrypoints object | ||
10. Plugins and apps are started together in the same process on the same port | ||
11. Custom `index.html` files are no longer supported | ||
12. `import * as MyClass from 'my-module'` is now more restrictive, and may break in some cases. So far, changing the import to `import MyClass from 'my-module'` has fixed it. DOMPurify is one example case. | ||
|
||
### Deprecations | ||
|
||
- The default `direction` config value is `ltr`; in the future, it will be `auto` | ||
- `--allowJsxInJs` will be removed | ||
- Some deprecated PWA config caching option names will be removed in the next version | ||
- Env vars prefixed with `REACT_APP_DHIS2_` will be removed in the next version in favor of just the `DHIS2_` prefix |
Oops, something went wrong.