From d122f9f2e4c7ed4adea89f4aeeaaefaf5fe97e5b Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Wed, 8 Oct 2025 14:25:13 +0200 Subject: [PATCH 1/2] Adds useConstructor hook Introduces a `useConstructor` hook that executes a provided callback only once during the component's lifecycle, similar to a constructor. This ensures that initialization logic is performed only once, even after re-renders or component remounts. Includes thorough tests to confirm the hook's behavior in different scenarios. --- cypress/component/useConstructor.cy.tsx | 65 +++++++++++++++++++++++++ src/index.ts | 1 + src/lib/hooks/useConstructor.ts | 15 ++++++ 3 files changed, 81 insertions(+) create mode 100644 cypress/component/useConstructor.cy.tsx create mode 100644 src/lib/hooks/useConstructor.ts diff --git a/cypress/component/useConstructor.cy.tsx b/cypress/component/useConstructor.cy.tsx new file mode 100644 index 0000000..429618f --- /dev/null +++ b/cypress/component/useConstructor.cy.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; +import { useConstructor } from "../../src/lib/hooks/useConstructor"; + +describe("useConstructor", () => { + it("calls the callback exactly once per mount, even after re-renders", () => { + const initSpy = cy.spy().as("initSpy"); + + const TestComponent: React.FC = () => { + const [count, setCount] = useState(0); + useConstructor(() => { + initSpy(); + }); + + return ( +
+ {count} + +
+ ); + }; + + // Turn off StrictMode as it will double‑invoke mount logic + cy.mount(, { strict: false }); + + cy.get("@initSpy").should("have.been.calledOnce"); + + // Cause several re-renders + cy.contains("Increment").click().click().click(); + cy.get("[data-cy='count']").should("have.text", "3"); + + cy.get("@initSpy").should("have.been.calledOnce"); + }); + + it("runs again after an unmount/remount (new instance)", () => { + const initSpy = cy.spy().as("initSpy"); + + const Wrapper: React.FC = () => { + const [show, setShow] = useState(true); + return ( +
+ + {show && } +
+ ); + }; + + const Child: React.FC = () => { + useConstructor(() => initSpy()); + return
Child
; + }; + + // Turn off StrictMode as it will double‑invoke mount logic + cy.mount(, { strict: false }); + + cy.get("@initSpy").should("have.been.calledOnce"); + + cy.contains("Toggle").click(); // unmount + cy.get("[data-cy='child']").should("not.exist"); + + cy.contains("Toggle").click(); // remount + cy.get("[data-cy='child']").should("exist"); + + cy.get("@initSpy").should("have.callCount", 2); + }); +}); diff --git a/src/index.ts b/src/index.ts index 7d1517e..e89e661 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ +export * from "./lib/hooks/useConstructor"; export * from "./lib/hooks/useHelloWorld"; diff --git a/src/lib/hooks/useConstructor.ts b/src/lib/hooks/useConstructor.ts new file mode 100644 index 0000000..1a19f88 --- /dev/null +++ b/src/lib/hooks/useConstructor.ts @@ -0,0 +1,15 @@ +import { useRef } from "react"; + +/** + * Custom hook that runs the provided callback only once during the component's lifecycle. + * @param callback - Function to be executed once on component mount. + */ +const useConstructor = (callback: () => void) => { + const calledRef = useRef(false); + if (!calledRef.current) { + callback(); + calledRef.current = true; + } +}; + +export { useConstructor }; From 8ce9787062d0780c549d01af3078506b17cacc15 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Wed, 8 Oct 2025 14:28:27 +0200 Subject: [PATCH 2/2] add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11bddf3..b1dd472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added + +- `useConstructor` hook that executes a provided callback only once during the component's lifecycle, similar to a constructor.