npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@billgangcom/theme-lib

v1.126.0

Published

Package Installation:

Readme

Package Installation:

npm i @billgangcom/theme-lib

or

pnpm i @billgangcom/theme-lib

In main.tsx import App component:

import { App } from '@billgangcom/theme-lib';

and use it like this:

createRoot(document.getElementById('root')!).render(
  <App
    blocks={blocks}
    settings={SettingsBlock}
    header={HeaderBlock}
    footer={FooterBlock}
    announcement={AnnouncementBlock}
  />
)

App accepts 5 props:

  1. blocks - is a blocks object in the format key - value, where key is the block name and value is the block class itself
  2. settings - a separate block for settings
  3. header - a separate block for header
  4. footer - a separate block for footer
  5. announcement - a separate block for the announcement block (above the header)

An object with blocks looks something like this:

import {
	SettingsBlock,
	ProductHeroBlock,
	HeaderBlock,
	FooterBlock,
	ContentBlock,
	FeatureBlock,
	ImageGalleryBlock,
	StatsBlock,
	LogosBlock,
	FaqBlock,
	ReviewsBlock,
	CtaBlock,
	ItemsBlock,
	FeaturedItemsBlock,
	ContactUsBlock,
	ProductInfoBlock,
	PrimaryHeroBlock,
	AnnouncementBlock,
	PostInfoBlock,
	VideoBlock,
} from './blocks';

const blocks = {
	PrimaryHero: PrimaryHeroBlock,
	ProductHero: ProductHeroBlock,
	Items: ItemsBlock,
	FeaturedItems: FeaturedItemsBlock,
	Content: ContentBlock,
	Feature: FeatureBlock,
	Video: VideoBlock,
	Stats: StatsBlock,
	ImageGallery: ImageGalleryBlock,
	Logos: LogosBlock,
	FAQ: FaqBlock,
	Reviews: ReviewsBlock,
	CTA: CtaBlock,
	ContactUs: ContactUsBlock,
	ProductInfo: ProductInfoBlock,
	PostInfo: PostInfoBlock,
};

How to create blocks:

We use classes to create.

import { IBlock } from '@billgangcom/theme-lib';

export class NameBlock implements IBlock {
	//realize block
}

Implementing the IBlock interface we need to implement three methods: renderBlock, renderSettings and renderPreview

  1. renderBlock is a method that is triggered when we render a block in builder and in storefront. It takes two arguments - settings (the same settings that you will have in settingsBlock, just as an object) and context - the context of the page, it looks like this:
export interface PageContext {
	listings: Listing[] | null;
	products: Product[] | null;
	reviews: any;
	categories: Category[] | null;
	faqs: Faq[] | null;
	general: ShopInfo | null;
	fullPosts: any;
	posts: any;
}
  1. renderSettings is a method that is triggered when we click on a block in the builder and the settings are opened. This method is responsible for rendering the settings. Accepts only pageContext
  2. renderPreview - method that is triggered when hovering over block cards (shows the initial state of the block)

Also in your block there should be two fields: blockSettings - the block settings themselves and blockColors - the colours that are used (to be taken from enum ColorVariables), a sample block snippet looks like this:

import { BlockSettings, ColorVariables, IBlock, PageContext } from '@billgangcom/theme-lib';

interface NameSettings extends BlockSettings {
	//some-fields
}

export class NameBlock implements IBlock {
	blockSettings: NameSettings;
	blockColors: ColorVariables[];

	renderBlock(settings: Record<string, any>, context: PageContext) {
		//realize
	}

	renderSettings(context: PageContext) {
		//realize
	}

	renderPreview() {
		//realize
	}
}

How to implement the renderSettings method.

  1. we initialise the states of the settings we need using React.useState
const [isActiveHeader, setIsActiveHeader] = React.useState<boolean>(
	this.blockSettings.isActiveHeader,
);
  1. and update the settings in useEffect our this.blockSettings.isActiveHeader
React.useEffect(() => {
	this.blockSettings.isActiveHeader = isActiveHeader;
}, [isActiveHeader]);

How does the state itself change? There is a separate section in the package with the UI folder

It's imported like this:

