@bem-react/core ·

Core package helps organize and manage components with BEM modifiers in React.
Install
npm i -S @bem-react/core
Usage
Let's say, you have an initial App file structure as follows:
App.tsx
Components/
Button/
Button.tsx
And you need to set up two optional types of buttons that will be different from the Button.tsx
. (In our example those will be Button of theme 'action' and Button of type 'link')
You can handle those using @bem-react/core.
Follow the guide.
Step 1.
In your Components/Button/index.tsx
, you define the type of props your button can get within the interface that extends IClassNameProps from '@bem-react/core' :
import { ReactType } from 'react'
import { IClassNameProps } from '@bem-react/core'
import { cn } from '@bem-react/classname'
export interface IButtonProps extends IClassNameProps {
as?: ReactType
}
export const cnButton = cn('Button')
Step 2.
Set up the basic Button variant which will be rendered if no modifiers props are set in the parent component.
Inside your Components/Button/Button.tsx
:
import React, { FC } from 'react'
import { IButtonProps, cnButton } from './index'
export const Button: FC<IButtonProps> = ({
children,
className,
as: Component = 'button',
...props
}) => (
<Component {...props} className={cnButton({}, [className])}>
{children}
</Component>
)
Step 3.
Set up the optional withButtonTypeLink and optional withButtonThemeAction variants that will be rendered if {type: 'link'}
and/or {theme: 'action'}
modifiers are set in the parent component respectively.
Inside your Components/Button/
you add folders _type/
with Button_type_link.tsx
file in it and _theme/
with Button_theme_action.tsx
.
App.tsx
Components/
Button/
Button.tsx
index.tsx
+ _type/
+ Button_type_link.tsx
+ _theme/
+ Button_theme_action.tsx
Set up the variants:
Note! The second parameter in withBemMod()
is the condition for this component to be applied.
1. In Components/Button/_type/Button_type_link.tsx
import React from 'react'
import { withBemMod } from '@bem-react/core'
import { IButtonProps, cnButton } from '../index'
export interface IButtonTypeLinkProps {
type?: 'link'
href?: string
}
export const withButtonTypeLink = withBemMod<IButtonTypeLinkProps, IButtonProps>(
cnButton(),
{ type: 'link' },
(Button) => (props) => <Button {...props} as="a" />,
)
2. In Components/Button/_theme/Button_theme_action.tsx
import { withBemMod } from '@bem-react/core'
import { cnButton } from '../index'
export interface IButtonThemeActionProps {
theme?: 'action'
}
export const withButtonThemeAction = withBemMod<IButtonThemeActionProps>(cnButton(), {
theme: 'action',
})
Step 4.
Finally, in your App.tsx
you need compose only necessary the variants with the basic Button.
Be careful with the import order - it directly affects your CSS rules.
NOTE: If you wanna use same modifiers with different values you should use composeU
for getting correctly typings.
import React, { FC } from 'react';
import { compose, composeU } from '@bem-react/core';
import { Button as ButtonPresenter } from './Components/Button/Button';
import { withButtonTypeLink } from './Components/Button/_type/Button_type_link';
import { withButtonThemeAction } from './Components/Button/_theme/Button_theme_action';
import { withButtonThemeDefault } from './Components/Button/_theme/Button_theme_default';
import './App.css';
const Button = compose(
composeU(withButtonThemeAction, withButtonThemeDefault),
withButtonTypeLink,
)(ButtonPresenter);
export const App: FC = () => (
<div className="App">
<Button>I'm basic</Button>
// Renders into HTML as: <button class="Button">I'm Basic</button>
<Button type="link" href="#stub">I'm type link</Button>
// Renders into HTML as: <a href="#stub" class="Button Button_type_link">I'm type link</a>
<Button theme="action">I'm theme action</Button>
// Renders into HTML as: <button class="Button Button_theme_action">I'm theme action</button>
<Button theme="action" type="link">I'm all together</Button>
// Renders into HTML as: <a class="Button Button_theme_action Button_type_link">I'm all together</a>
</div>
);
Note! The order of optional components composed onto ButtonPresenter is important: in case you have different layouts and need to apply several modifiers the FIRST one inside the compose method will be rendered!
E.g., here:
export const Button = compose(
withButtonThemeAction,
withButtonTypeLink,
)(ButtonPresenter)
If your withButtonThemeAction was somewhat like
<button className={className}>{children}</button>
your JSX-component:
<Button type="link" theme="action">Hello</Button>
would render into HTML:
<a class="Button Button_theme_action Button_type_link">Hello</a>
Use reexports for better DX
IMPORTANT: use this solution if tree shaking enabled
Example:
Block/Block.tsx
Block/Block@desktop.tsx
Block/_mod/Block_mod_val1.tsx
Block/_mod/Block_mod_val2.tsx
Block/_mod/Block_mod_val3.tsx
Create reexports for all modifers in intex files by platform: desktop, phone, amp, etc.
export * from './Block'
export * from './Block/_mod'
export * from './Block@desktop'
export * from './Block/_mod'
export * from './'
export * from './Block_mod_val1.tsx'
export * from './Block_mod_val2.tsx'
export * from './Block_mod_val3.tsx'
Usage:
import { Block as BlockPresenter, withModVal1 } from './components/Block/desktop'
const Block = withModVal1(BlockPresenter)
Optimization. Lazy load for modifiers.
Solution for better code spliting with React.lazy and dynamic imports
NOTE If your need SSR replace React.lazy method for load Block_mod.async.tsx
module to @loadable/components or react-loadable
import React from 'react'
import { cnBlock } from '../Block'
import './Block_mod.css'
export const DynamicPart: React.FC = () => <i className={cnBlock('Inner')}>Loaded dynamicly</i>
export default DynamicPart
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>
)
})
Usage:
import {
Block as BlockPresenter,
withMod
} from './components/Block/desktop';
const Block = withMod(BlockPresenter);
export const App = () => {
return (
{}
<Block />
{}
<Block mod />
);
}
Debug
To help your debug "@bem-react/core" support development mode.
For <Button type="link" theme="action">Hello</Button>
(from the Example above), React DevTools will show:
<WithBemMod(Button)[theme:action] ...>
<WithBemMod(Button)[type:link] className="Button Button_theme_action" ...>
<a className="Button Button_type_link Button_theme_action">Hello</a>
</WithBemMod(Button)[type:link]>
</WithBemMod(Button)[theme:action]>