bem-react

repository·master·Indexed 19 days ago

https://github.com/bem/bem-react

A 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.

Tokens
20.2K
Snippets
77
Records
87
Agent score
65%

What's inside bem-react

  1. Overview of bem-react packages

    master
    bem-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.
  2. How Dependency Injection (DI) works in @bem-react/di

    master

    Dependency Injection in this library allows you to decouple React components into different versions (e.g., desktop vs mobile, or experimental vs common) and switch between them easily.

    The Core Workflow:

    1. Define a Registry ID: Use a unique identifier (often via @bem-react/classname) to represent a specific dependency context.
    2. Create a Registry: Instantiate a Registry with that ID.
    3. Register Components: Use .set() or .fill() to map descriptive keys to specific component implementations.
    4. Inject the Registry: Wrap your common component with withRegistry(registry)(Component) to create a versioned instance.
    5. Consume Dependencies: Inside your components, use useRegistry(id) or the RegistryConsumer component 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)
  3. How to write a custom plugin

    master

    You can extend @bem-react/pack by implementing the Plugin interface. 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 done callback (to signal completion) and HookOptions containing the context and output paths.

    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()
    }
  4. How to manage BEM modifiers in React

    master

    The @bem-react/core package 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 using withBemMod and compose functions.

    Core Workflow

    1. Define Props: Extend IClassNameProps from @bem-react/core in your component's interface.
    2. Create Base Component: Use cn from @bem-react/classname to create a class name generator for your block.
    3. Define Modifiers: Use withBemMod to create variants that trigger based on specific prop values.
    4. Compose: Use compose to merge the base component with all desired modifiers.

    Composition Order

    The order of components passed to compose is critical. The FIRST modifier inside the compose method will be the one rendered if multiple modifiers attempt to change the same underlying structure (e.g., changing a button tag to an a tag).

    import { compose, withBemMod } from '@bem-react/core';
    
    // The first modifier in the list takes precedence for structural changes
    const Button = compose(
      withButtonThemeAction,
      withButtonTypeLink,
    )(ButtonPresenter);
  5. How redefinition levels are determined for imports

    master

    The linter determines the redefinition level of a file based on its filename to apply the whiteList restrictions:

    1. Filename Pattern: For files like Icon@desktop.tsx or Icon@desktop.example.tsx, the level is identified as desktop.
    2. Direct Naming: For a file named mobile.tsx, the level is mobile (provided mobile is defined in the whiteList).
    3. Fallback: If the filename does not match a known level, the linter assigns the level specified in the defaultLevel option (commonly set to common or base).
  6. Why use bem-react instead of standard React patterns?

    master

    While React is the industry standard, bem-react-core addresses 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 if or switch statements to handle variations, making them hard to grasp and modify. Most logic resides in the render() 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-core provides more efficient alternatives through redefinition levels and declarative block modification.

  7. Store non-component values in a Registry

    master

    A 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 useRegistry hook 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())
  8. Implement platform-specific features

    master

    When 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'
  9. Install and configure @bem-react/eslint-plugin

    master

    To use the BEM React ESLint plugin, add @bem-react to the plugins array in your .eslintrc configuration file. You can then enable specific rules in the rules section of your configuration.

    {
      "plugins": ["@bem-react"],
      "rules": {
        "@bem-react/no-classname-runtime": "warn"
      }
    }
  10. Define a platform-specific Public API

    master

    To 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.ts entry 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'
  11. Optimize modifiers with lazy loading

    master

    For better code splitting, you can use React.lazy inside a withBemMod wrapper to load heavy modifier-specific logic or styles only when the modifier is active.

    Note: If using Server-Side Rendering (SSR), replace React.lazy with @loadable/components or react-loadable.

    Implementation Pattern

    1. Create an async component file with a default export.
    2. In your modifier file, use withBemMod and wrap the component in Suspense with a lazy import.
    // 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>
      )
    })