import {
	Alignment,
	Button,
	ButtonsSettings,
	ButtonType,
	LayoutSettings,
	Padding,
	TextSettings,
	TextType,
	Link,
	Wrapper,
} from '@billgangcom/theme-lib/ui';

And it's used that way in renderSettings:

return (
      <div className={styles.settings}>
        <LayoutSettings
          alignment={alignment}
          padding={padding}
          setAlignment={setAlignment}
          setPadding={setPadding}
        />

        <TextSettings
          title="Header"
          isActiveText={isActiveHeader}
          setIsActiveText={setIsActiveHeader}
          text={headerText}
          setText={setHeaderText}
          typesText={headerTypesText}
          setTypesText={setHeaderTypesText}
          typeFont={headerTypeFont}
          setTypeFont={setHeaderTypeFont}
        />
        <TextSettings
          title="Text"
          isActiveText={isActiveText}
          setIsActiveText={setIsActiveText}
          text={text}
          setText={setText}
          typesText={typesText}
          setTypesText={setTypesText}
          typeFont={textTypeFont}
          setTypeFont={setTextTypeFont}
        />
        <ButtonsSettings
          isActiveButtons={isActiveButtons}
          setIsActiveButtons={setIsActiveButtons}
          buttons={buttons}
          setButtons={setButtons}
        />
      </div>
    )

LayoutSettings - responsible for alignment and padding for the block TextSettings - responsible for text customisation ButtonsSettings - button settings in block

There is also the most popular component, ItemsSettings

<ItemsSettings<Cards>
    items={cards}
    setItems={setCards}
    title="Cards"
    itemsType="input"
    itemsPlaceholder="Title"
    draggable={false}
    editable
    other={[
    {
        isItem: isIcon,
        setIsItem: setIsIcon,
        label: 'Icon',
    },
    ]}
    withImage
    modalOptions={[
    {
        type: 'text',
        label: 'Description',
        field: {
        type: 'types',
        text: 'description',
        typeFont: 'typeFont',
	},
    },
    {
        type: 'selectInfiniteList',
        label: 'Icon',
        field: 'iconName',
        itemsOptions: [...iconNames],
        // leftAddonOptions создаются автоматически из itemsOptions
        // Иконки загружаются динамически по мере скролла
    },
    ]}
    addableOptions={{
    description: {
        'en-US': '',
    },
    types: [],
    iconName: 'GameController',
    typeFont: 'md',
    }}
    limit={4}
/>

The ItemsSettings component is designed for creating and customising interactive lists with the ability to:

  1. add, delete and edit items;
  2. drag'n'drop sorting;
  3. using different input types (text, select, image, etc.);
  4. connecting additional modal settings for each element;
  5. customisation of display and logic (icons, captions, additional fields).

| Prop name | Description | | ------------------------------------------ | ----------------------------------------------------------------------- | | items, setItems | A list of editable items and a function to update it. | | itemsType | Element type: ‘input’, ‘select’, ‘image’, ‘selectInfiniteList’. | | modalOptions | An array of options for each element's modal configuration window. | | addableOptions | Default values for the new element. | | draggable | Includes drag'n'drop sorting. | | editable, deletable, addable | Enables/disables editing, deleting, and adding features. | | withImage | Shows the item's thumbnail or icon. | | other | Additional settings, such as toggles. | | limit | Maximum number of items. | | aspectRatio, setAspectRatio | Select an aspect ratio (if desired). | | hasRangeSelector, rangeSelectorOptions | Setting the range of values (e.g. number of repetitions). | | autoInterval | Auto Interval for Auto Scroll (if applicable). |

The rest of the components in the billgang/theme-lib/ui folder can be explored here

How to implement the renderBlock method:

This is where we pull our fields from the block settings

const {
	alignment,
	padding,
	cards,
	statistics,
	textHeader,
	textTypes,
	text,
	textTypesHeader,
	isActiveButtons,
	isActiveHeader,
	isActiveStatistics,
	isActiveText,
	isIcon,
	buttons,
	headerTypeFont,
	textTypeFont,
} = this.blockSettings;

And use it when rendering our block, but it's important to wrap everything in a Wrapper block and pass the block padding there

