Skip to content

Commit

Permalink
switch update
Browse files Browse the repository at this point in the history
  • Loading branch information
XantreDev committed Apr 15, 2024
1 parent b7f18de commit 02878e1
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-rivers-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preact-signals/utils": minor
---

Removed caching from Switch component. Added `Switch.Match` as alias for separete Match component
1 change: 1 addition & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"rollup-plugin-node-externals": "^6.1.2",
"typedoc": "^0.25.8",
"typedoc-plugin-markdown": "4.0.0-next.53",
"vite-plugin-babel": "^1.1.3",
"zx": "^7.2.3"
},
"keywords": [
Expand Down
6 changes: 6 additions & 0 deletions packages/utils/src/__tests__/hoc.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ describe("reactifyLite()", () => {
async ({ expect, act, root, reactRoot }) => {
const sig = signal(10);

/**
* @useSignals
*/
const aRender = vi.fn((props: ReactiveProps<{ value: number }>) => (
<div>{props.value}</div>
));
Expand Down Expand Up @@ -155,6 +158,9 @@ describe("reactifyLite()", () => {
async ({ act, reactRoot, expect }) => {
const sig = signal(10);

/**
* @useSignals
*/
const aRender = vi.fn(
(props: ReactiveProps<{ value: number }>) => props.value
);
Expand Down
16 changes: 12 additions & 4 deletions packages/utils/src/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ export const withRenderer =
});
});

export const itRenderer = (
name: string,
callback: (props: WithRendererProps) => unknown
) => it(name, withRenderer(callback));
export const itRenderer = Object.assign(
function isRenderer(
name: string,
callback: (props: WithRendererProps) => unknown
) {
return it(name, withRenderer(callback));
},
{
only: (name: string, callback: (props: WithRendererProps) => unknown) =>
it.only(name, withRenderer(callback)),
}
);
41 changes: 39 additions & 2 deletions packages/utils/src/lib/components/__tests__/Switch.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { signal } from "@preact-signals/unified-signals";
import React from "react";
import { describe, vi } from "vitest";
import React, { useMemo } from "react";
import { describe, Mock, vi } from "vitest";
import { itRenderer } from "../../../__tests__/utils";
import { Match, Switch } from "../components";

Expand Down Expand Up @@ -171,4 +171,41 @@ describe.concurrent("Switch()", () => {
}
}
);
itRenderer(
"must reexecute child even if nothing changed",
async ({ act, expect, reactRoot, root }) => {
const sig = signal(0);
const matchWhen = vi.fn(() => 10);
const renderWhen = vi.fn(() => 220);

const Component = vi.fn(() => {
console.log("render");
sig.value;

return (
<Switch fallback={"fallback"}>
<Switch.Match when={matchWhen}>{renderWhen}</Switch.Match>
</Switch>
);
});

await reactRoot().render(<Component />);

const firstChild = root.firstChild;
expect(firstChild).is.instanceOf(Text);
expect(firstChild).has.property("data", "220");

expect(Component).toHaveBeenCalledTimes(1);
expect(matchWhen).toHaveBeenCalledTimes(1);
expect(renderWhen).toHaveBeenCalledTimes(1);

await act(() => {
sig.value++;
});

expect(Component).toHaveBeenCalledTimes(2);
expect(matchWhen).toHaveBeenCalledTimes(2);
expect(renderWhen).toHaveBeenCalledTimes(2);
}
);
});
1 change: 0 additions & 1 deletion packages/utils/src/lib/components/components/Computed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useComputedOnce } from "../../hooks";
import { Accessor } from "../../utils";
import { RenderResult } from "../type";

