Components Separation
This section provides an overview of component separation patterns.
highlighting the main differences in implementation between the old pattern and the new pattern:
Old Code Pattern
Component Structure
Mixed Component structure depending on the state from its outer scope.
import {
IOptionPickerColorItem,
OptionPicker,
OPTION_PICKER_TYPE_COLOR,
} from "@vfde-brix/ws10/option-picker";
import { StateProps } from "app/container/Options/interface";
import { mountOptionPicker } from "./OptionPicker";
import { OptionsActionDispatchers } from "app/container/Options/slice";
/**
* Mount the color picker
*/
const mountColorOptionPicker = (
containerId: string,
onChangeAction?: OptionsActionDispatchers["changeColor"]
): OptionPicker | null => {
const items: IOptionPickerColorItem[] = [];
const container = document.getElementById(containerId);
const onChange = (event: Event) => {
onChangeAction &&
onChangeAction((event.target as HTMLInputElement).value);
};
return (
container &&
mountOptionPicker(
container,
"option-picker-color",
OPTION_PICKER_TYPE_COLOR,
"option-picker-color",
items,
onChange
)
);
};
/**
* Convert colors from API to color option picker items
*/
export const convertColorsFromApiToColorOptionPickerItems = ({
currentColor,
availableColors = [],
}: StateProps): IOptionPickerColorItem[] | undefined => {
const items: IOptionPickerColorItem[] = [];
for (const color of availableColors!) {
let optChecked = false;
if (currentColor?.displayLabel === color.displayLabel) {
optChecked = true;
}
items.push({
// We need to encode the value, because the API operates with HTML entities
// (e. g. 'Grün' instead of 'GrĂ¼n'). It's decoded in the reducer.
stdValue: encodeURIComponent(color.displayLabel),
stdPrimaryLabel: color.displayLabel,
stdColor: `rgb(${color.primaryColorRgb})`,
optChecked,
});
}
return items;
};
export default mountColorOptionPicker;
Feature Entry structure
Landing all components + Performing heavy state manipulation for all components.
Mount Component
// Each Brix component needs to be mounted first
// This involves selecting elements using JS and passing them to createComponent Function
injectSaga(optionsSlice.name, optionsSaga);
const { setDefaultState, changeColor, changeCapacity, toggleAccordion } = actions;
// Set atomic Id in the default state
setDefaultState(getAtomicId());
const deliveryDateIconText = mountIconText(DELIVERY_DATE_CONTAINER_ID);
const colorOptionPicker = mountColorOptionPicker(
COLOR_OPTION_PICKER_CONTAINER_ID,
changeColor
);
const capacityOptionPicker = mountCapacityOptionPicker(
CAPACITY_OPTION_PICKER_CONTAINER_ID,
changeCapacity
);
const textHeader = mountTextHeader(TEXT_HEADER_CONTAINER_ID);
State Derivation
// getDerivedStateFromProps is used to listen for store state changes
// Runs logic when state values change, like toggling component visibility
return {
getDerivedStateFromProps(newState: StateProps, oldState: StateProps) {
const {
technicalDetails,
deliveryScope,
availableColors,
availableCapacities,
capacitiesForColor,
currentCapacity,
images,
cellular,
shippingInfo,
deviceName,
} = newState;
textHeader &&
deviceName &&
updateTextHeader(textHeader, deviceName);
technicalDetailsAcordion =
mountTechnicalDetailsAccordion(toggleAccordion);
if (
technicalDetails &&
technicalDetails !== oldState.technicalDetails
) {
technicalDetailsAcordion &&
updateTechnicalDetailsAccordion(
technicalDetailsAcordion,
technicalDetails,
deliveryScope!
);
}
if (availableColors !== oldState.availableColors) {
colorOptionPicker?.update({
items: convertColorsFromApiToColorOptionPickerItems(
newState
),
});
}
},
};
State Mapping
// mapStateToProps maps state to selectors
// Example: creating selectIsLoadingFunction for isLoading state
const mapStateToProps = createStructuredSelector<
RootState<IInitialStateOptions & IInitialStateApp>,
StateProps
>({
devicePayload: selectDevicePayload(),
availableColors: selectColors(),
availableCapacities: selectCapacities(),
currentColor: selectCurrentColor(),
currentCapacity: selectCurrentCapacity(),
capacitiesForColor: selectCapacitiesForColor(),
images: selectImages(),
atomicId: selectAtomicId(),
technicalDetails: selectTechnicalDetails(),
shippingInfo: selectShippingInfo(),
deliveryScope: selectDeliveryScope(),
deviceName: selectDeviceName(),
cellular: selectCellular(),
});
const mountOptionsContainer = connect(
mapStateToProps,
optionsActionDispatchers
)(Options);
export default mountOptionsContainer;
Key Functions
InjectReducer: Adds new slices to the store (initial empty store creation)InjectSaga: Injects main saga for application logic initializationgetDerivedStateFromProps: Listens for and handles store state changesmapStateToProps: Maps state to selectors for components
New Code Pattern
New Component Structure
Standalone Component
Creating standalone self-state managed components with Redux Toolkit Listeners.
import {
IOptionPickerColorItem,
OPTION_PICKER_TYPE_COLOR,
OptionPicker,
} from "@vfde-brix/ws10/option-picker";
import { startAppListening } from "../../../app/listener";
import { RootState, useAppDispatch } from "../../../app/store";
import { mountOptionPicker } from "../../../components/OptionPicker";
import { selectColorOptions } from "../selectors";
import { changeColor } from "../slice";
import { convertColorsFromApiToColorOptionPickerItems } from "../helpers/convertColorHelper";
/**
* Mount the color picker
*/
export const mountColorOptionPicker = (
containerId: string
): OptionPicker | null => {
const dispatch = useAppDispatch();
const onChange = (event: Event) => {
dispatch(changeColor((event.target as HTMLInputElement).value));
};
const items: IOptionPickerColorItem[] = [];
const optionPicker = mountOptionPicker(containerId, {
stdName: "option-picker-color",
optType: OPTION_PICKER_TYPE_COLOR,
items,
stdScreenreaderLegend: "option-picker-color",
business: {
onChange,
},
});
/* istanbul ignore if */
if (!optionPicker) {
return null;
}
listenForUpdates(optionPicker);
return optionPicker;
};
const listenForUpdates = (optionPicker: OptionPicker) => {
startAppListening({
predicate: (_action, currentState, previousState) =>
selectColorOptions(currentState) !==
selectColorOptions(previousState),
effect: (_action, listenerApi) => {
const state = listenerApi.getState();
updateColors(state, optionPicker);
},
});
};
const updateColors = (state: RootState, optionPicker: OptionPicker) => {
const items = convertColorsFromApiToColorOptionPickerItems(state);
optionPicker.update({ items });
};
Shared Components
Developing shared components for use across the application.
import {
createOptionPicker,
IOptionPickerBusinessLogic,
IOptionPickerItem,
IOptionPickerProperties,
OptionPicker,
} from "@vfde-brix/ws10/option-picker";
/**
* Mount the option picker
*/
export const mountOptionPicker = (
containerId: string,
businessLogicOrProperties:
| IOptionPickerBusinessLogic
| IOptionPickerProperties<IOptionPickerItem>
): OptionPicker | null => {
const container = document.getElementById(containerId);
/* istanbul ignore if */
if (!container) {
return null;
}
return createOptionPicker(container, businessLogicOrProperties);
};
/**
* Update optionPicker items
* @param optionPicker the option picker
* @param items the items of the option picker
*/
export const updateOptionPickerItems = (
optionPicker: OptionPicker,
items: IOptionPickerItem[]
) => {
const optionPickerProperties = optionPicker.getProperties();
optionPicker.update({ ...optionPickerProperties, items });
};
Functionality Separated into Helpers
Isolating the component functionality into reusable and testable helper files.
import { RootState } from "../../../app/store";
import { selectColorOptions, selectCurrentColor } from "../selectors";
import { IOptionPickerColorItem } from "@vfde-brix/ws10/option-picker";
/**
* Convert colors from API to color option picker items
*/
export const convertColorsFromApiToColorOptionPickerItems = (
state: RootState
): IOptionPickerColorItem[] => {
const colorOptions = selectColorOptions(state);
const currentColor = selectCurrentColor(state);
const items: IOptionPickerColorItem[] = [];
if (!colorOptions) {
return items;
}
for (const color of colorOptions) {
items.push({
stdValue: encodeURIComponent(color.displayLabel),
stdPrimaryLabel: color.displayLabel,
stdColor: `rgb(${color.primaryColorRgb})`,
optChecked: currentColor?.displayLabel === color.displayLabel,
});
}
return items;
};
New Feature Entry structure
Clean Simplified version of the feature entry point.
import { useAppDispatch } from "../../app/store";
import "./style.scss";
import initTariff from "../Tariff";
import { mountTechnicalDetailsAccordion } from "./components/AccordionForTechnicalDetails";
import { mountDeliveryDateIconText } from "./components/IconTextForDeliveryDate";
import { prepareImageGallery } from "./components/ImageGallery";
import { mountCapacityOptionPicker } from "./components/OptionPickerForCapacity";
import { mountColorOptionPicker } from "./components/OptionPickerForColor";
import { mountTextHeader } from "./components/TextHeader";
import { getAtomicId } from "./helpers/getAtomicId";
import { startListeners } from "./listeners";
import { setDefaultState } from "./slice";
import {
CAPACITY_OPTION_PICKER_CONTAINER_ID,
COLOR_OPTION_PICKER_CONTAINER_ID,
DELIVERY_DATE_CONTAINER_ID,
IMAGE_GALLERY_CONTAINER_ID,
TECHNICAL_DETAILS_ACCORDION_CONTAINER_ID,
TEXT_HEADER_CONTAINER_ID,
} from "./constants";
/**
* Init options
*/
export const initOptions = () => {
startListeners();
const dispatch = useAppDispatch();
dispatch(setDefaultState(getAtomicId()));
mountTextHeader(TEXT_HEADER_CONTAINER_ID);
prepareImageGallery(IMAGE_GALLERY_CONTAINER_ID);
mountColorOptionPicker(COLOR_OPTION_PICKER_CONTAINER_ID);
mountCapacityOptionPicker(CAPACITY_OPTION_PICKER_CONTAINER_ID);
mountDeliveryDateIconText(DELIVERY_DATE_CONTAINER_ID);
mountTechnicalDetailsAccordion(TECHNICAL_DETAILS_ACCORDION_CONTAINER_ID);
initTariff();
};
New Key Functions
startAppListening: Initializes listeners for handling side effectsuseAppDispatch: Hook to dispatch actions within componentscreateOptionPicker: Creates an option picker componentupdateOptionPickerItems: Updates items in the option picker componentconvertColorsFromApiToColorOptionPickerItems: Converts API color data to option picker items
Component State Management:
In the new pattern, listeners are used in the standalone component file to
handle side effects and update the state based on state changes. This
approach replaces the old pattern, which relied on the
getDerivedStateFromProps function to update the state.