Skip to content

Commit

Permalink
refactor(vdom): change shouldUpdate API for stateless components
Browse files Browse the repository at this point in the history
This API is much more flexible and opens up different possibilities how
to improve stateless components without breaking API in the future.
The key problem with such API was that there is no direct way to get
access to the stateless component descriptor from the factory function,
but the trick here is to execute factory function and retrieve component
descriptor from the virtual DOM node.

BREAKING CHANGE: `shouldUpdate` for stateless components is now assigned
with a `withShouldUpdate()` function.

Before:

const C = statelessComponent(
  (props) => h.div().c(props.title),
  (prev, next) => (prev.title !== next.title),
);

After:

const C = withShouldUpdate(
  (prev, next) => (prev.title !== next.title),
  statelessComponent(
    (props) => h.div().c(props.title),
  ),
);
  • Loading branch information
localvoid committed May 12, 2018
1 parent 87b7d22 commit e4daf4c
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 50 deletions.
7 changes: 5 additions & 2 deletions packages/ivi/__tests__/lifecycle.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Component, VNode, statelessComponent, statefulComponent } from "ivi";
import { Component, VNode, statelessComponent, withShouldUpdate, statefulComponent } from "ivi";
import * as h from "ivi-html";
import { startRender, checkLifecycle, lifecycleTouch } from "./utils";

const Static = statelessComponent<VNode>((child) => child, () => false);
const Static = withShouldUpdate(
() => false,
statelessComponent<VNode>((child) => child),
);

