Skip to content

Latest commit

 

History

History
324 lines (249 loc) · 10.2 KB

hooks.md

File metadata and controls

324 lines (249 loc) · 10.2 KB

🦉 Hooks 🦉

Content

Overview

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.

The Hook Rule

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 });
  }
}

Lifecycle Hooks

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)

Other Hooks

useState

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.

useRef

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="someDiv"/>
    <span>hello</span>
</div>

In this example, the component will be able to access the div and the component SubComponent with the useRef hook:

class Parent extends Component {
  inputRef = useRef("someComponent");

  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="component_{{someCondition ? '1' : '2'}}"/>

Here, the references need to be set like this:

this.ref1 = useRef("component_1");
this.ref2 = useRef("component_2");

References are only guaranteed to be active while the parent component is mounted. If this is not the case, accessing el or comp on it will return null.

useSubEnv and useChildSubEnv

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 new env to itself and to all children components
  • useChildSubEnv will only assign this new env 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.

useExternalListener

The useExternalListener hook helps solve a very common problem: adding and removing a listener on some target whenever a component is mounted/unmounted. 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);

useComponent

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
}

useEnv

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
}

useEffect

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. 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");
  }
}

Example: mouse position

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.