Skip to content
Back to skills

React 19 Plugin Migration

BSecurity

Grafana 13 (April 2026) moves from React 18 to React 19. Incompatible plugins will break. **Do not upgrade React to 19** — only make forward-compatible changes. All changes go in **one PR**. Execute steps in order. Never manually edit `yarn.lock`. ---

  • 268 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added June 11, 2026
developmenttypescriptgobashreactnodedockertestinggit

Works with

  • cli

Security analysis

B89/100
  • highPerforms destructive filesystem operations
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned June 11, 2026

npx -y skills add grafana/skills --skill react-19-plugin-migration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of React 19 Plugin Migration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for React 19 Plugin Migration
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/grafana-react-19-plugin-migration/badge)](https://www.skillsdirectory.com/skills/grafana-react-19-plugin-migration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: react-19-plugin-migration
license: Apache-2.0
description:
  Migrate a Grafana plugin to React 19 compatibility. Use when the user asks to update a plugin
  for React 19, prepare for React 19, fix React 19 compatibility, upgrade to React 19, migrate
  to React 19, bump grafanaDependency to 12.3.0, externalize jsx-runtime, or run react-detect.
  Triggers on phrases like "update plugin for React 19", "React 19 migration", "prepare for
  React 19", "plugin React 19 compat", "grafanaDependency 12.3.0", "JSX runtime externals",
  "react-detect", "SECRET_INTERNALS", "ReactCurrentOwner", or "ReactCurrentDispatcher".
---

# Migrate Grafana Plugin to React 19

Grafana 13 (April 2026) moves from React 18 to React 19. Incompatible plugins will break.
**Do not upgrade React to 19** — only make forward-compatible changes.

All changes go in **one PR**. Execute steps in order. Never manually edit `yarn.lock`.

---

## Step 1: Detect plugin context

```bash
PLUGIN_JSON=$([ -f src/plugin.json ] && echo "src/plugin.json" \
  || ([ -f plugin/src/plugin.json ] && echo "plugin/src/plugin.json" || echo ""))
PKG_JSON=$([ -f package.json ] && echo "package.json" \
  || ([ -f plugin/package.json ] && echo "plugin/package.json" || echo ""))
PLUGIN_ID=$(jq -r '.id' $PLUGIN_JSON 2>/dev/null)
[ -f yarn.lock ] && PM="yarn" || ([ -f pnpm-lock.yaml ] && PM="pnpm" || PM="npm")
CP_VERSION=$(jq -r '.version' .config/.cprc.json 2>/dev/null)
echo "PLUGIN_ID=$PLUGIN_ID  PM=$PM  CP=$CP_VERSION"
```

If `PLUGIN_ID` is empty, ask the user for the plugin root path.

---

## Step 2: Scan for compatibility issues

Build the plugin and run the React 19 compatibility scanner:

```bash
npm run build 2>&1 | tail -5
npx -y @grafana/react-detect@latest 2>&1
```

Save the output. It flags:

- `jsxRuntimeImport` / `__SECRET_INTERNALS` → Step 4 fixes this
- `defaultProps` / `propTypes` / `ReactDOM.render` → Step 8 (source fixes)
- `findDOMNode` → Step 6 (dependency bump) or Step 8 (source fix)

If the build fails (plugin hasn't been built before), skip this step and run react-detect
after Step 9 instead. If output says "No breaking changes detected", still proceed — jsx-runtime
externalization and grafanaDependency bump are always required.

Re-run react-detect after Step 9 to confirm all issues are resolved.

---

## Step 3: Update `@grafana/create-plugin`

The scaffolding update brings in externals extraction, jest mocks, Docker fixes, and webpack
improvements needed for React 19. **Always do this before `add externalize-jsx-runtime`.**

Requires a clean git working tree. Create a feature branch first if not already on one.

### Run the update

```bash
npx @grafana/create-plugin@latest update 2>&1
```

### If `yarn install` fails with "engine is incompatible"

The update runs an intermediate `yarn install` without `--ignore-engines`. Complete it manually:

```bash
yarn install --ignore-scripts --ignore-engines 2>&1 | tail -10
```

Commit the intermediate state and re-run:

```bash
git add -A && git commit -m "chore: intermediate create-plugin update" --no-verify
npx @grafana/create-plugin@latest update 2>&1
```

### If ESLint 9 migration (004) fails with a parser error

The auto-migration can generate invalid JS on plugins with complex ESLint configs.
**Do not skip** — commit what succeeded, then complete the ESLint 9 migration manually:

```bash
git add -A && git commit -m "chore: update create-plugin (ESLint 9 migration manual)" --no-verify
```

Then follow the "Complete ESLint 9 migration" section below to finish.

### After the update

Always run install and verify:

```bash
yarn install --ignore-scripts --ignore-engines 2>&1 | tail -10
cat .config/.cprc.json
```

Commit if there are changes:

```bash
git add -A && git diff --cached --quiet || git commit -m "chore: update create-plugin scaffolding" --no-verify
```

---

## Step 3b: Complete ESLint 9 migration

The `create-plugin update` bumps ESLint to v9, which requires flat config (`eslint.config.js`)
instead of `.eslintrc`. Whether the auto-migration (004) succeeded, partially succeeded, or
failed, **you must ensure ESLint works before proceeding**.

### Check the current state

```bash
ls eslint.config.js .eslintrc* .config/.eslintrc* 2>/dev/null
npx eslint --version 2>&1
```

Three scenarios:

**A) `eslint.config.js` exists and `yarn lint` passes** — auto-migration succeeded. Proceed.

**B) `eslint.config.js` exists but `yarn lint` fails** — partial migration. Fix the issues:

