[IMP] component: disallow calling hooks outside of setup

(and constructor)

Doing so could cause strange and difficult bugs
This commit is contained in:
Géry Debongnie
2022-01-31 15:03:52 +01:00
committed by aab-odoo
parent 45ea6a195e
commit efaa186544
6 changed files with 54 additions and 17 deletions
+5 -1
View File
@@ -15,7 +15,10 @@ import { STATUS } from "./status";
let currentNode: ComponentNode | null = null; let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode | null { export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new Error("No active component (a hook function should only be called in 'setup')");
}
return currentNode; return currentNode;
} }
@@ -108,6 +111,7 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
this.component = new C(props, env, this) as any; this.component = new C(props, env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this); this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup(); this.component.setup();
currentNode = null;
} }
mountComponent(target: any, options?: MountOptions) { mountComponent(target: any, options?: MountOptions) {
+10 -10
View File
@@ -6,42 +6,42 @@ import { nodeErrorHandlers } from "./error_handling";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export function onWillStart(fn: () => Promise<void> | void | any) { export function onWillStart(fn: () => Promise<void> | void | any) {
const node = getCurrent()!; const node = getCurrent();
node.willStart.push(fn.bind(node.component)); node.willStart.push(fn.bind(node.component));
} }
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) { export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
const node = getCurrent()!; const node = getCurrent();
node.willUpdateProps.push(fn.bind(node.component)); node.willUpdateProps.push(fn.bind(node.component));
} }
export function onMounted(fn: () => void | any) { export function onMounted(fn: () => void | any) {
const node = getCurrent()!; const node = getCurrent();
node.mounted.push(fn.bind(node.component)); node.mounted.push(fn.bind(node.component));
} }
export function onWillPatch(fn: () => Promise<void> | any | void) { export function onWillPatch(fn: () => Promise<void> | any | void) {
const node = getCurrent()!; const node = getCurrent();
node.willPatch.unshift(fn.bind(node.component)); node.willPatch.unshift(fn.bind(node.component));
} }
export function onPatched(fn: () => void | any) { export function onPatched(fn: () => void | any) {
const node = getCurrent()!; const node = getCurrent();
node.patched.push(fn.bind(node.component)); node.patched.push(fn.bind(node.component));
} }
export function onWillUnmount(fn: () => Promise<void> | void | any) { export function onWillUnmount(fn: () => Promise<void> | void | any) {
const node = getCurrent()!; const node = getCurrent();
node.willUnmount.unshift(fn.bind(node.component)); node.willUnmount.unshift(fn.bind(node.component));
} }
export function onWillDestroy(fn: () => Promise<void> | void | any) { export function onWillDestroy(fn: () => Promise<void> | void | any) {
const node = getCurrent()!; const node = getCurrent();
node.willDestroy.push(fn.bind(node.component)); node.willDestroy.push(fn.bind(node.component));
} }
export function onWillRender(fn: () => void | any) { export function onWillRender(fn: () => void | any) {
const node = getCurrent()!; const node = getCurrent();
const renderFn = node.renderFn; const renderFn = node.renderFn;
node.renderFn = () => { node.renderFn = () => {
fn.call(node.component); fn.call(node.component);
@@ -50,7 +50,7 @@ export function onWillRender(fn: () => void | any) {
} }
export function onRendered(fn: () => void | any) { export function onRendered(fn: () => void | any) {
const node = getCurrent()!; const node = getCurrent();
const renderFn = node.renderFn; const renderFn = node.renderFn;
node.renderFn = () => { node.renderFn = () => {
const result = renderFn(); const result = renderFn();
@@ -61,7 +61,7 @@ export function onRendered(fn: () => void | any) {
type OnErrorCallback = (error: any) => void | any; type OnErrorCallback = (error: any) => void | any;
export function onError(callback: OnErrorCallback) { export function onError(callback: OnErrorCallback) {
const node = getCurrent()!; const node = getCurrent();
let handlers = nodeErrorHandlers.get(node); let handlers = nodeErrorHandlers.get(node);
if (!handlers) { if (!handlers) {
handlers = []; handlers = [];
+5 -5
View File
@@ -11,7 +11,7 @@ import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks
* html node or component. * html node or component.
*/ */
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } { export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
const node = getCurrent()!; const node = getCurrent();
const refs = node.refs; const refs = node.refs;
return { return {
get el(): T | null { get el(): T | null {
@@ -29,7 +29,7 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
* need a reference to the env of the component calling them. * need a reference to the env of the component calling them.
*/ */
export function useEnv<E extends Env>(): E { export function useEnv<E extends Env>(): E {
return getCurrent()!.component.env as any; return getCurrent().component.env as any;
} }
function extendEnv(currentEnv: Object, extension: Object): Object { function extendEnv(currentEnv: Object, extension: Object): Object {
@@ -44,13 +44,13 @@ function extendEnv(currentEnv: Object, extension: Object): Object {
* constructor method. * constructor method.
*/ */
export function useSubEnv(envExtension: Env) { export function useSubEnv(envExtension: Env) {
const node = getCurrent()!; const node = getCurrent();
node.component.env = extendEnv(node.component.env as any, envExtension); node.component.env = extendEnv(node.component.env as any, envExtension);
useChildSubEnv(envExtension); useChildSubEnv(envExtension);
} }
export function useChildSubEnv(envExtension: Env) { export function useChildSubEnv(envExtension: Env) {
const node = getCurrent()!; const node = getCurrent();
node.childEnv = extendEnv(node.childEnv, envExtension); node.childEnv = extendEnv(node.childEnv, envExtension);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -121,7 +121,7 @@ export function useExternalListener(
handler: EventListener, handler: EventListener,
eventParams?: AddEventListenerOptions eventParams?: AddEventListenerOptions
) { ) {
const node = getCurrent()!; const node = getCurrent();
const boundHandler = handler.bind(node.component); const boundHandler = handler.bind(node.component);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams)); onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams)); onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
+1 -1
View File
@@ -239,7 +239,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
* @see reactive * @see reactive
*/ */
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> { export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
const node = getCurrent()!; const node = getCurrent();
if (!batchedRenderFunctions.has(node)) { if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set( batchedRenderFunctions.set(
node, node,
@@ -85,6 +85,17 @@ exports[`basics simple catchError 2`] = `
}" }"
`; `;
exports[`can catch errors calling a hook outside setup should crash 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors can catch an error in a component render function 1`] = ` exports[`can catch errors can catch an error in a component render function 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
+22
View File
@@ -374,6 +374,28 @@ describe("can catch errors", () => {
expect(mockConsoleWarn).toBeCalledTimes(0); expect(mockConsoleWarn).toBeCalledTimes(0);
}); });
test("calling a hook outside setup should crash", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(() => {
this.state = useState({ value: 2 });
});
}
}
let e: any = null;
try {
await mount(Root, fixture);
} catch (error) {
e = error;
}
expect(e.message).toBe(
"No active component (a hook function should only be called in 'setup')"
);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => { test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component { class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`; static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;