react-19-plugin-migration

Installation
SKILL.md

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

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:

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

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:

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

Commit the intermediate state and re-run:

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:

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:

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

Commit if there are changes:

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

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:

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:

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

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

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

yarn lint 2>&1 | tail -20

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

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.

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

Verify:

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:
externals: ['react/jsx-runtime', 'react/jsx-dev-runtime'],

Commit:

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

Step 5: Bump grafanaDependency

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)

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

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:

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

Then delete yarn.lock and node_modules and reinstall:

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:

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

If version starts with 1., check for warnings:

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

If warnings are found:

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

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

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.


Step 10: Update CI (if applicable)

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

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

Related skills
Installs
128
Repository
grafana/skills
GitHub Stars
31
First Seen
Mar 31, 2026