```bash
yarn lint 2>&1 | head -30
```

Common fixes:
- `Invalid option '--ignore-path'` or `Invalid option '--ext'` → remove those flags from
  the `lint` script in `package.json`. In ESLint v9 flat config, ignores and file matching
  are configured inside `eslint.config.js`, not via CLI flags. Update to: `eslint --cache .`
- `Cannot find module 'eslint-plugin-deprecation'` → remove the import/reference from
  `eslint.config.js` (replaced by `@typescript-eslint/no-deprecated`)
- Other dead plugin imports → remove them from the config if the package was removed

**C) No `eslint.config.js` exists** — auto-migration failed. Create one manually:

```bash
ls node_modules/@grafana/eslint-config/flat.js 2>/dev/null
```

If `flat.js` exists, create `eslint.config.js` using it as the base:

```js
import grafanaConfig from '@grafana/eslint-config/flat';

export default [
  ...grafanaConfig,
  {
    ignores: ['**/dist/', '**/node_modules/', '**/.config/', '**/coverage/'],
  },
];
```

Then migrate any custom rules from the old `.eslintrc` into additional config objects in the array.
After creating the flat config:

1. Update the `lint` script: `"lint": "eslint --cache ."`
2. Delete the root `.eslintrc` (leave `.config/.eslintrc` — it's scaffolded and harmless)

### Verify lint works

```bash
yarn lint 2>&1 | tail -20
```

Fix auto-fixable issues with `yarn lint --fix`. Commit:

```bash
git add -A && git diff --cached --quiet || git commit -m "chore: complete ESLint 9 flat config migration" --no-verify
```

---

## Step 4: Externalize jsx-runtime

**Always use the `create-plugin add` command.** Requires a clean git working tree.

```bash
npx @grafana/create-plugin@latest add externalize-jsx-runtime 2>&1
```

Verify:

```bash
grep "jsx-runtime" .config/bundler/externals.ts 2>/dev/null
```

- Found → commit and proceed.
- Not found → command failed. **Only then** add externals manually to the root `webpack.config.ts`:

```ts
externals: ['react/jsx-runtime', 'react/jsx-dev-runtime'],
```

Commit:

```bash
git add -A && git diff --cached --quiet || git commit -m "feat: externalize jsx-runtime" --no-verify
```

---

## Step 5: Bump `grafanaDependency`

```bash
jq -r '.dependencies.grafanaDependency' $PLUGIN_JSON
```

If not already `>=12.3.0`, update it. The `create-plugin add` in Step 3 may have already done this.

---

## Step 6: Bump dependencies

### Faro (if present)

```bash
grep '"@grafana/faro' $PKG_JSON
```

| Package | Target |
|---------|--------|
| `@grafana/faro-react` | `^2.2.3` |
| `@grafana/faro-web-sdk` | `^2.2.3` |
| `@grafana/faro-web-tracing` | `^2.0.0` |

### Grafana packages

```bash
grep '"@grafana/' $PKG_JSON | grep -v faro | grep -v create-plugin
```

Bump `@grafana/data`, `@grafana/runtime`, `@grafana/schema`, `@grafana/ui` to `^12.2.0` or later.
Add `@grafana/i18n@^12.2.0` if the plugin uses translations or `@grafana/scenes` requires it.

### React types

Bump `react` and `react-dom` to `^18.3.0` (surfaces React 19 issues early).
Add `@types/react@^18.3.0` and `@types/react-dom@^18.3.0` to devDependencies if missing.

### Remove deprecated packages

Remove from devDependencies if present:
- `eslint-plugin-deprecation` (replaced by `@typescript-eslint/no-deprecated`)
- `@types/testing-library__jest-dom` (replaced by `setupTests.d.ts`)

### Broken transitive dependencies

If `yarn install` fails with a stale git reference, **do not edit yarn.lock**. Add a `resolutions` entry:

```json
"resolutions": {
  "<package-name>": "<working-version-or-git-ref>"
}
```

Then delete `yarn.lock` and `node_modules` and reinstall:

```bash
rm -rf node_modules yarn.lock
yarn install --ignore-engines 2>&1 | tail -10
```

---

## Step 7: Fix unmet `@openfeature/web-sdk` peer dependency

`@grafana/runtime` depends on `@openfeature/react-sdk` which has `@openfeature/web-sdk` as a
**peer dependency**. Yarn v1 (classic) does not auto-install peer deps.

Check if the plugin uses yarn classic:

```bash
yarn --version 2>&1 | head -1
```

If version starts with `1.`, check for warnings:

```bash
yarn install --ignore-engines 2>&1 | grep "unmet peer dependency.*openfeature/web-sdk"
```

If warnings are found:

```bash
yarn add -D @openfeature/web-sdk @openfeature/core --ignore-engines
```

**Skip condition:** Yarn v2+ or npm v7+ (peer deps are auto-installed).

---

## Step 8: Fix source code issues

```bash
grep -rn "ReactDOM\.render\|ReactDOM\.unmountComponentAtNode\|ReactDOM\.findDOMNode" src/ --include="*.tsx" --include="*.ts"
grep -rn "\.defaultProps\s*=" src/ --include="*.tsx" --include="*.ts"
grep -rn "\.propTypes\s*=" src/ --include="*.tsx" --include="*.ts"
grep -rn "contextTypes\|getChildContext" src/ --include="*.tsx" --include="*.ts"
grep -rn "createFactory" src/ --include="*.tsx" --include="*.ts"
grep -rn "ChangeEvent<HTMLInputElement>" src/ --include="*.tsx" --include="*.ts"
```

| Pattern | Fix |
|---------|-----|
| `ReactDOM.render()` | `createRoot(container).render(element)` |
| `defaultProps` on **function** components | Move to destructured parameter defaults |
| `defaultProps` on **class** components | Leave — still works |
| `propTypes` | Remove |
| `contextTypes` / `getChildContext` | Use `React.createContext()` + `useContext()` |
| `createFactory` | Use JSX or `createElement()` |
| `ChangeEvent<HTMLInputElement>` on checkbox | Change to `FormEvent<HTMLInputElement>` |

---

## Step 9: Build, typecheck, test

```bash
rm -rf node_modules dist
yarn install --ignore-engines 2>&1 | tail -10
yarn build 2>&1 | tail -10
yarn typecheck 2>&1 | tail -10
yarn test --watchAll=false 2>&1 | tail -10
```

| Error | Fix |
|-------|-----|
| `Cannot find module 'react/jsx-runtime'` | Step 4 not applied — re-run `create-plugin add` |
| `Cannot find module '@openfeature/web-sdk'` | Step 7 — `yarn add -D @openfeature/web-sdk @openfeature/core` |
| `Can't resolve '@grafana/i18n'` | `yarn add @grafana/i18n@^12.2.0` |
| `Cannot read properties of undefined (reading 'ReactCurrentOwner')` | Bump `@grafana-cloud/*` packages — see Step 6 |
| `aria-label is missing` on icon-only `Button` | Add `aria-label` prop (newer `@grafana/ui` requires it) |
| Stale git hash in `yarn.lock` | Add `resolutions` in `package.json`, delete lockfile, reinstall |

For detailed known issues (i18n crash, `@grafana/schema` type breaks, publicPath mismatch), see
[references/known-issues.md](references/known-issues.md).

---

## Step 10: Update CI (if applicable)

```bash
grep -rn "plugin-ci-workflows\|e2e-version" .github/workflows/ 2>/dev/null
```

- `plugin-ci-workflows@main` or >= 6.0.0 → already tests React 19. No changes needed.
- `plugin-actions/e2e-version` → add `skip-grafana-react-19-preview-image: false`.
- Neither found → test manually with `GRAFANA_VERSION=dev-preview-react19 docker compose up --build`.

---

## Step 11: Squash and push

```bash
git reset --soft origin/main
git add -A
git commit -m "fix: Prepare plugin for React 19 compatibility"
```

Commit message body should list: create-plugin version change, ESLint 9 migration, key dependency
bumps, and any source code fixes.

---

## References

- [Migration guide](https://grafana.com/developers/plugin-tools/migration-guides/update-from-grafana-versions/migrate-12_x-to-13_x)
- [React 19 blog post for plugin developers](https://grafana.com/blog/react-19-is-coming-to-grafana-what-plugin-developers-need-to-know/)
- [React 19 changelog](https://react.dev/blog/2024/12/05/react-19)
- [grafana-collector-app #1337](https://github.com/grafana/grafana-collector-app/pull/1337) — full migration with create-plugin update + source fixes
- [grafana/scenes issues](https://github.com/grafana/scenes/issues) — upstream i18n tracking

Files in this skill

  • SKILL.md11.9 KB
  • references/known-issues.md4.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.