Expand Down
102 changes: 64 additions & 38 deletions packages/utils/src/lib/components/components/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Children, isValidElement } from "react";
import { useComputedOnce, useSignalOfState } from "../../hooks";
import React, { Children, isValidElement } from "react";
import {
Accessor,
AnyReactive,
Expand All @@ -22,13 +21,15 @@ export type MatchProps<T extends AnyReactive> = {
};

const matchSymbol = Symbol("match");
export const Match = Object.assign(
const Match = Object.assign(
<const T extends AnyReactive>(_props: MatchProps<T>) => null,
{
[matchSymbol]: true,
}
);

export { Match };

export type SwitchProps = {
/**
* shouldn't change during the lifecycle of the component
Expand All @@ -43,23 +44,8 @@ export type SwitchProps = {
/**
*
* @useSignals
* @example
* // when prop can be callback or signal
* <Switch>
* <Match when={() => isLoading.value}>
* <Loader />
* </Match>
* <Match when={() => isError.value}>
* There are an error
* </Match>
* <Match when={() => data.value}>
* {(contentAccessor) => (
* contentAccessor().id === 10 ? 1 : 2
* )}
* </Match>
* </Switch>
*/
export const Switch = (props: SwitchProps): JSX.Element => {
const Switch_ = (props: SwitchProps): JSX.Element => {
if (
__DEV__ &&
!Children.toArray(props.children).every(
Expand All @@ -76,25 +62,65 @@ export const Switch = (props: SwitchProps): JSX.Element => {
"every child of switch should be Match and have when and children props"
);
}
const matches = useSignalOfState(
Children.toArray(props.children) as {
props: MatchProps<Reactive<unknown>>;
}[]
const childrenArray = Children.toArray(props.children) as {
props: MatchProps<Reactive<unknown>>;
}[];

const fallback = props.fallback ?? null;

if (__DEV__ && childrenArray.length === 0) {
console.warn("Switch component has no children");
}

const itemIndex = childrenArray.findIndex(
({ props: item }) => !isExplicitFalsy(unwrapReactive(item.when))
);
const fallback = useSignalOfState<RenderResult>(props.fallback ?? null);

return useComputedOnce(() => {
if (matches.value.length === 0) {
return fallback.value ?? null;
}
const item = matches.value.find(
(item) => !isExplicitFalsy(unwrapReactive(item.props.when))
);
if (!item) {
return fallback.value ?? null;
}
return typeof item.props.children === "function"
? item.props.children(accessorOfReactive(item.props.when))
: item.props.children;
}).value as JSX.Element;
if (itemIndex === -1) {
return fallback as JSX.Element;
}
const item = childrenArray[itemIndex]?.props;
if (!item) {
throw new Error("item is not found");
}
const children = item.children;

return (
typeof children === "function"
? children(accessorOfReactive(item.when))
: children
) as JSX.Element;
};

/**
*
* @example
* // when prop can be callback or signal
* <Switch>
* <Switch.Match when={() => isLoading.value}>
* <Loader />
* </Switch.Match>
* <Switch.Match when={() => isError.value}>
* There are an error
* </Switch.Match>
* <Switch.Match when={() => data.value}>
* {(contentAccessor) => (
* contentAccessor().id === 10 ? 1 : 2
* )}
* </Switch.Match>
* </Switch>
*
* @description only static `Match` components are allowed. Here how you **cannot** use it:
* @example
* <Switch>
* {a > 10 ? <Switch.Match when={() => isLoading.value}>
* <Loader />
* </Switch.Match>
* : <Switch.Match when={() => isError.value}>
* There are an error
* </Switch.Match>}
* </Switch>
*/
export const Switch = Object.assign(Switch_, {
Match,
});
18 changes: 18 additions & 0 deletions packages/utils/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "vitest/config";
import babel from "vite-plugin-babel";

export default defineConfig({
test: {
Expand All @@ -8,4 +9,21 @@ export default defineConfig({
define: {
__DEV__: true,
},
resolve: {
alias: {
"@preact/signals-react": "@preact-signals/safe-react",
},
},
plugins: [
// @ts-expect-error
babel({
filter: /\.(j|t)sx$/,
babelConfig: {
plugins: ["module:@preact-signals/safe-react/babel"],
parserOpts: {
plugins: ["jsx", 'typescript'],
},
},
}),
],
});
27 changes: 24 additions & 3 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 02878e1

Please sign in to comment.