From 4231d416a5dd819e4a2ad3ebf634d5af292252b7 Mon Sep 17 00:00:00 2001 From: Dmitriy Shlenskiy Date: Tue, 3 Feb 2026 17:13:29 +0300 Subject: [PATCH 1/2] feat: upgrade to React 19 with new JSX runtime - Update react, react-dom to 19.0.0 - Migrate from createElement to jsx from react/jsx-runtime - Remove forwardRef usage in withBemMod (refs forwarded by default in React 19) - Update peerDependencies to require React 19 - Update ESLint config for React 19 - Fix typos in comments and variable names BREAKING CHANGE: drop support for React 16, 17, and 18 --- .eslintrc | 5 ++--- environment.d.ts | 2 +- package.json | 8 ++++---- packages/core/core.ts | 34 ++++++++++++++-------------------- packages/core/package.json | 2 +- packages/di/di.tsx | 14 +++----------- packages/di/package.json | 2 +- scripts/rollup/build.js | 6 +++++- tsconfig.json | 2 +- 9 files changed, 32 insertions(+), 43 deletions(-) diff --git a/.eslintrc b/.eslintrc index 526aaf5c..dd197f51 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,8 +17,7 @@ }, "settings": { "react": { - "pragma": "React", - "version": "16.0" + "version": "19.0" } }, "rules": { @@ -178,7 +177,7 @@ "react/no-find-dom-node": 2, "react/no-multi-comp": 0, "react/no-set-state": 0, - "react/react-in-jsx-scope": 2, + "react/react-in-jsx-scope": 0, "react/require-optimization": 0, "react/self-closing-comp": 2, "react/style-prop-object": 2, diff --git a/environment.d.ts b/environment.d.ts index 3256ef89..3264ea8b 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -1,5 +1,5 @@ /** - * Eenvironment flag for development. + * Environment flag for development. * @internal */ declare const __DEV__: boolean diff --git a/package.json b/package.json index 315af101..840fd696 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@commitlint/cli": "17.3.0", "@commitlint/config-conventional": "17.3.0", "@types/jest": "26.0.14", - "@types/react": "18.0.26", + "@types/react": "19.0.6", "@typescript-eslint/eslint-plugin": "2.22.0", "@typescript-eslint/parser": "2.22.0", "chalk": "2.4.1", @@ -24,13 +24,13 @@ "husky": "3.0.5", "jest": "29.3.1", "jest-environment-jsdom": "29.3.1", - "@testing-library/react": "13.4.0", + "@testing-library/react": "16.1.0", "lerna": "3.5.1", "lint-staged": "9.2.5", "prettier": "2.8.1", "pretty-bytes": "5.2.0", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "19.0.0", + "react-dom": "19.0.0", "rollup": "1.10.1", "rollup-plugin-node-resolve": "4.2.3", "rollup-plugin-replace": "2.1.0", diff --git a/packages/core/core.ts b/packages/core/core.ts index 20fbd24a..4a61922c 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -1,4 +1,5 @@ -import { ComponentType, FC, createElement, forwardRef } from 'react' +import { ComponentType, FC, JSX } from 'react' +import { jsx } from 'react/jsx-runtime' import { cn, NoStrictEntityMods, ClassNameFormatter } from '@bem-react/classname' import { classnames } from '@bem-react/classnames' @@ -71,7 +72,7 @@ export function withBemMod( entity = entity || cn(blockName) entityClassName = entityClassName || entity() - const BemMod = forwardRef((props: T & K, ref) => { + const BemMod: FC = (props) => { modNames = modNames || Object.keys(mod) // TODO: For performance can rewrite `every` to `for (;;)`. @@ -82,8 +83,6 @@ export function withBemMod( return modValue === propValue || (modValue === '*' && Boolean(propValue)) }) - const nextProps = Object.assign({}, props, { ref }) - if (isModifierMatched) { const modifiers = modNames.reduce((acc: Dictionary, key: string) => { if (mod[key] !== '*') acc[key] = mod[key] @@ -92,8 +91,8 @@ export function withBemMod( }, {}) modifierClassName = modifierClassName || entity(modifiers) - nextProps.className = classnames(modifierClassName, props.className) - // Replace first entityClassName for remove duplcates from className. + const className = classnames(modifierClassName, props.className) + // Replace first entityClassName for remove duplicates from className. .replace(`${entityClassName} `, '') if (typeof enhance === 'function') { @@ -111,13 +110,11 @@ export function withBemMod( ModifiedComponent = WrappedComponent as any } - // Use createElement instead of jsx to avoid __assign from tslib. - return createElement(ModifiedComponent, nextProps) + return jsx(ModifiedComponent, Object.assign({}, props, { className })) } - // Use createElement instead of jsx to avoid __assign from tslib. - return createElement(WrappedComponent, nextProps) - }) + return jsx(WrappedComponent, props) + } if (__DEV__) { setDisplayName(BemMod, { @@ -130,11 +127,8 @@ export function withBemMod( return BemMod } - // Ignore `forwardRef` typings to keep compatibility with `HOC` - const withMod = (WithBemMod as any) as { - (WrappedComponent: ComponentType): ( - props: T & K, - ) => React.ReactElement + const withMod = WithBemMod as any as { + (WrappedComponent: ComponentType): FC __isSimple: boolean __blockName: string @@ -194,7 +188,7 @@ function composeSimple(mods: any[]) { return (Base: ComponentType) => { function SimpleComposeWrapper(props: Record) { const modifiers: NoStrictEntityMods = {} - const newProps: any = { ...props } + const newProps: any = Object.assign({}, props) for (let key of modNames) { const modValues = allMods[key] @@ -221,7 +215,7 @@ function composeSimple(mods: any[]) { newProps.className = entity(modifiers, [props.className]) - return createElement(Base, newProps) + return jsx(Base, newProps) } if (__DEV__) { const allModsFormatted = Object.keys(allMods) @@ -320,9 +314,9 @@ export function compose() { f.__isSimple ? simple.push(f) : enhanced.push(f) } - const oprimizedFns = simple.length ? [composeSimple(simple), ...enhanced] : enhanced + const optimizedFns = simple.length ? [composeSimple(simple), ...enhanced] : enhanced - return oprimizedFns.reduce( + return optimizedFns.reduce( (a, b) => { return function() { return a(b.apply(0, arguments)) diff --git a/packages/core/package.json b/packages/core/package.json index e51df915..72045266 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,7 +19,7 @@ "@bem-react/classnames": "1.4.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^19.0.0" }, "files": ["build", "core.d.ts"], "main": "./build/core.cjs", diff --git a/packages/di/di.tsx b/packages/di/di.tsx index bd172ecb..5afd4c8b 100644 --- a/packages/di/di.tsx +++ b/packages/di/di.tsx @@ -1,12 +1,5 @@ -import React, { - ReactNode, - FC, - ComponentType, - createContext, - useContext, - useRef, - createElement, -} from 'react' +import { ReactNode, FC, ComponentType, createContext, useContext, useRef } from 'react' +import { jsx } from 'react/jsx-runtime' export type RegistryContext = Record @@ -49,8 +42,7 @@ export function withRegistry() { return ( - {/* Use createElement instead of jsx to avoid __assign from tslib. */} - {createElement(Component, props)} + {jsx(Component, props)} ) }} diff --git a/packages/di/package.json b/packages/di/package.json index 2d7d1bd3..b5644985 100644 --- a/packages/di/package.json +++ b/packages/di/package.json @@ -15,7 +15,7 @@ "unit": "../../node_modules/.bin/jest --config ../../.config/jest/jest.config.js" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^19.0.0" }, "files": ["build", "di.d.ts"], "main": "./build/di.cjs", diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index d6911cef..51c2b416 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -33,7 +33,11 @@ function getExternalDependencies(packagePath) { const content = readFileSync(packageJsonPath, 'utf-8') const { dependencies, peerDependencies } = JSON.parse(content) - return [...Object.keys(Object(dependencies)), ...Object.keys(Object(peerDependencies))] + return [ + ...Object.keys(Object(dependencies)), + ...Object.keys(Object(peerDependencies)), + 'react/jsx-runtime', + ] } function getTypescriptConfig(packagePath) { diff --git a/tsconfig.json b/tsconfig.json index c4df3787..42980bbf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ "declaration": true, - "jsx": "react", + "jsx": "react-jsx", "lib": ["es6", "dom"], "module": "esnext", "moduleResolution": "node", From ee9d71195225afb72ea09c0782ccaca760721955 Mon Sep 17 00:00:00 2001 From: Dmitriy Shlenskiy Date: Thu, 5 Feb 2026 15:37:41 +0300 Subject: [PATCH 2/2] fix(pack): add skipLibCheck to fix @types/node compatibility The @types/node package uses Symbol.dispose and esnext.disposable which require TypeScript 5.2+. Adding skipLibCheck allows building with TypeScript 4.9. --- packages/pack/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pack/tsconfig.json b/packages/pack/tsconfig.json index d845ebbf..299d6006 100644 --- a/packages/pack/tsconfig.json +++ b/packages/pack/tsconfig.json @@ -7,6 +7,7 @@ "target": "ES2017", "module": "CommonJS", "declaration": true, + "skipLibCheck": true, /* Module Resolution Options */ "esModuleInterop": true, /* Strict Type-Checking Options */