bem-react
repository·master·Indexed 19 days ago
https://github.com/bem/bem-reactA set of tools for developing user interfaces using the BEM methodology in React with built-in TypeScript support. The ecosystem includes @bem-react/classname for BEM class building, @bem-react/classnames for merging CSS classes, @bem-react/core for managing modifiers via withBemMod and compose, @bem-react/di for dependency injection and component versioning, and @bem-react/eslint-plugin for enforcing BEM-related linting rules.
What's inside bem-react
- bem-react is a toolkit for developing user interfaces using the BEM methodology within React. It includes built-in support for TypeScript type annotations.
Overview of bem-react packages
masterbem-react is a suite of tools designed for developing user interfaces using the BEM (Block, Element, Modifier) methodology within React. It provides full support for TypeScript type annotations. The ecosystem is divided into several specialized packages for class name management, core logic, dependency injection, and linting.How Dependency Injection (DI) works in @bem-react/di
masterDependency Injection in this library allows you to decouple React components into different versions (e.g.,
desktopvsmobile, orexperimentalvscommon) and switch between them easily.The Core Workflow:
- Define a Registry ID: Use a unique identifier (often via
@bem-react/classname) to represent a specific dependency context. - Create a Registry: Instantiate a
Registrywith that ID. - Register Components: Use
.set()or.fill()to map descriptive keys to specific component implementations. - Inject the Registry: Wrap your common component with
withRegistry(registry)(Component)to create a versioned instance. - Consume Dependencies: Inside your components, use
useRegistry(id)or theRegistryConsumercomponent to access the registered dependencies instead of importing them directly.
// 1. Define ID export const registryId = cn('App') // 2. Create Registry & 3. Register const registry = new Registry({ id: registryId }) registry.set('Header', HeaderDesktop) // 4. Inject export const AppDesktop = withRegistry(registry)(AppCommon) // 5. Consume const { Header } = useRegistry(registryId)- Define a Registry ID: Use a unique identifier (often via
How to write a custom plugin
masterYou can extend
@bem-react/packby implementing thePlugininterface. Plugins can hook into different stages of the build lifecycle using the following methods:onStart: Run at the very beginning.onBeforeRun: Run before the main build step.onRun: Run during the main build step.onAfterRun: Run after the main build step.onFinish: Run at the very end.
Each hook receives a
donecallback (to signal completion) andHookOptionscontaining thecontextandoutputpaths.import { Plugin, OnDone, HookOptions } from '@bem-react/pack/lib/interfaces' class MyPlugin implements Plugin { async onRun(done: OnDone, { context, output }: HookOptions) { // Do something stuff. done() } } export function useMyPlugin(): MyPlugin { return new MyPlugin() }How to manage BEM modifiers in React
masterThe
@bem-react/corepackage allows you to organize components using BEM (Block Element Modifier) methodology. It works by defining a base component (the "Presenter") and then composing various modifier variants onto it usingwithBemModandcomposefunctions.Core Workflow
- Define Props: Extend
IClassNamePropsfrom@bem-react/corein your component's interface. - Create Base Component: Use
cnfrom@bem-react/classnameto create a class name generator for your block. - Define Modifiers: Use
withBemModto create variants that trigger based on specific prop values. - Compose: Use
composeto merge the base component with all desired modifiers.
Composition Order
The order of components passed to
composeis critical. The FIRST modifier inside thecomposemethod will be the one rendered if multiple modifiers attempt to change the same underlying structure (e.g., changing abuttontag to anatag).import { compose, withBemMod } from '@bem-react/core'; // The first modifier in the list takes precedence for structural changes const Button = compose( withButtonThemeAction, withButtonTypeLink, )(ButtonPresenter);- Define Props: Extend
How redefinition levels are determined for imports
masterThe linter determines the redefinition level of a file based on its filename to apply the
whiteListrestrictions:- Filename Pattern: For files like
Icon@desktop.tsxorIcon@desktop.example.tsx, the level is identified asdesktop. - Direct Naming: For a file named
mobile.tsx, the level ismobile(providedmobileis defined in thewhiteList). - Fallback: If the filename does not match a known level, the linter assigns the level specified in the
defaultLeveloption (commonly set tocommonorbase).
- Filename Pattern: For files like
Why use bem-react instead of standard React patterns?
masterWhile React is the industry standard,
bem-react-coreaddresses specific architectural limitations in common web development tasks. Standard React patterns often lead to suboptimal solutions in the following areas:- Decomposition: React components often rely on imperative
iforswitchstatements to handle variations, making them hard to grasp and modify. Most logic resides in therender()method, making it difficult to override functionality without rewriting the method. - Code Reuse: React typically uses Higher-Order Components (HOCs) or inheritance for reuse. Inheritance struggles with combining orthogonal features without complex hierarchies, and HOCs can add complexity.
- Cross-platform Development: Teams often choose between building separate versions for each platform (high maintenance/sync cost) or a single responsive version (increased code complexity and potential performance degradation on mobile).
- Experiments (A/B Testing): Codebase branching is expensive to maintain and sync, while targeted conditionals within a single codebase increase complexity, hurt performance, and leave "dead code" after experiments end.
- Sharing Component Libraries: Modifying shared libraries via forking, inheritance, or runtime patching is often difficult or incomplete (e.g., an inherited component might not be used by other library components that compose it).
bem-react-coreprovides more efficient alternatives through redefinition levels and declarative block modification.- Decomposition: React components often rely on imperative
Store non-component values in a Registry
masterA registry is not limited to React components; it can store any auxiliary data, such as settings, configuration objects, or functions. These can be accessed via the
useRegistryhook just like components.// In the registry setup registry.set('theme', 'dark') registry.set('showNotification', () => console.log('Hello!')) // In the component const { theme, showNotification } = useRegistry(cnApp())Implement platform-specific features
masterWhen a component requires platform-specific logic (such as hover states or specific input handling), create a platform-specific file (e.g.,
Button@desktop.tsx) that re-exports everything from the common implementation and then applies platform-specific styles or logic.Common implementation:
// src/components/Button/Button.tsx import React from 'react' import './Button.css' export const Button = ({ children }) => <button className="Button">{children}</button>Desktop implementation:
// src/components/Button/Button@desktop.tsx export * from './Button' import './Button@desktop.css'// src/components/Button/Button@desktop.tsx export * from './Button' import './Button@desktop.css'Install and configure @bem-react/eslint-plugin
masterTo use the BEM React ESLint plugin, add
@bem-reactto thepluginsarray in your.eslintrcconfiguration file. You can then enable specific rules in therulessection of your configuration.{ "plugins": ["@bem-react"], "rules": { "@bem-react/no-classname-runtime": "warn" } }Define a platform-specific Public API
masterTo control which parts of a component are exposed to developers for a specific platform and to encapsulate the absence of code for other platforms, use a platform entry point (e.g.,
desktop.ts).Note: Ensure your project is configured with tree shaking to prevent unused code from being included in the final bundle.
Example
desktop.tsentry point:// src/components/Button/desktop.ts export * from './Button@desktop' export * from './_view/Button_view_default' export * from './hooks/useCheckedState'Usage in a consumer component:
// src/components/Feature/Feature.tsx import { Button as ButtonDesktop, withViewDefault, useCheckedState, } from 'components/Button/desktop'// src/components/Button/desktop.ts export * from './Button@desktop' export * from './_view/Button_view_default' export * from './hooks/useCheckedState'Optimize modifiers with lazy loading
masterFor better code splitting, you can use
React.lazyinside awithBemModwrapper to load heavy modifier-specific logic or styles only when the modifier is active.Note: If using Server-Side Rendering (SSR), replace
React.lazywith@loadable/componentsorreact-loadable.Implementation Pattern
- Create an async component file with a
defaultexport. - In your modifier file, use
withBemModand wrap the component inSuspensewith alazyimport.
// Inside your modifier file export const withMod = withBemMod<BlockModProps>(cnBlock(), { mod: true }, (Block) => (props) => { const DynamicPart = lazy(() => import('./Block_mod.async.tsx')) return ( <Suspense fallback={<div>Updating...</div>}> <Block {...props}> <DynamicPart /> </Block> </Suspense> ) })import React, { Suspense, lazy } from 'react' import { cnBlock } from '../Block' export interface BlockModProps { mod?: boolean } export const withMod = withBemMod<BlockModProps>(cnBlock(), { mod: true }, (Block) => (props) => { const DynamicPart = lazy(() => import('./Block_mod.async.tsx')) return ( <Suspense fallback={<div>Updating...</div>}> <Block {...props}> <DynamicPart /> </Block> </Suspense> ) })- Create an async component file with a