Skip to content

Commit

Permalink
feat: unregister house components on unmount
Browse files Browse the repository at this point in the history
  • Loading branch information
ReidyT committed Dec 4, 2024
1 parent 7cf6072 commit 1b990a0
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 18 deletions.
14 changes: 14 additions & 0 deletions src/context/HouseComponentsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type HouseComponentsContextType = {
houseComponentsConfigurator: HouseComponentsConfigurator;
numberOfFloors: number;
registerComponent: (params: RegisterComponentParams) => void;
unregisterComponent: ({
componentId,
}: Pick<RegisterComponentParams, 'componentId'>) => void;
changeComponentInsulation: <
T extends HouseComponent,
K extends keyof (typeof HouseInsulationPerComponent)[T],
Expand Down Expand Up @@ -109,6 +112,15 @@ export const HouseComponentsProvider = ({ children }: Props): ReactNode => {
[houseComponentsConfigurator],
);

const unregisterComponent = useCallback(
({ componentId }: Pick<RegisterComponentParams, 'componentId'>): void => {
setHouseComponentsConfigurator((curr) =>
curr.cloneWithout({ componentId }),
);
},
[],
);

const changeComponentInsulation = useCallback(
<
T extends HouseComponent,
Expand Down Expand Up @@ -200,6 +212,7 @@ export const HouseComponentsProvider = ({ children }: Props): ReactNode => {
houseComponentsConfigurator,
numberOfFloors,
registerComponent,
unregisterComponent,
changeComponentInsulation,
updateCompositionOfInsulation,
updateNumberOfFloors,
Expand All @@ -208,6 +221,7 @@ export const HouseComponentsProvider = ({ children }: Props): ReactNode => {
houseComponentsConfigurator,
numberOfFloors,
registerComponent,
unregisterComponent,
changeComponentInsulation,
updateCompositionOfInsulation,
updateNumberOfFloors,
Expand Down
43 changes: 43 additions & 0 deletions src/models/HouseComponentsConfigurator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,49 @@ describe('HouseComponentsConfigurator', () => {
);
});

it('should remove component', () => {
const houseComponents = HouseComponentsConfigurator.create()
.cloneWith({
componentId: 'wall1',
component: WALL_COMPONENT_INSULATION,
})
.cloneWith({
parentId: 'wall1',
componentId: 'window1',
component: WINDOW_COMPONENT_INSULATION,
})
.cloneWithout({ componentId: 'window1' });

expect(houseComponents.getAll().length).eq(1);
expect(houseComponents.getAll()[0].houseComponentId).eq('wall1');
});

it('should remove component and its children', () => {
const houseComponents = HouseComponentsConfigurator.create()
.cloneWith({
componentId: 'wall1',
component: WALL_COMPONENT_INSULATION,
})
.cloneWith({
parentId: 'wall1',
componentId: 'window1',
component: WINDOW_COMPONENT_INSULATION,
})
.cloneWith({
parentId: 'wall1',
componentId: 'window2',
component: WINDOW_COMPONENT_INSULATION,
})
.cloneWith({
parentId: 'window2',
componentId: 'window3',
component: WINDOW_COMPONENT_INSULATION,
})
.cloneWithout({ componentId: 'wall1' });

expect(houseComponents.getAll().length).eq(0);
});

it('should get a component', () => {
const houseComponents = HouseComponentsConfigurator.create()
.cloneWith({ componentId: 'wall1', component: WALL_COMPONENT_INSULATION })
Expand Down
90 changes: 74 additions & 16 deletions src/models/HouseComponentsConfigurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,39 @@ type HouseComponentInsulationResult = HouseComponentInsulation & {
houseComponentId: string;
};

// Helpful aliases
type ComponentId = string;
type ChildrenId = string;
type ParentId = string;

/**
* Manages a tree-like structure of house component insulations, ensuring immutability for efficient React updates.
*/
export class HouseComponentsConfigurator {
/**
* A map storing all house component insulations, keyed by their unique ID.
*/
private readonly components: Map<string, HouseComponentInsulation> =
private readonly components: Map<ComponentId, HouseComponentInsulation> =
new Map();

/**
* A map storing the parent ID of each component.
* It is useful to know which wall the window is associated with.
*/
private readonly parentComponentIds: Map<string, string> = new Map();
private readonly componentParents: Map<ChildrenId, ParentId> = new Map();

/**
* Private constructor; instances should be created using the `create()` factory method.
* This allows to abstract the internal structure of the Class and to faciliate the instantiation of immutable object.
* @param initialComponents An optional initial set of components.
* @param initialParentComponentIds An optional initial set of parent-child relationships.
* @param initialComponentParents An optional initial set of child-parent relationships.
*/
private constructor(
initialComponents?: Map<string, HouseComponentInsulation>,
initialParentComponentIds?: Map<string, string>,
initialComponentParents?: Map<string, string>,
) {
this.components = initialComponents || new Map();
this.parentComponentIds = initialParentComponentIds || new Map();
this.componentParents = initialComponentParents || new Map();
}

public static create(): HouseComponentsConfigurator {
Expand Down Expand Up @@ -67,9 +72,9 @@ export class HouseComponentsConfigurator {
const newComponents = new Map(this.components);
newComponents.set(componentId, component);

const newParentComponentIds = new Map(this.parentComponentIds);
const newComponentParents = new Map(this.componentParents);

const currentParentId = newParentComponentIds.get(componentId);
const currentParentId = newComponentParents.get(componentId);

if (currentParentId && currentParentId !== parentId) {
throw new Error(
Expand All @@ -78,15 +83,67 @@ export class HouseComponentsConfigurator {
}

if (parentId) {
newParentComponentIds.set(componentId, parentId);
newComponentParents.set(componentId, parentId);
} else {
newParentComponentIds.delete(componentId);
newComponentParents.delete(componentId);
}

return new HouseComponentsConfigurator(
newComponents,
newParentComponentIds,
);
return new HouseComponentsConfigurator(newComponents, newComponentParents);
}

/**
* Creates a new `HouseComponentsConfigurator` instance with a specific component and its children removed. The original instance remains unchanged. This method is designed to support immutable updates in React applications.
* @param componentId The unique ID of the component.
* @returns A new `HouseComponentsConfigurator` instance with the component added or updated.
*/
public cloneWithout({
componentId,
}: {
componentId: string;
}): HouseComponentsConfigurator {
const newComponents = new Map(this.components);
newComponents.delete(componentId);

const newComponentParents = new Map(this.componentParents);
newComponentParents.delete(componentId);

this.removeComponent({
componentId,
components: newComponents,
componentParents: newComponentParents,
});

return new HouseComponentsConfigurator(newComponents, newComponentParents);
}

/**
* Recursively removes a component and its children from the provided component and parent maps.
*
* @param componentId - The ID of the component to remove.
* @param components - The map of components.
* @param componentParents - The map of child-parent relationships.
*/
private removeComponent({
componentId,
components,
componentParents,
}: {
componentId: string;
components: Map<ComponentId, HouseComponentInsulation>;
componentParents: Map<ChildrenId, ParentId>;
}): void {
Array.from(componentParents.entries()).forEach(([childId, parentId]) => {
if (parentId === componentId) {
componentParents.delete(childId);
components.delete(childId);

this.removeComponent({
componentId: childId,
componentParents,
components,
});
}
});
}

/**
Expand Down Expand Up @@ -130,17 +187,17 @@ export class HouseComponentsConfigurator {

return new HouseComponentsConfigurator(
new Map(newComponents),
new Map(this.parentComponentIds),
new Map(this.componentParents),
);
}

/**
* Retrieves all child components of a given parent component.
* Retrieves all children components of a given parent component.
* @param parentId The ID of the parent component.
* @returns An array of child components. Returns an empty array if no children are found or the parent doesn't exist.
*/
private getChildren(parentId: string): HouseComponentInsulation[] {
return Array.from(this.parentComponentIds.entries())
return Array.from(this.componentParents.entries())
.filter(([_, v]) => v === parentId)
.map(([k, _]) => this.components.get(k))
.filter((c): c is HouseComponentInsulation => Boolean(c));
Expand All @@ -160,6 +217,7 @@ export class HouseComponentsConfigurator {
}

const children = this.getChildren(componentId);

const totalChildrenArea = children.reduce(
(acc, comp) => acc + comp.actualArea,
0,
Expand Down
3 changes: 2 additions & 1 deletion src/modules/models/House/ResidentialHouse/Wall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const WallComponent = ({
wallProps: WallProps;
}): JSX.Element => {
const { heatLosses } = useSimulation();
const { registerComponent } = useHouseComponents();
const { registerComponent, unregisterComponent } = useHouseComponents();
const heatLoss = heatLosses[id] ?? 0;

const material = useWallMaterial({ wallMaterial: materials.Wall });
Expand All @@ -38,6 +38,7 @@ const WallComponent = ({
componentType: HouseComponent.Wall,
});

return () => unregisterComponent({ componentId: id });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down
4 changes: 3 additions & 1 deletion src/modules/models/House/ResidentialHouse/WindowFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const WindowFrame = ({
}: Props): JSX.Element => {
const id = `${wallId}-Window-${windowIdx}`;
const { heatLosses } = useSimulation();
const { registerComponent } = useHouseComponents();
const { registerComponent, unregisterComponent } = useHouseComponents();
const { frameMaterial } = useWindowMaterial({
windowMaterial: materials.Wood,
});
Expand All @@ -41,6 +41,8 @@ export const WindowFrame = ({
size: getComponentSize(nodes.WindowFrame_2.geometry, windowScaleSize),
componentType: HouseComponent.Window,
});

return () => unregisterComponent({ componentId: id });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowScaleSize]);

Expand Down

0 comments on commit 1b990a0

Please sign in to comment.