interface ComponentHooks<P> {
construct?: (
Expand Down
25 changes: 19 additions & 6 deletions packages/ivi/__tests__/stateless_component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
import { statelessComponent } from "ivi";
import { statelessComponent, withShouldUpdate } from "ivi";
import * as h from "ivi-html";
import { startRender } from "./utils";

test(`props should be passed to render hook`, () => {
startRender((r) => {
let a = -1;
const c = statelessComponent<number>(
(props) => {
expect(props).toBe(1337);
a = props;
return h.div().c(props);
},
);

r(c(1337));

expect(a).toBe(1337);
});
});

test(`props should be passed to shouldUpdate hook`, () => {
startRender((r) => {
const c = statelessComponent<number>(
(props) => h.div().c(props),
let a = -1;
let b = -1;

const c = withShouldUpdate<number>(
(oldProps, newProps) => {
expect(oldProps).toBe(1337);
expect(newProps).toBe(1338);
a = oldProps;
b = newProps;
return true;
},
statelessComponent<number>(
(props) => h.div().c(props),
),
);

r(c(1337));
r(c(1338));

expect(a).toBe(1337);
expect(b).toBe(1338);
});
});
2 changes: 1 addition & 1 deletion packages/ivi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export {
} from "./vdom/vnode";
export { children, map, mapRange } from "./vdom/vnode_collections";
export { element } from "./vdom/element";
export { statefulComponent, statelessComponent, context, connect } from "./vdom/vnode_factories";
export { statefulComponent, statelessComponent, withShouldUpdate, context, connect } from "./vdom/vnode_factories";

/**
* API available only in browser environment
Expand Down
2 changes: 1 addition & 1 deletion packages/ivi/src/vdom/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { VNode } from "./vnode";
*/
export interface StatelessComponent<P = undefined> {
render: (props: P) => VNode;
shouldUpdate: ((oldProps: P, newProps: P) => boolean) | undefined;
shouldUpdate: ((oldProps: P, newProps: P) => boolean) | null;
}

/**
Expand Down
95 changes: 55 additions & 40 deletions packages/ivi/src/vdom/vnode_factories.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { VNodeFlags } from "./flags";
import { StatefulComponent } from "./component";
import { StatefulComponent, StatelessComponent } from "./component";
import { VNode } from "./vnode";
import { ConnectDescriptor } from "./connect_descriptor";

/**
* statelessComponent creates a virtual DOM node factory that produces nodes for stateless components.
*
* const A = statelessComponent<{ text: string }>(
* (props) => h.div().c(props.text);
* (prevProps, nextProps) => prevProps.text !== nextProps.text;
* (props) => h.div().c(props.text),
* );
*
* render(
* A({ text: "Hello" });
* A({ text: "Hello" }),
* DOMContainer,
* );
*
* @param render render function.
* @param shouldUpdate optional function that performs an early check that prevent unnecessary updates.
* @returns factory that produces stateless component nodes.
*/
export function statelessComponent(c: () => VNode): () => VNode<undefined>;
Expand All @@ -26,65 +24,82 @@ export function statelessComponent(c: () => VNode): () => VNode<undefined>;
* statelessComponent creates a virtual DOM node factory that produces nodes for stateless components.
*
* const A = statelessComponent<{ text: string }>(
* (props) => h.div().c(props.text);
* (prevProps, nextProps) => prevProps.text !== nextProps.text;
* (props) => h.div().c(props.text),
* );
*
* render(
* A({ text: "Hello" });
* A({ text: "Hello" }),
* DOMContainer,
* );
*
* @param render render function.
* @param shouldUpdate optional function that performs an early check that prevent unnecessary updates.
* @returns factory that produces stateless component nodes.
*/
export function statelessComponent<P>(
render: undefined extends P ? (props?: P) => VNode<any> : (props: P) => VNode<any>,
shouldUpdate?: (oldProps: P, newProps: P) => boolean,
): undefined extends P ? (props?: P) => VNode<P> : (props: P) => VNode<P>;

/**
* statelessComponent creates a virtual DOM node factory that produces nodes for stateless components.
*
* const A = statelessComponent<{ text: string }>(
* (props) => h.div().c(props.text);
* (prevProps, nextProps) => prevProps.text !== nextProps.text;
* (props) => h.div().c(props.text),
* );
*
* render(
* A({ text: "Hello" });
* A({ text: "Hello" }),
* DOMContainer,
* );
*
* @param render render function.
* @param shouldUpdate optional function that performs an early check that prevent unnecessary updates.
* @returns factory that produces stateless component nodes.
*/
export function statelessComponent<P>(
render: (props: P) => VNode<any>,
shouldUpdate?: (oldProps: P, newProps: P) => boolean,
export function statelessComponent<P>(render: (props: P) => VNode<any>): (props: P) => VNode<P> {
const d = { render, shouldUpdate: null };
const f = function (props: P): VNode<P> {
const n = new VNode<P>(
VNodeFlags.StatelessComponent,
d,
props,
void 0,
null,
);
/* istanbul ignore else */
if (DEBUG) {
n.factory = f;
}
return n;
};
return f;
}

/**
* withShouldUpdate creates a virtual DOM node factory that produces nodes for stateless components with custom
* `shouldUpdate` function to prevent unnecessary updates.
*
* const A = withShouldUpdate<{ text: string }>(
* (prevProps, nextProps) => prevProps.text !== nextProps.text,
* statelessComponent(
* (props) => h.div().c(props.text),
* ),
* );
*
* render(
* A({ text: "Hello" }),
* DOMContainer,
* );
*
* @param shouldUpdate function that performs an early check that prevent unnecessary updates.
* @param factory factory that produces stateless component nodes.
* @returns factory that produces stateless component nodes.
*/
export function withShouldUpdate<P>(
shouldUpdate: (oldProps: P, newProps: P) => boolean,
factory: (props: P) => VNode<P>,
): (props: P) => VNode<P> {
const d = { render, shouldUpdate };
let f: (props: P) => VNode<P>;
if (shouldUpdate === undefined) {
f = function (props: P): VNode<P> {
const n = new VNode<P>(
VNodeFlags.StatelessComponent,
d,
props,
void 0,
null,
);
/* istanbul ignore else */
if (DEBUG) {
n.factory = f;
}
return n;
};
return f;
}
f = function (props: P): VNode<P> {
const v = factory(null as any);
const d = { render: (v.tag as StatelessComponent<P>).render, shouldUpdate };
const f = function (props: P): VNode<P> {
const n = new VNode<P>(
VNodeFlags.StatelessComponent | VNodeFlags.ShouldUpdateHint,
d,
Expand Down Expand Up @@ -117,7 +132,7 @@ export function statelessComponent<P>(
* });
*
* render(
* A("clicked");
* A("clicked"),
* DOMContainer,
* );
*
Expand All @@ -142,7 +157,7 @@ export function statefulComponent(c: StatefulComponent<undefined>): () => VNode<
* });
*
* render(
* A("clicked");
* A("clicked"),
* DOMContainer,
* );
*
Expand All @@ -169,7 +184,7 @@ export function statefulComponent<P>(
* });
*
* render(
* A("clicked");
* A("clicked"),
* DOMContainer,
* );
*
Expand Down

0 comments on commit e4daf4c

Please sign in to comment.