import { Wrapper } from '@billgangcom/theme-lib/ui'


export class NameBlock implements IBlock {
	//...

	renderBlock() {

		const {
		  //someSettings
		} = this.blockSettings

		return (
				<Wrapper padding={padding}>
					 {(adaptiveStyles) => {
						 return (
					//Here we render the UI taking into account adaptiveStyles
						 )
					 }}
				</Wrapper>
			)
	}
}

The adaptiveStyles parameter contains the current state of adaptivity: ‘desktop’ | ‘tablet’ | ‘mobile’. It is used to define how to render the block on different devices. For example:

{adaptiveStyles === 'mobile' ? <MobileHeader /> : <DesktopHeader />}

Example of ContentBlock implementation

import {
  BlockSettings,
  ColorVariables,
  hotReload,
  IBlock,
  iconNames,
  IconNames,
  PageContext,
  TypeFontKeys,
} from '@billgangcom/theme-lib'
import {
  Alignment,
  Button,
  ButtonsSettings,
  ButtonType,
  Icon,
  ItemBase,
  ItemsSettings,
  LayoutSettings,
  Link,
  Padding,
  TextSettings,
  TextType,
  Wrapper,
} from '@billgangcom/theme-lib/ui'

import styles from './index.module.scss'

import clx from 'classnames'
import React from 'react'
import { splitTextIntoSpans } from '@billgangcom/theme-lib'
import contentPreview from '../../assets/blocks/content.svg'
import { useBlockSettingSync } from '../../utils/useBlockSettingsSync'
import { ImagePreviewLoader } from '../../components/ImagePreviewLoader'

interface Cards extends ItemBase {
  description?: {
    'en-US': string
  }
  types?: TextType[]
  iconName?: IconNames
  typeFont?: TypeFontKeys
}
interface Statistics extends ItemBase {
  description?: {
    'en-US': string
  }
  types?: TextType[]
  typeFont?: TypeFontKeys
}

interface ContentSettings extends BlockSettings {
  alignment: Alignment
  padding: Record<Padding, number>
  isActiveHeader: boolean
  isActiveText: boolean
  textHeader: {
    'en-US': string
  }
  text: {
    'en-US': string
  }
  textTypesHeader: TextType[]
  textTypes: TextType[]
  cards: Cards[]
  isIcon: boolean
  isActiveButtons: boolean
  buttons: ButtonType[]
  isActiveStatistics: boolean
  statistics: Statistics[]
}

export class ContentBlock implements IBlock {
  blockSettings: ContentSettings
  blockColors: ColorVariables[]

  constructor(blockSettings?: ContentSettings, blockColors?: ColorVariables[]) {
    if (blockColors) {
      this.blockColors = blockColors
    } else {
      this.blockColors = [
        ColorVariables.BORDER_SECONDARY,
        ColorVariables.TEXT_PRIMARY,
        ColorVariables.TEXT_SECONDARY,
        ColorVariables.SURFACE_SECONDARY,
        ColorVariables.SURFACE_PRIMARY,
      ]
    }

    if (blockSettings) {
      this.blockSettings = {
        ...blockSettings,
      }
    } else {
      this.blockSettings = {
        displayName: 'Custom Content',
        isAddable: true,
        alignment: 'left',
        padding: {
          top: 10,
          left: 10,
          bottom: 10,
          right: 10,
        },
        isActiveHeader: true,
        isActiveText: true,
        textHeader: {
          'en-US': 'Active-Filled',
        },
        text: {
          'en-US': 'Active-Filled',
        },
        textTypesHeader: ['bold'],
        textTypes: [],
        cards: [],
        isIcon: true,
        isActiveButtons: true,
        buttons: [],
        isActiveStatistics: true,
        statistics: [],
        headerTypeFont: 'h2',
        textTypeFont: 'md',
      }
    }
  }

  renderPreview(): JSX.Element {
    return <ImagePreviewLoader image={contentPreview} />
  }

