(react) add useControllableState hook

This hook is used to create a state that can be controlled by the parent.
If not the state is handled internally. We start to have this redundant
use case across Cunningham, so creating a dedicated hook reduces the
components verbosity and complexity.
This commit is contained in:
Nathan Vasse
2023-12-20 11:01:24 +01:00
committed by NathanVss
parent 3dd7b3ef8e
commit 6d91c1d19f
3 changed files with 119 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useControllableState } from ":/hooks/useControllableState";
import { Button } from ":/components/Button";
describe("useControllableState", () => {
const TestComponent = (props: {
value?: string;
setValue?: (value: string) => void;
}) => {
const [value, setValue] = useControllableState(
"default",
props.value,
props.setValue,
);
return (
<div>
<div>Controllable value: {value}|</div>
<Button onClick={() => setValue("custom value")}>Set value</Button>
</div>
);
};
it("works non controlled", async () => {
render(<TestComponent />);
screen.getByText("Controllable value: default|");
// Change internal state.
const $button = screen.getByRole("button", { name: "Set value" });
await userEvent.click($button);
screen.getByText("Controllable value: custom value|");
});
it("work controlled", async () => {
const Wrapper = () => {
const [value, setValue] = React.useState("controlled value");
return (
<>
<TestComponent value={value} setValue={setValue} />
<Button onClick={() => setValue("new controlled value")}>
Set controlled value
</Button>
<div>Controlled value: {value}|</div>
</>
);
};
render(<Wrapper />);
const user = userEvent.setup();
// Default value is controlled.
screen.getByText("Controllable value: controlled value|");
screen.getByText("Controlled value: controlled value|");
// Set value from parent.
const $button = screen.getByRole("button", {
name: "Set controlled value",
});
await user.click($button);
screen.getByText("Controllable value: new controlled value|");
screen.getByText("Controlled value: new controlled value|");
// Set value from child.
const $buttonNested = screen.getByRole("button", {
name: "Set value",
});
await user.click($buttonNested);
screen.getByText("Controllable value: custom value|");
screen.getByText("Controlled value: custom value|");
});
});

View File

@@ -0,0 +1,39 @@
import React, { useEffect } from "react";
/**
* This hook is used to create a state that can be controlled by the parent. If not the state is handled internally.
*
* @param defaultValue if not controlled by the parent, this is the default value
* @param propsValue if controlled by the parent, this is the value
* @param propsCallback if controlled by the parent, this is the callback to call when the value changes
*/
export const useControllableState = <T>(
defaultValue: T,
propsValue?: T,
propsCallback?: (value: T) => void,
): [T, (value: T) => void] => {
const [state, setState] = React.useState(
typeof propsValue === "undefined" ? defaultValue : propsValue,
);
// Bottom-Up.
const onChange = (value: T) => {
if (propsCallback) {
propsCallback(value);
} else {
setState(value);
}
};
// Top-Down.
useEffect(() => {
if (!propsCallback) {
return;
}
if (typeof propsValue !== "undefined" && propsValue !== state) {
setState(propsValue);
}
}, [propsValue]);
return [state, onChange];
};