Skip to content

Commit e3d8198

Browse files
authored
Add useConstructor hook (#11)
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. Closes #7
1 parent 450b287 commit e3d8198

4 files changed

Lines changed: 85 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
10+
### Added
11+
12+
- `useConstructor` hook that executes a provided callback only once during the component's lifecycle, similar to a constructor.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React, { useState } from "react";
2+
import { useConstructor } from "../../src/lib/hooks/useConstructor";
3+
4+
describe("useConstructor", () => {
5+
it("calls the callback exactly once per mount, even after re-renders", () => {
6+
const initSpy = cy.spy().as("initSpy");
7+
8+
const TestComponent: React.FC = () => {
9+
const [count, setCount] = useState(0);
10+
useConstructor(() => {
11+
initSpy();
12+
});
13+
14+
return (
15+
<div>
16+
<span data-cy="count">{count}</span>
17+
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
18+
</div>
19+
);
20+
};
21+
22+
// Turn off StrictMode as it will double‑invoke mount logic
23+
cy.mount(<TestComponent />, { strict: false });
24+
25+
cy.get("@initSpy").should("have.been.calledOnce");
26+
27+
// Cause several re-renders
28+
cy.contains("Increment").click().click().click();
29+
cy.get("[data-cy='count']").should("have.text", "3");
30+
31+
cy.get("@initSpy").should("have.been.calledOnce");
32+
});
33+
34+
it("runs again after an unmount/remount (new instance)", () => {
35+
const initSpy = cy.spy().as("initSpy");
36+
37+
const Wrapper: React.FC = () => {
38+
const [show, setShow] = useState(true);
39+
return (
40+
<div>
41+
<button onClick={() => setShow((s) => !s)}>Toggle</button>
42+
{show && <Child />}
43+
</div>
44+
);
45+
};
46+
47+
const Child: React.FC = () => {
48+
useConstructor(() => initSpy());
49+
return <div data-cy="child">Child</div>;
50+
};
51+
52+
// Turn off StrictMode as it will double‑invoke mount logic
53+
cy.mount(<Wrapper />, { strict: false });
54+
55+
cy.get("@initSpy").should("have.been.calledOnce");
56+
57+
cy.contains("Toggle").click(); // unmount
58+
cy.get("[data-cy='child']").should("not.exist");
59+
60+
cy.contains("Toggle").click(); // remount
61+
cy.get("[data-cy='child']").should("exist");
62+
63+
cy.get("@initSpy").should("have.callCount", 2);
64+
});
65+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from "./lib/hooks/useConstructor";
12
export * from "./lib/hooks/useHelloWorld";

src/lib/hooks/useConstructor.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useRef } from "react";
2+
3+
/**
4+
* Custom hook that runs the provided callback only once during the component's lifecycle.
5+
* @param callback - Function to be executed once on component mount.
6+
*/
7+
const useConstructor = (callback: () => void) => {
8+
const calledRef = useRef(false);
9+
if (!calledRef.current) {
10+
callback();
11+
calledRef.current = true;
12+
}
13+
};
14+
15+
export { useConstructor };

0 commit comments

Comments
 (0)