  renderBlock(
    _settings: Record<string, any>,
    _context: PageContext
  ): JSX.Element {
    const {
      alignment,
      padding,
      cards,
      statistics,
      textHeader,
      textTypes,
      text,
      textTypesHeader,
      isActiveButtons,
      isActiveHeader,
      isActiveStatistics,
      isActiveText,
      isIcon,
      buttons,
      headerTypeFont,
      textTypeFont,
    } = this.blockSettings

    return (
      <Wrapper padding={padding}>
        {(adaptiveStyles) => {
          return (
            <div className={styles.wrapper}>
              <div
                className={clx(styles.content, {
                  [styles.left]: alignment === 'left',
                  [styles.top]: alignment === 'top',
                  [styles.center]: alignment === 'center',
                  [styles.right]: alignment === 'right',
                  [styles.bottom]: alignment === 'bottom',
                  [styles.topLeft]: alignment === 'topLeft',
                  [styles.topRight]: alignment === 'topRight',
                  [styles.bottomLeft]: alignment === 'bottomLeft',
                  [styles.bottomRight]: alignment === 'bottomRight',
                })}>
                {adaptiveStyles === 'desktop' && cards.length > 0 && (
                  <div
                    className={clx(styles.cards, {
                      [styles.oneСard]: cards.length === 1,
                      [styles.twoСards]: cards.length === 2,
                      [styles.center]: alignment === 'center',
                      [styles.left]: alignment === 'left',
                      [styles.right]: alignment === 'right',
                      [styles.top]: alignment === 'top',
                      [styles.bottom]: alignment === 'bottom',
                      [styles.topLeft]: alignment === 'topLeft',
                      [styles.topRight]: alignment === 'topRight',
                      [styles.bottomLeft]: alignment === 'bottomLeft',
                      [styles.bottomRight]: alignment === 'bottomRight',
                    })}>
                    {cards.map((card) => (
                      <div className={styles.card} key={card.id}>
                        {isIcon && (
                          <div className={styles.icon}>
                            <Icon
                              name={card.iconName || 'ImageBroken'}
                              width={40}
                              height={40}
                            />
                          </div>
                        )}
                        <div className={styles.cardContent}>
                          <h5
                            id="h3"
                            className={clx(styles.title)}>
                            {card.name['en-US']}
                          </h5>
                          {card.description && (
                            <p
                              id={card.typeFont}
                              className={clx(
                                styles.description,
                                {
                                  [styles.bold]: card.types?.includes('bold'),
                                  [styles.italic]:
                                    card.types?.includes('italic'),
                                  [styles.underline]:
                                    card.types?.includes('underline'),
                                  [styles.strike]:
                                    card.types?.includes('strike-through'),
                                  [styles.understrike]:
                                    card.types?.includes('strike-through') &&
                                    card.types?.includes('underline'),
                                }
                              )}>
                              {card.description['en-US']}
                            </p>
                          )}
                        </div>
                      </div>
                    ))}
                  </div>
                )}
                <div className={styles.rightContent}>
                  <div
                    className={clx(styles.info, {
                      [styles.left]: alignment === 'left',
                      [styles.top]: alignment === 'top',
                      [styles.center]: alignment === 'center',
                      [styles.right]: alignment === 'right',
                      [styles.bottom]: alignment === 'bottom',
                      [styles.topLeft]: alignment === 'topLeft',
                      [styles.topRight]: alignment === 'topRight',
                      [styles.bottomLeft]: alignment === 'bottomLeft',
                      [styles.bottomRight]: alignment === 'bottomRight',
                    })}>
                    {isActiveHeader && (
                      <h2
                        id={
                          !textTypesHeader.includes('bold')
                            ? headerTypeFont
                            : `${headerTypeFont}-bold`
                        }
                        className={clx(styles.title, {
                          [styles.italic]: textTypesHeader.includes('italic'),
                          [styles.underline]:
                            textTypesHeader.includes('underline'),
                          [styles.strike]:
                            textTypesHeader.includes('strike-through'),
                          [styles.understrike]:
                            textTypesHeader.includes('strike-through') &&
                            textTypesHeader.includes('underline'),
                        })}>
                        {textHeader['en-US']}
                      </h2>
                    )}
                    {isActiveText && (
                      <p
                        id={
                          !textTypes.includes('bold')
                            ? textTypeFont
                            : `${textTypeFont}-bold`
                        }
                        className={clx(styles.text, {
                          [styles.italic]: textTypes.includes('italic'),
                          [styles.underline]: textTypes.includes('underline'),
                          [styles.strike]: textTypes.includes('strike-through'),
                          [styles.understrike]:
                            textTypes.includes('strike-through') &&
                            textTypes.includes('underline'),
                        })}>
                        {text['en-US']}
                      </p>
                    )}
                    {isActiveButtons && (
                      <div
                        style={{
                          flex: isActiveStatistics ? 1 : 0,
                        }}>
                        <div className={styles.buttons}>
                          {buttons.map((button) => (
                            <Link
                              path={
                                button.destination === 'Open Link' &&
                                button.link
                                  ? button.link
                                  : button.page === 'home'
                                  ? '/'
                                  : button.page
                              }
                              target={button.openInNewTab ? '_blank' : '_self'}
                              key={button.id}>
                              <Button
                                type={button.type}
                                className={clx({
                                  [styles.bold]:
                                    button.typesText.includes('bold'),
                                  [styles.italic]:
                                    button.typesText.includes('italic'),
                                  [styles.underline]:
                                    button.typesText.includes('underline'),
                                  [styles.strike]:
                                    button.typesText.includes('strike-through'),
                                  [styles.understrike]:
                                    button.typesText.includes(
                                      'strike-through'
                                    ) && button.typesText.includes('underline'),
                                })}>
                                {splitTextIntoSpans(button.text['en-US'])}
                              </Button>
                            </Link>
                          ))}
                        </div>
                      </div>
                    )}
                    {adaptiveStyles !== 'desktop' && cards.length > 0 && (
                      <div
                        className={clx(styles.cards, {
                          [styles.oneСard]: cards.length === 1,
                          [styles.twoСards]: cards.length === 2,
                          [styles.mobile]: adaptiveStyles === 'mobile',
                        })}>
                        {cards.map((card) => (
                          <div
                            className={clx(styles.card, {
                              [styles.mobile]: adaptiveStyles === 'mobile',
                            })}
                            key={card.id}>
                            {isIcon && (
                              <div className={styles.icon}>
                                <Icon
                                  name={card.iconName || 'ImageBroken'}
                                  width={40}
                                  height={40}
                                />
                              </div>
                            )}
                            <h5 id="h4" className={styles.title}>
                              {splitTextIntoSpans(card.name['en-US'])}
                            </h5>
                            {card.description && (
                              <p
                                id={card.typeFont}
                                className={clx(styles.description, {
                                  [styles.bold]: card.types?.includes('bold'),
                                  [styles.italic]:
                                    card.types?.includes('italic'),
                                  [styles.underline]:
                                    card.types?.includes('underline'),
                                  [styles.strike]:
                                    card.types?.includes('strike-through'),
                                  [styles.understrike]:
                                    card.types?.includes('strike-through') &&
                                    card.types?.includes('underline'),
                                })}>
                                {splitTextIntoSpans(card.description['en-US'])}
                              </p>
                            )}
                          </div>
                        ))}
                      </div>
                    )}
                    {isActiveStatistics && (
                      <div className={styles.statistics}>
                        {statistics.map((stat) => (
                          <div className={styles.stat} key={stat.id}>
                            <p id="md">
                              {splitTextIntoSpans(stat.name['en-US'])}
                            </p>
                            {stat.description && (
                              <h3
                                id={stat.typeFont}
                                className={clx({
                                  [styles.bold]: stat.types?.includes('bold'),
                                  [styles.italic]:
                                    stat.types?.includes('italic'),
                                  [styles.underline]:
                                    stat.types?.includes('underline'),
                                  [styles.strike]:
                                    stat.types?.includes('strike-through'),
                                  [styles.understrike]:
                                    stat.types?.includes('strike-through') &&
                                    stat.types?.includes('underline'),
                                })}>
                                {splitTextIntoSpans(stat.description['en-US'])}
                              </h3>
                            )}
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                </div>
              </div>
            </div>
          )
        }}
      </Wrapper>
    )
  }
  renderSettings(_context: PageContext): JSX.Element {
    const [alignment, setAlignment] = React.useState<Alignment>(
      this.blockSettings.alignment
    )

    const [padding, setPadding] = React.useState<Record<Padding, number>>(
      this.blockSettings.padding
    )

    const [isActiveHeader, setIsActiveHeader] = React.useState<boolean>(
      this.blockSettings.isActiveHeader
    )

    const [headerText, setHeaderText] = React.useState<string>(
      this.blockSettings.textHeader['en-US']
    )

    const [headerTypesText, setHeaderTypesText] = React.useState(
      this.blockSettings.textTypesHeader
    )

    const [isActiveText, setIsActiveText] = React.useState<boolean>(
      this.blockSettings.isActiveText
    )

    const [isIcon, setIsIcon] = React.useState<boolean>(
      this.blockSettings.isIcon
    )

    const [text, setText] = React.useState<string>(
      this.blockSettings.text['en-US']
    )

    const [typesText, setTypesText] = React.useState(
      this.blockSettings.textTypes
    )

    const [cards, setCards] = React.useState<Cards[]>(this.blockSettings.cards)

    const [isActiveButtons, setIsActiveButtons] = React.useState(
      this.blockSettings.isActiveButtons
    )

    const [isActiveStatistics, setIsActiveStatistics] = React.useState(
      this.blockSettings.isActiveStatistics
    )

    const [buttons, setButtons] = React.useState(this.blockSettings.buttons)
    const [statistics, setStatistics] = React.useState(
      this.blockSettings.statistics
    )

    const [headerTypeFont, setHeaderTypeFont] = React.useState(
      this.blockSettings.headerTypeFont
    )

    const [textTypeFont, setTextTypeFont] = React.useState(
      this.blockSettings.textTypeFont
    )

    useBlockSettingSync(
      this.blockSettings,
      'alignment',
      alignment,
      setAlignment
    )

    useBlockSettingSync(this.blockSettings, 'padding', padding, setPadding)

    useBlockSettingSync(
      this.blockSettings,
      'isActiveHeader',
      isActiveHeader,
      setIsActiveHeader
    )

    useBlockSettingSync(
      this.blockSettings,
      'textTypesHeader',
      headerTypesText,
      setHeaderTypesText
    )

    useBlockSettingSync(
      this.blockSettings,
      'isActiveText',
      isActiveText,
      setIsActiveText
    )

    useBlockSettingSync(this.blockSettings, 'isIcon', isIcon, setIsIcon)

    React.useEffect(() => {
      if (this.blockSettings.textHeader['en-US'] !== headerText) {
        setHeaderText(this.blockSettings.textHeader['en-US'])
      }
    }, [this.blockSettings.textHeader['en-US']])

    React.useEffect(() => {
      if (this.blockSettings.textHeader['en-US'] !== headerText) {
        this.blockSettings.textHeader['en-US'] = headerText
        hotReload()
      }
    }, [headerText])

    React.useEffect(() => {
      if (this.blockSettings.text['en-US'] !== text) {
        setText(this.blockSettings.text['en-US'])
      }
    }, [this.blockSettings.text['en-US']])

    React.useEffect(() => {
      if (this.blockSettings.text['en-US'] !== text) {
        this.blockSettings.text['en-US'] = text
        hotReload()
      }
    }, [text])

    useBlockSettingSync(
      this.blockSettings,
      'textTypes',
      typesText,
      setTypesText
    )

    useBlockSettingSync(this.blockSettings, 'cards', cards, setCards)

    useBlockSettingSync(
      this.blockSettings,
      'isActiveButtons',
      isActiveButtons,
      setIsActiveButtons
    )

    useBlockSettingSync(
      this.blockSettings,
      'isActiveStatistics',
      isActiveStatistics,
      setIsActiveStatistics
    )

    useBlockSettingSync(this.blockSettings, 'buttons', buttons, setButtons)

    useBlockSettingSync(
      this.blockSettings,
      'statistics',
      statistics,
      setStatistics
    )

    useBlockSettingSync(
      this.blockSettings,
      'headerTypeFont',
      headerTypeFont,
      setHeaderTypeFont
    )

    useBlockSettingSync(
      this.blockSettings,
      'textTypeFont',
      textTypeFont,
      setTextTypeFont
    )

    return (
      <div className={styles.settings}>
        <LayoutSettings
          alignment={alignment}
          padding={padding}
          setAlignment={setAlignment}
          setPadding={setPadding}
        />
        <ItemsSettings<Cards>
          items={cards}
          setItems={setCards}
          title="Cards"
          itemsType="input"
          itemsPlaceholder="Title"
          draggable={false}
          editable
          other={[
            {
              isItem: isIcon,
              setIsItem: setIsIcon,
              label: 'Icon',
            },
          ]}
          withImage
          modalOptions={[
            {
              type: 'text',
              label: 'Description',
              field: {
                type: 'types',
                text: 'description',
                typeFont: 'typeFont',
              },
            },
            {
              type: 'selectInfiniteList',
              label: 'Icon',
              field: 'iconName',
              itemsOptions: [...iconNames],
              leftAddonOptions: [
                ...iconNames.map((icon: (typeof iconNames)[number]) => ({
                  value: icon,
                  addon: <Icon name={icon} width={20} height={20} />,
                })),
              ],
            },
          ]}
          addableOptions={{
            description: {
              'en-US': '',
            },
            types: [],
            iconName: 'GameController',
            typeFont: 'md',
          }}
          limit={4}
        />

        <TextSettings
          title="Header"
          isActiveText={isActiveHeader}
          setIsActiveText={setIsActiveHeader}
          text={headerText}
          setText={setHeaderText}
          typesText={headerTypesText}
          setTypesText={setHeaderTypesText}
          typeFont={headerTypeFont}
          setTypeFont={setHeaderTypeFont}
        />
        <TextSettings
          title="Text"
          isActiveText={isActiveText}
          setIsActiveText={setIsActiveText}
          text={text}
          setText={setText}
          typesText={typesText}
          setTypesText={setTypesText}
          typeFont={textTypeFont}
          setTypeFont={setTextTypeFont}
        />
        <ButtonsSettings
          isActiveButtons={isActiveButtons}
          setIsActiveButtons={setIsActiveButtons}
          buttons={buttons}
          setButtons={setButtons}
        />
        <ItemsSettings<Statistics>
          isActiveItems={isActiveStatistics}
          limit={4}
          setIsActiveItems={setIsActiveStatistics}
          items={statistics}
          setItems={setStatistics}
          title="Statistics"
          itemsType="input"
          itemsPlaceholder="Title"
          draggable={false}
          modalOptions={[
            {
              type: 'text',
              label: 'Description',
              field: {
                type: 'types',
                text: 'description',
                typeFont: 'typeFont',
              },
            },
          ]}
          addableOptions={{
            description: {
              'en-US': '',
            },
            types: [],
            typeFont: 'h3',
          }}
          editable
        />
      </div>
    )
  }
}

useBlockSettingsSync is your util function that simply replaces useEffect:

import { BlockSettings, hotReload } from '@billgangcom/theme-lib';
import React from 'react';

function getNestedProperty(obj: any, path: string) {
	return path.split('.').reduce((acc, part) => acc && acc[part], obj);
}

function setNestedProperty(obj: any, path: string, value: any) {
	const parts = path.split('.');
	const last = parts.pop();
	const target = parts.reduce((acc, part) => acc && acc[part], obj);
	if (target && last) {
		target[last] = value;
	}
}

export function useBlockSettingSync(
	blockSettings: BlockSettings,
	property: string, // key, example: 'alignment' or 'key1.key2'
	localValue: any,
	setLocalValue: (val: any) => void,
) {
	// from blockSettings -> local state
	React.useEffect(() => {
		const blockValue = getNestedProperty(blockSettings, property);
		if (JSON.stringify(blockValue) !== JSON.stringify(localValue)) {
			setLocalValue(blockValue);
		}
	}, [getNestedProperty(blockSettings, property)]);

	// from local state -> blockSettings
	React.useEffect(() => {
		const blockValue = getNestedProperty(blockSettings, property);
		if (JSON.stringify(blockValue) !== JSON.stringify(localValue)) {
			setNestedProperty(blockSettings, property, localValue);
			hotReload();
		}
	}, [localValue]);
}