Skip to content

Commit

Permalink
add track value options (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bloomca authored May 26, 2024
1 parent e0b000a commit e7b52c9
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function Counter() {
}
```

This subscription will not cause any re-renders.
This subscription will not cause any re-renders. By default, the first call will happen during the component initialization, and you can pass a second options object to alter this behaviour. You can either set `{ skipFirstCall: true }` to completely skip it, or you can specify to run it when the component is mounted in DOM: `{ callOnMount: true }`.

### Combining different states

Expand Down
83 changes: 83 additions & 0 deletions integration-tests/create-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,89 @@ describe("createState", () => {
expect(onUnmountCheck).not.toHaveBeenCalled();
});

it("supports custom subscriptions with state.trackValue with skipFirstCall option", async () => {
const user = userEvent.setup();
const spyFn = jest.fn();
function StateComponent() {
const valueState = createState(0);
valueState.trackValue((value) => spyFn(value), { skipFirstCall: true });

return createElement("div", {
children: [
createElement("button", {
"data-testid": "button",
onClick: () => {
valueState.setValue((currentValue) => currentValue + 1);
},
}),
valueState.useValue((value) =>
createElement("div", { children: [`current value is ${value}`] })
),
],
});
}

cleanup = attachComponent({
htmlElement: document.body,
component: createElement(StateComponent),
});

expect(spyFn).toHaveBeenCalledTimes(0);

const btn = screen.getByTestId("button");
await user.click(btn);
expect(spyFn).toHaveBeenCalledTimes(1);
expect(spyFn).toHaveBeenLastCalledWith(1);
await user.click(btn);
expect(spyFn).toHaveBeenCalledTimes(2);
expect(spyFn).toHaveBeenLastCalledWith(2);
});

it("supports custom subscriptions with state.trackValue with callOnMount option", async () => {
const user = userEvent.setup();
const spyFn = jest.fn();
function StateComponent() {
const valueState = createState(0);
valueState.trackValue((value) => spyFn(value), { callOnMount: true });

return createElement("div", {
children: [
createElement("button", {
"data-testid": "button",
onClick: () => {
valueState.setValue((currentValue) => currentValue + 1);
},
}),
valueState.useValue((value) =>
createElement("div", { children: [`current value is ${value}`] })
),
],
});
}

cleanup = attachComponent({
htmlElement: document.body,
component: createElement(StateComponent),
});

expect(spyFn).toHaveBeenCalledTimes(0);

// wait until the component is mounted in DOM
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
expect(spyFn).toHaveBeenCalledTimes(1);
expect(spyFn).toHaveBeenLastCalledWith(0);

const btn = screen.getByTestId("button");
await user.click(btn);
expect(spyFn).toHaveBeenCalledTimes(2);
expect(spyFn).toHaveBeenLastCalledWith(1);
await user.click(btn);
expect(spyFn).toHaveBeenCalledTimes(3);
expect(spyFn).toHaveBeenLastCalledWith(2);
});

// test to make sure that `useValueSelector` is correctly called only when
// the selector function returns a different result
test("support selector functions correctly with useValueSelector", async () => {
Expand Down
23 changes: 17 additions & 6 deletions src/hooks/create-state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getComponentVelesNode, identity } from "../utils";
import { onUnmount } from "./lifecycle";
import { onUnmount, onMount } from "./lifecycle";
import { createElement } from "../create-element/create-element";

import type { VelesElement, VelesComponent } from "../types";
Expand All @@ -10,7 +10,10 @@ type AttributeHelper = {
};

export type State<ValueType> = {
trackValue(cb: (value: ValueType) => void | Function): void;
trackValue(
cb: (value: ValueType) => void | Function,
options?: { callOnMount?: boolean; skipFirstCall?: boolean }
): void;
useValue(
cb: (value: ValueType) => VelesElement | VelesComponent,
comparator?: (value1: ValueType, value2: ValueType) => boolean
Expand Down Expand Up @@ -87,11 +90,19 @@ function createState<T>(
const result: State<T> = {
// supposed to be used at the component level
// TODO: add a version of trackValueSelector
trackValue: (cb) => {
trackValue: (cb, options = {}) => {
trackingEffects.push(cb);
// trigger the callback first time
// maybe provide an option to skip it first time?
cb(value);
if (!options.skipFirstCall) {
// trigger the callback first time
// execute the first callback when the component is mounted
if (options.callOnMount) {
onMount(() => {
cb(value);
});
} else {
cb(value);
}
}
// track value is attached at the component level
onUnmount(() => {
trackingEffects = trackingEffects.filter(
Expand Down

0 comments on commit e7b52c9

Please sign in to comment.