Skip to content

Commit

Permalink
start useValueSelector tracking only after mounting
Browse files Browse the repository at this point in the history
  • Loading branch information
Bloomca committed Jun 9, 2024
1 parent 690c70a commit e080257
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 24 deletions.
24 changes: 19 additions & 5 deletions integration-tests/create-state/create-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ describe("createState", () => {
expect(screen.getByTestId("text").textContent).toBe("length is 17");
});

test.only("unsubscribes from updates if wasn't mounted", async () => {
test("unsubscribes from updates if wasn't mounted", async () => {
const user = userEvent.setup();
const valueState = createState(0);
function App() {
Expand All @@ -585,7 +585,7 @@ describe("createState", () => {
children: [
createElement("button", {
"data-testid": "button",
onClick: () => showState.setValue(false),
onClick: () => showState.setValue((value) => !value),
}),
showState.useValue((shouldShow) =>
shouldShow ? createElement(NestedComponent) : null
Expand All @@ -608,6 +608,10 @@ describe("createState", () => {
return createElement("div", {
children: [
createElement("h1", { children: "nested component" }),
createElement("button", {
"data-testid": "nestedButton",
onClick: () => showState.setValue((value) => !value),
}),
showState.useValue((shouldShow) => (shouldShow ? x : null)),
],
});
Expand All @@ -619,11 +623,21 @@ describe("createState", () => {
});

valueState.setValue(1);
expect(spyFn).toHaveBeenCalledTimes(2);
// it only was called one time, but it does not track because it is not mounted
expect(spyFn).toHaveBeenCalledTimes(1);

await user.click(screen.getByTestId("button"));

valueState.setValue(2);
expect(spyFn).toHaveBeenCalledTimes(1);

await user.click(screen.getByTestId("button"));
expect(spyFn).toHaveBeenCalledTimes(2);
valueState.setValue(3);
expect(spyFn).toHaveBeenCalledTimes(2);

// valueState.setValue(2);
// expect(spyFn).toHaveBeenCalledTimes(2);
await user.click(screen.getByTestId("nestedButton"));
expect(spyFn).toHaveBeenCalledTimes(3);
expect(await screen.findByText("value is 3")).toBeVisible();
});
});
3 changes: 1 addition & 2 deletions src/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,12 @@ function getComponentVelesNode(
function callMountHandlers(
component: VelesComponent | VelesElement | VelesStringElement
): void {
component._privateMethods._callMountHandlers();
if ("velesStringElement" in component) {
// string elements don't have mount callbacks, only unmount
return;
}

if ("velesComponent" in component) {
component._privateMethods._callMountHandlers();
callMountHandlers(component.tree);
}

Expand Down
13 changes: 12 additions & 1 deletion src/create-element/create-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,19 @@ function createElement(
velesNode.velesNode = true;
velesNode.childComponents = childComponents;
velesNode.phantom = phantom;

// these handlers are used to start tracking `useValue` only when the node
// is actually mounted in the DOM
let mountHandlers: Function[] = [];
velesNode._privateMethods = {
_addUnmountHandler: (cb: Function) => {
_addMountHandler(cb: Function) {
mountHandlers.push(cb);
},
_callMountHandlers() {
mountHandlers.forEach((cb) => cb());
mountHandlers = [];
},
_addUnmountHandler(cb: Function) {
unmountHandlers.push(cb);
},
_callUnmountHandlers: callUnmountHandlers,
Expand Down
11 changes: 10 additions & 1 deletion src/create-element/create-text-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ import type { VelesStringElement } from "../types";
export function createTextElement(
text: string | undefined | null
): VelesStringElement {
const unmountHandlers: Function[] = [];
let mountHandlers: Function[] = [];
let unmountHandlers: Function[] = [];
return {
velesStringElement: true,
// in case there is no text, we create an empty Text node, so we still can
// have a reference to it, replace it, call lifecycle methods, etc
html: document.createTextNode(text || ""),

_privateMethods: {
_addMountHandler(cb: Function) {
mountHandlers.push(cb);
},
_callMountHandlers() {
mountHandlers.forEach((cb) => cb());
mountHandlers = [];
},
_addUnmountHandler: (cb: Function) => {
unmountHandlers.push(cb);
},
_callUnmountHandlers: () => {
unmountHandlers.forEach((cb) => cb());
// unmountHandlers = [];
},
},
};
Expand Down
17 changes: 9 additions & 8 deletions src/create-element/parse-component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { addContext, popContext } from "../hooks/lifecycle";
import { createTextElement } from "./create-text-element";

import type {
VelesComponent,
Expand All @@ -15,8 +16,8 @@ function parseComponent({
element: ComponentFunction;
props: VelesElementProps;
}) {
const componentUnmountCbs: Function[] = [];
const componentMountCbs: Function[] = [];
let componentUnmountCbs: Function[] = [];
let componentMountCbs: Function[] = [];
const componentAPI: ComponentAPI = {
onMount: (cb) => {
componentMountCbs.push(cb);
Expand All @@ -31,12 +32,7 @@ function parseComponent({

const componentTree =
typeof _componentTree === "string" || !_componentTree
? ({
velesStringElement: true,
html: document.createTextNode(
typeof _componentTree === "string" ? _componentTree : ""
),
} as VelesStringElement)
? createTextElement(_componentTree as string)
: _componentTree;

// here we exit our context
Expand All @@ -45,6 +41,9 @@ function parseComponent({
velesComponent: true,
tree: componentTree,
_privateMethods: {
_addMountHandler(cb: Function) {
componentMountCbs.push(cb);
},
_addUnmountHandler: (cb: Function) => {
componentAPI.onUnmount(cb);
},
Expand All @@ -56,6 +55,7 @@ function parseComponent({
componentAPI.onUnmount(mountCbResult);
}
});
// componentMountCbs = [];
},
_callUnmountHandlers: () => {
// this should trigger recursive checks, whether it is a VelesNode or VelesComponent
Expand All @@ -66,6 +66,7 @@ function parseComponent({

// we execute own unmount callbacks after children, so the order is reversed
componentUnmountCbs.forEach((cb) => cb());
// componentUnmountCbs = [];
},
},
};
Expand Down
42 changes: 35 additions & 7 deletions src/create-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { onUnmount, onMount } from "../hooks/lifecycle";
import { createElement } from "../create-element/create-element";
import { createTextElement } from "../create-element/create-text-element";
import { triggerUpdates } from "./trigger-updates";
import { updateUseValueSelector } from "./update-usevalue-selector-value";

import type {
VelesElement,
VelesComponent,
VelesStringElement,
} from "../types";

import type { State, TrackingIterator, StateTrackers } from "./types";
import type {
State,
TrackingIterator,
StateTrackers,
TrackingSelectorElement,
} from "./types";

function createState<T>(
initialValue: T,
Expand Down Expand Up @@ -78,6 +84,7 @@ function createState<T>(
) => VelesElement | VelesComponent | string | undefined | null,
comparator: (value1: F, value2: F) => boolean = identity
): VelesElement | VelesComponent | VelesStringElement {
const valueOnInit = value;
// @ts-expect-error
const selectedValue = selector ? selector(value) : (value as F);
const returnedNode = cb
Expand All @@ -97,14 +104,35 @@ function createState<T>(
node,
comparator,
};
trackers.trackingSelectorElements.push(trackingSelectorElement);

node._privateMethods._addUnmountHandler(() => {
trackers.trackingSelectorElements =
trackers.trackingSelectorElements.filter(
(el) => trackingSelectorElement !== el
);
node._privateMethods._addMountHandler(() => {
// we need to trigger update useValueSelector manually, but only
// in case the value has changed
if (value !== valueOnInit) {
const newTrackingSelectorElements: TrackingSelectorElement[] = [];
updateUseValueSelector({
value,
trackers,
selectorTrackingElement: trackingSelectorElement,
newTrackingSelectorElements,
});

if (newTrackingSelectorElements[0]) {
trackers.trackingSelectorElements.push(
newTrackingSelectorElements[0]
);
}
} else {
trackers.trackingSelectorElements.push(trackingSelectorElement);
node._privateMethods._addUnmountHandler(() => {
trackers.trackingSelectorElements =
trackers.trackingSelectorElements.filter(
(el) => trackingSelectorElement !== el
);
});
}
});

return node;
},
useValueIterator<Element>(
Expand Down
5 changes: 5 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type VelesElement = {

// not intended to be used directly
_privateMethods: {
_addMountHandler: Function;
_callMountHandlers: Function;
_addUnmountHandler: Function;
_callUnmountHandlers: Function;
};
Expand All @@ -33,6 +35,8 @@ export type VelesStringElement = {
// methods is useful for state changes, to remove tracking
// when the said Text is returned from `useValue` state method
_privateMethods: {
_addMountHandler: Function;
_callMountHandlers: Function;
_addUnmountHandler: Function;
_callUnmountHandlers: Function;
};
Expand All @@ -46,6 +50,7 @@ export type VelesComponent = {

// not intended to be used directly
_privateMethods: {
_addMountHandler: Function;
_callMountHandlers: Function;
_callUnmountHandlers: Function;
_addUnmountHandler: Function;
Expand Down

0 comments on commit e080257

Please sign in to comment.