Skip to content

Commit

Permalink
refactor zodRefine, add hooks lint, refactor hooks.
Browse files Browse the repository at this point in the history
  • Loading branch information
AkifumiSato committed Sep 8, 2023
1 parent 4b9a393 commit d26d50b
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 41 deletions.
19 changes: 9 additions & 10 deletions apps/example-next/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
"use client";

import { useLocationState, StoreName } from "@location-state/core";
import { z } from "zod";
import { useLocationState, StoreName, Refine } from "@location-state/core";
import { z, ZodType } from "zod";

const schema = z.number();
const zodRefine =
<T extends unknown>(schema: ZodType<T>): Refine<T> =>
(value) => {
const result = schema.safeParse(value);
return result.success ? result.data : undefined;
};

export function Counter({ storeName }: { storeName: StoreName }) {
const [counter, setCounter] = useLocationState({
name: "counter",
defaultValue: 0,
storeName,
refine: (value) => {
const result = schema.safeParse(value);
if (result.success) {
return result.data;
}
return undefined;
},
refine: zodRefine(z.number()),
});
console.debug("rendered Counter", { storeName, counter });

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"eslint": "8.47.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.3",
"jest": "29.6.1",
"navigation-api-types": "0.3.1",
Expand Down
7 changes: 7 additions & 0 deletions packages/eslint-config-custom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier",
],
parser: "@typescript-eslint/parser",
Expand All @@ -18,5 +19,11 @@ module.exports = {
rules: {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
"react-hooks/exhaustive-deps": "error",
},
settings: {
react: {
version: "detect",
},
},
};
48 changes: 19 additions & 29 deletions packages/location-state-core/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { LocationStateContext } from "./context";
import { StoreName } from "./types";
import { useCallback, useContext, useSyncExternalStore } from "react";
import { useCallback, useContext, useState, useSyncExternalStore } from "react";

const useStore = (storeName: StoreName | string) => {
const { stores } = useContext(LocationStateContext);
const store = stores[storeName];
if (!store) {
throw new Error("`LocationStateProvider` is required.");
throw new Error(`Store not found: ${storeName}`);
}

return store;
Expand All @@ -21,11 +21,13 @@ export type LocationStateDefinition<T> = {
refine?: Refine<T>;
};

type SetStateArg<T> = T | ((prev: T) => T);
type Updater<T> = (prev: T) => T;
type UpdaterOrValue<T> = T | Updater<T>;
type SetState<T> = (updaterOrValue: UpdaterOrValue<T>) => void;

export const useLocationState = <T>(
definition: LocationStateDefinition<T>,
): [T, (setterOrValue: SetStateArg<T>) => void] => {
): [T, SetState<T>] => {
const storeState = useLocationStateValue(definition);
const setStoreState = useLocationSetState<T>(definition);
return [storeState, setStoreState];
Expand Down Expand Up @@ -56,36 +58,24 @@ export const useLocationStateValue = <T>({
return storeState;
};

export const useLocationSetState = <T>({
name,
defaultValue,
storeName,
refine,
}: LocationStateDefinition<T>): ((setterOrValue: SetStateArg<T>) => void) => {
export const useLocationSetState = <T>(
props: LocationStateDefinition<T>,
): SetState<T> => {
const { name, defaultValue, storeName, refine } = useState(props)[0];
const store = useStore(storeName);
const setStoreState = useCallback(
(setterOrValue: SetStateArg<T>) => {
if (typeof setterOrValue === "function") {
const setter = setterOrValue as (prev: T) => T;
const storeValue = store.get(name) as T | undefined;
const refinedValue = refine ? refine(storeValue) : storeValue;
const prev = refinedValue ?? defaultValue;
store.set(name, setter(prev));
(updaterOrValue: UpdaterOrValue<T>) => {
if (typeof updaterOrValue !== "function") {
store.set(name, updaterOrValue);
return;
}
store.set(name, setterOrValue);
const updater = updaterOrValue as Updater<T>;
const storeValue = store.get(name) as T | undefined;
const refinedValue = refine ? refine(storeValue) : storeValue;
const prev = refinedValue ?? defaultValue;
store.set(name, updater(prev));
},
[name, store],
[name, store, defaultValue, refine],
);
return setStoreState;
};

export const useLocationStateSnapshot = <T>({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
storeName,
}: {
name: string;
storeName: StoreName | string;
}): T => {
throw new Error("Not implemented yet");
};
44 changes: 42 additions & 2 deletions packages/location-state-core/src/location-sync.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect, useRef } from "react";
import {
LocationStateDefinition,
useLocationSetState,
Expand All @@ -20,7 +21,7 @@ beforeEach(() => {
sessionStorage.clear();
});

describe("`useLocationState` used.", () => {
describe("using `useLocationState`.", () => {
function LocationSyncCounter() {
const [count, setCount] = useLocationState({
name: "count",
Expand Down Expand Up @@ -91,7 +92,7 @@ describe("`useLocationState` used.", () => {
});
});

describe("`useLocationStateValue`/`useLocationSetState` used.", () => {
describe("using `useLocationStateValue`/`useLocationSetState`.", () => {
function LocationSyncCounter() {
const counter: LocationStateDefinition<number> = {
name: "counter",
Expand Down Expand Up @@ -137,3 +138,42 @@ describe("`useLocationStateValue`/`useLocationSetState` used.", () => {
);
});
});

describe("using `useLocationSetState`.", () => {
function LocationSyncCounter() {
const counter: LocationStateDefinition<number> = {
name: "counter",
defaultValue: 0,
storeName: "session",
};
const rendered = useRef(1);
const setCount = useLocationSetState(counter);
useEffect(() => {
rendered.current++;
}, []);

return (
<div>
<h1>rendered: {rendered.current}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>increment</button>
</div>
);
}

function LocationSyncCounterPage() {
return (
<LocationStateProvider>
<LocationSyncCounter />
</LocationStateProvider>
);
}

test("setCount does not re-render.", async () => {
// Arrange
const { user } = renderWithUser(<LocationSyncCounterPage />);
// Act
await user.click(await screen.findByRole("button", { name: "increment" }));
// Assert
expect(screen.getByRole("heading")).toHaveTextContent("rendered: 1");
});
});
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d26d50b

Please sign in to comment.