Skip to content

Instantly share code, notes, and snippets.

@ericzakariasson
Last active December 1, 2021 15:27
Show Gist options
  • Save ericzakariasson/3bb9abc9d0b7f65cd873573cac36cb38 to your computer and use it in GitHub Desktop.
Save ericzakariasson/3bb9abc9d0b7f65cd873573cac36cb38 to your computer and use it in GitHub Desktop.
useArray
import { act, renderHook } from "@testing-library/react-hooks";
import { useArray } from "./useArray";
describe("useArray", () => {
describe("props", () => {
it("should have initial state", () => {
const { result } = renderHook(() =>
useArray({
initialState: ["a", "b", "c"],
selector: (x) => x,
})
);
const [array] = result.current;
expect(array).toMatchInlineSnapshot(`
Array [
"a",
"b",
"c",
]
`);
});
});
describe("add", () => {
it("should add item ", () => {
const { result } = renderHook(() =>
useArray<string>({ selector: (x) => x })
);
expect(result.current[0]).toHaveLength(0);
act(() => {
result.current[1].add("a");
});
expect(result.current[0]).toHaveLength(1);
expect(result.current[0][0]).toBe("a");
});
});
describe("remove", () => {
it("should remove primitive item ", () => {
const { result } = renderHook(() =>
useArray<string>({
initialState: ["a"],
selector: (x) => x,
})
);
expect(result.current[0]).toHaveLength(1);
act(() => {
result.current[1].remove("a");
});
expect(result.current[0]).toHaveLength(0);
});
it("should remove complex item ", () => {
const { result } = renderHook(() =>
useArray<{ foo: string }>({
initialState: [{ foo: "bar" }],
selector: (x) => x.foo,
})
);
expect(result.current[0]).toHaveLength(1);
act(() => {
result.current[1].remove({ foo: "bar" });
});
expect(result.current[0]).toHaveLength(0);
});
});
describe("has", () => {
it("should yield true for primitive items", () => {
const { result } = renderHook(() =>
useArray<string>({
initialState: ["a"],
selector: (x) => x,
})
);
const [, { has }] = result.current;
expect(has("a")).toBe(true);
});
it("should yield false for primitive items ", () => {
const { result } = renderHook(() =>
useArray<string>({
initialState: ["a"],
selector: (x) => x,
})
);
const [, { has }] = result.current;
expect(has("b")).toBe(false);
});
it("should yield true for complex items", () => {
const { result } = renderHook(() =>
useArray({
initialState: [{ foo: { bar: "baz" } }],
selector: (x) => x.foo.bar,
})
);
const [, { has }] = result.current;
expect(has({ foo: { bar: "baz" } })).toBe(true);
});
it("should yield false for complex items ", () => {
const { result } = renderHook(() =>
useArray({
initialState: [{ foo: { bar: "baz" } }],
selector: (x) => x.foo.bar,
})
);
const [, { has }] = result.current;
expect(has({ foo: { bar: "foo" } })).toBe(false);
});
});
describe("reset", () => {
it("should reset to initial state", () => {
const { result } = renderHook(() =>
useArray({
initialState: ["a"],
selector: (x) => x,
})
);
expect(result.current[0]).toHaveLength(1);
act(() => {
result.current[1].add("b");
result.current[1].add("c");
});
expect(result.current[0]).toHaveLength(3);
act(() => {
result.current[1].reset();
});
expect(result.current[0]).toHaveLength(1);
expect(result.current[0][0]).toBe("a");
});
});
describe("clear", () => {
it("should clear state", () => {
const { result } = renderHook(() =>
useArray<string>({
initialState: ["a", "b", "c"],
selector: (x) => x,
})
);
expect(result.current[0]).toHaveLength(3);
act(() => {
result.current[1].clear();
});
expect(result.current[0]).toHaveLength(0);
});
});
describe("set", () => {
it("should set state", () => {
const { result } = renderHook(() =>
useArray<string>({
initialState: ["a", "b", "c"],
selector: (x) => x,
})
);
expect(result.current[0]).toMatchInlineSnapshot(`
Array [
"a",
"b",
"c",
]
`);
act(() => {
result.current[1].set(["d", "e", "f"]);
});
expect(result.current[0]).toMatchInlineSnapshot(`
Array [
"d",
"e",
"f",
]
`);
});
});
});
import { useState } from "react";
type Primitive = string | number | boolean;
type Selector<T> = (item: T) => Primitive;
const isPrimitive = (value: unknown): value is Primitive => {
return ["string", "number", "boolean"].includes(typeof value);
};
const primitiveSelector: Selector<unknown> = (item) => {
if (!isPrimitive(item)) {
throw new Error(`${typeof item} is not a primitive type`);
}
return item;
};
const getArrayHelpers = <T>(selector: Selector<T>) => ({
add: (array: T[], item: T) => array.concat(item),
remove: (array: T[], item: T) =>
array.filter((i) => selector(i) !== selector(item)),
has: (array: T[], item: T) =>
array.some((i) => selector(i) === selector(item)),
toggle: (array: T[], item: T) => {
const helpers = getArrayHelpers(selector);
if (helpers.has(array, item)) {
return helpers.remove(array, item);
}
return helpers.add(array, item);
},
});
type UseArrayProps<T> = {
initialState?: T[];
selector?: T extends Primitive ? never : Selector<T>;
};
type UseArrayActions<T> = {
add: (item: T) => void;
remove: (item: T) => void;
has: (item: T) => boolean;
set: (items: T[]) => void;
toggle: (item: T) => void;
reset: () => void;
clear: () => void;
};
type UseArrayValue<T> = [T[], UseArrayActions<T>];
export const useArray = <T>({
initialState,
selector: customSelector,
}: UseArrayProps<T> = {}): UseArrayValue<T> => {
const [array, setArray] = useState<T[]>(initialState ?? []);
const selector = customSelector ?? primitiveSelector;
const helpers = getArrayHelpers(selector);
const add = (item: T) => setArray((prev) => helpers.add(prev, item));
const remove = (item: T) => setArray((prev) => helpers.remove(prev, item));
const has = (item: T) => helpers.has(array, item);
const toggle = (item: T) => setArray((prev) => helpers.toggle(prev, item));
const reset = () => setArray(initialState ?? []);
const clear = () => setArray([]);
const set = (items: T[]) => setArray(items);
return [array, { add, remove, has, toggle, reset, clear, set }];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment