Skip to content

Commit

Permalink
feat(useVal): support useVal with non-val value
Browse files Browse the repository at this point in the history
  • Loading branch information
crimx committed May 21, 2024
1 parent c71005d commit 13bda27
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 23 deletions.
69 changes: 47 additions & 22 deletions src/use-val.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type ReadonlyVal } from "value-enhancer";
import type { UnwrapVal } from "value-enhancer";
import { isVal, type ReadonlyVal } from "value-enhancer";

import reactExports, {
useDebugValue,
Expand Down Expand Up @@ -30,6 +31,24 @@ interface UseVal {
<TValue = any>(val$?: ReadonlyVal<TValue>, eager?: boolean):
| TValue
| undefined;

/**
* Returns the value if it is not a val.
*
* @param value A non-val value
* @param eager Trigger subscription callback synchronously. Default true.
* @returns the value
*/
<TValue>(value: TValue, eager?: boolean): UnwrapVal<TValue>;

/**
* Returns the value if it is not a val.
*
* @param value A non-val value
* @param eager Trigger subscription callback synchronously. Default true.
* @returns the value
*/
<TValue>(value?: TValue, eager?: boolean): UnwrapVal<TValue> | undefined;
}

// Utility types and functions for useValWithUseSyncExternalStore
Expand All @@ -39,16 +58,24 @@ interface UseVal {
*/
type Subscriber = Parameters<(typeof reactExports)["useSyncExternalStore"]>[0];

const noop = () => {
/* noop */
};

const noopSubscriber: Subscriber = () => noop;

export const useValWithUseSyncExternalStore: UseVal = <TValue>(
val$?: ReadonlyVal<TValue>,
val$?: TValue,
eager = true
): TValue | undefined => {
): UnwrapVal<TValue> | undefined => {
const [subscriber, getSnapshot] = useMemo(
() =>
[
(onStoreChange => val$?.subscribe(onStoreChange, eager)) as Subscriber,
() => val$?.$version,
] as const,
isVal(val$)
? ([
(onChange: () => void) => val$.subscribe(onChange, eager),
() => val$.$version,
] as const)
: ([noopSubscriber, noop] as const),
[val$, eager]
);

Expand All @@ -60,32 +87,30 @@ export const useValWithUseSyncExternalStore: UseVal = <TValue>(
getSnapshot
);

useDebugValue(val$?.value);
const value = isVal(val$) ? val$.get() : val$;

return val$?.value;
useDebugValue(value);

return value;
};

export const useValWithUseEffect: UseVal = <TValue>(
val$?: ReadonlyVal<TValue>,
val$?: TValue,
eager = true
): TValue | undefined => {
const [value, setValue] = useState(val$ ? val$.get : void 0);
const [, setVersion] = useState(val$?.$version);
): UnwrapVal<TValue> | undefined => {
const [, setVersion] = useState(() => (isVal(val$) ? val$.$version : val$));

useEffect(() => {
if (val$) {
if (isVal(val$)) {
const versionSetter = () => val$.$version;
const updateValue = () => {
setVersion(versionSetter);
setValue(val$.get);
};
return val$.subscribe(updateValue, eager);
} else {
setVersion(void 0);
setValue(void 0);
return val$.subscribe(() => setVersion(versionSetter), eager);
}

setVersion(noop);
}, [val$, eager]);

const value = isVal(val$) ? val$.get() : val$;

useDebugValue(value);

return value;
Expand Down
37 changes: 36 additions & 1 deletion test/use-val.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import type { ReadonlyVal } from "value-enhancer";
import { derive, val, nextTick } from "value-enhancer";
import {
useVal,
Expand All @@ -22,7 +23,7 @@ describe.each([
name: "useValWithUseSyncExternalStore",
useVal: useValWithUseSyncExternalStore,
},
])("useVal ($a)", ({ useVal }) => {
])("useVal ($name)", ({ useVal }) => {
it("should get value from val", () => {
const val$ = val(1);
const { result } = renderHook(() => useVal(val$));
Expand All @@ -36,6 +37,40 @@ describe.each([
expect(result.current).toBeUndefined();
});

it("should return the value if it is not a val", () => {
const { result, unmount } = renderHook(() => useVal(1));

expect(result.current).toBe(1);

unmount();
});

it("should support switching between value and val", () => {
const { result, rerender } = renderHook(({ count }) => useVal(count), {
initialProps: { count: 1 } as { count: number | ReadonlyVal<number> },
});

expect(result.current).toBe(1);

const v$ = val(2);

rerender({ count: v$ });

expect(result.current).toBe(2);

act(() => v$.set(3));

expect(result.current).toBe(3);

rerender({ count: 4 });

expect(result.current).toBe(4);

rerender({ count: 5 });

expect(result.current).toBe(5);
});

it("should update after value changes", async () => {
const val$ = val("a");
const { result } = renderHook(() => useVal(val$));
Expand Down

0 comments on commit 13bda27

Please sign in to comment.