Hooks were popularised by React as a way to solve the following issues:
- help reusing stateful logic between components
- help organizing code by feature in complex components
- use state in functional components, without writing a class.
Owl hooks serve the same purpose, except that they work for class components (note: React hooks do not work on class components, and maybe because of that, there seems to be the misconception that hooks are in opposition to class. This is clearly not true, as shown by Owl hooks).
Hooks work beautifully with Owl components: they solve the problems mentioned above, and in particular, they are the perfect way to make your component reactive.
There is only one rule: every hook for a component has to be called in the setup method, or in class fields:
// ok
class SomeComponent extends Component {
state = useState({ value: 0 });
}
// also ok
class SomeComponent extends Component {
setup() {
this.state = useState({ value: 0 });
}
}
// not ok: this is executed after the constructor is called
class SomeComponent extends Component {
async willStart() {
this.state = useState({ value: 0 });
}
}
All lifecycle hooks are documented in detail in their specific section.
Hook | Description |
---|---|
onWillStart | async, before first rendering |
onWillRender | just before component is rendered |
onRendered | just after component is rendered |
onMounted | just after component is rendered and added to the DOM |
onWillUpdateProps | async, before props update |
onWillPatch | just before the DOM is patched |
onPatched | just after the DOM is patched |
onWillUnmount | just before removing component from DOM |
onWillDestroy | just before component is destroyed |
onError | catch and handle errors (see error handling page) |
The useState
hook is certainly the most important hook for Owl components:
this is what allows a component to be reactive, to react to state change.
The useState
hook has to be given an object or an array, and will return
an observed version of it (using a Proxy
).
const { useState, Component } = owl;
class Counter extends Component {
static template = xml`
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 });
increment() {
this.state.value++;
}
}
It is important to remember that useState
only works with objects or arrays. It
is necessary, since Owl needs to react to a change in state.
The useRef
hook is useful when we need a way to interact with some inside part
of a component, rendered by Owl. It only work on a html element tagged by the
t-ref
directive:
<div>
<input t-ref="someInput"/>
<span>hello</span>
</div>
In this example, the component will be able to access the input
with the useRef
hook:
class Parent extends Component {
inputRef = useRef("someInput");
someMethod() {
// here, if component is mounted, refs are active:
// - this.inputRef.el is the input HTMLElement
}
}
As shown by the example above, the actual HTMLElement instance is accessed with
the el
key.
The t-ref
directive also accepts dynamic values with string interpolation
(like the t-attf-
and
t-component
directives). For example,
<div t-ref="div_{{someCondition ? '1' : '2'}}"/>
Here, the references need to be set like this:
this.ref1 = useRef("div_1");
this.ref2 = useRef("div_2");
References are only guaranteed to be active while the parent component is mounted.
If this is not the case, accessing el
on it will return null
.
The environment is sometimes useful to share some common information between all components. But sometimes, we want to scope that knowledge to a subtree.
For example, if we have a form view component, maybe we would like to make some
model
object available to all sub components, but not to the whole application.
This is where the useChildSubEnv
hook may be useful: it lets a component add some
information to the environment in a way that only its children
can access it:
class FormComponent extends Component {
setup() {
const model = makeModel();
// model will be available on this.env for this component and all children
useSubEnv({ model });
// someKey will be available on this.env for all children
useChildSubEnv({ someKey: "value" });
}
}
The useSubEnv
and useChildSubEnv
hooks take one argument: an object which
contains some key/value that will be added to the current environment. These hooks
will create a new env object with the new information:
useSubEnv
will assign this newenv
to itself and to all children componentsuseChildSubEnv
will only assign this newenv
to all children components.
As usual in Owl, environments created with these two hooks are frozen, to prevent unwanted modifications.
Note that both these hooks can be called an arbitrary number of times. The env
will then be updated accordingly.
The useExternalListener
hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. It takes a target
as its first argument, forwards the other arguments to addEventListener
. For example,
a dropdown menu (or its parent) may need to listen to a click
event on window
to be closed:
useExternalListener(window, "click", this.closeMenu, { capture: true });
The useComponent
hook is useful as a building block for some customized hooks,
that may need a reference to the component calling them.
function useSomething() {
const component = useComponent();
// now, component is bound to the instance of the current component
}
The useEnv
hook is useful as a building block for some customized hooks,
that may need a reference to the env of the component calling them.
function useSomething() {
const env = useEnv();
// now, env is bound to the env of the current component
}
This hook will run a callback when a component is mounted and patched, and will run a cleanup function before patching and before unmounting the the component (only if some dependencies have changed).
It has almost the same API as the React useEffect
hook, except that the dependencies
are defined by a function instead of just the dependencies.
The useEffect
hook takes two function: the effect function and the dependency
function. The effect function perform some task and return (optionally) a cleanup
function. The dependency function returns a list of dependencies, these dependencies
are passed as parameters in the effect function . If any of these
dependencies changes, then the current effect will be cleaned up and reexecuted.
Here is an example without any dependencies:
useEffect(
() => {
window.addEventListener("mousemove", someHandler);
return () => window.removeEventListener("mousemove", someHandler);
},
() => []
);
In the example above, the dependency list is empty, so the effect is only cleaned up when the component is unmounted.
If the dependency function is skipped, then the effect will be cleaned up and rerun at every patch.
Here is another example, of how one could implement a useAutofocus
hook with
the useEffect
hook:
function useAutofocus(name) {
let ref = useRef(name);
useEffect(
(el) => el && el.focus(),
() => [ref.el]
);
}
This hook takes the name of a valid t-ref
directive, which should be present
in the template. It then checks whenever the component is mounted or patched if
the reference is not valid, and in this case, it will focus the node element.
This hook can be used like this:
class SomeComponent extends Component {
static template = xml`
<div>
<input />
<input t-ref="myinput"/>
</div>`;
setup() {
useAutofocus("myinput");
}
}
Here is the classical example of a non trivial hook to track the mouse position.
const { useState, onWillDestroy, Component } = owl;
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({ x: 0, y: 0 });
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener("mousemove", update);
onWillDestroy(() => {
window.removeEventListener("mousemove", update);
});
return position;
}
// Main root component
class Root extends Component {
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
// this hooks is bound to the 'mouse' property.
mouse = useMouse();
}
Note that we use the prefix use
for hooks, just like in React. This is just
a convention.