@billgangcom/theme-lib
v1.126.0
Published
Package Installation:
Readme
Package Installation:
npm i @billgangcom/theme-libor
pnpm i @billgangcom/theme-libIn 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:
- blocks - is a blocks object in the format key - value, where key is the block name and value is the block class itself
- settings - a separate block for settings
- header - a separate block for header
- footer - a separate block for footer
- 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
- 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;
}- 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
- 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.
- we initialise the states of the settings we need using React.useState
const [isActiveHeader, setIsActiveHeader] = React.useState<boolean>(
this.blockSettings.isActiveHeader,
);- 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:
- add, delete and edit items;
- drag'n'drop sorting;
- using different input types (text, select, image, etc.);
- connecting additional modal settings for each element;
- 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]);
}