🦉 Хуки 🦉

Содержание

Введение

Хуки были популяризированы React как способ решения следующих проблем:

  • помочь повторно использовать логику состояния между компонентами
  • помощь в организации кода по функциям в сложных компонентах
  • использовать состояние в функциональных компонентах, без написания класса.

Хуки Owl служат той же цели, за исключением того, что они работают с компонентами класса (примечание: хуки React не работают с компонентами класса, и, возможно, из-за этого, кажется, существует неправильное представление о том, что хуки противопоставлены классу. Это явно не так, как показано хуками Owl).

Хуки отлично работают с компонентами Owl: они решают проблемы, упомянутые выше, и, в частности, являются идеальным способом сделать ваш компонент реактивным.

Пример: позиция мыши

Вот классический пример нетривиального хука для отслеживания положения мыши.

const { useState, onMounted, onWillUnmount } = owl.hooks;

// Здесь мы определяем пользовательское поведение: этот хук отслеживает
// состояние позиции мыши.
function useMouse() {
  const position = useState({ x: 0, y: 0 });

  function update(e) {
    position.x = e.clientX;
    position.y = e.clientY;
  }
  onMounted(() => {
    window.addEventListener("mousemove", update);
  });
  onWillUnmount(() => {
    window.removeEventListener("mousemove", update);
  });

  return position;
}

// Основной корневой компонент
class App extends owl.Component {
  static template = xml`
    <div t-name="App">
      <div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
    </div>`;

  // этот хук привязан к свойству 'mouse'.
  mouse = useMouse();
}

Обратите внимание, что мы используем префикс use для хуков, как и в React. Это просто условность.

Пример: автофокус

Хуки можно комбинировать для создания нужного эффекта. Например, следующий хук объединяет хук useRef с функциями onPatched и onMounted, чтобы создать простой способ навести фокус на input всякий раз, когда он появляется в DOM:

function useAutofocus(name) {
  let ref = useRef(name);
  let isInDom = false;
  function updateFocus() {
    if (!isInDom && ref.el) {
      isInDom = true;
      ref.el.focus();
    } else if (isInDom && !ref.el) {
      isInDom = false;
    }
  }
  onPatched(updateFocus);
  onMounted(updateFocus);
}

Этот хук берет имя допустимой директивы t-ref, которая должна присутствовать в шаблоне. Затем он проверяет всякий раз, когда компонент монтируется или патчится, недействительна ли ссылка, и в этом случае фокусируется на элементе узла. Этот хук можно использовать так:

class SomeComponent extends Component {
  static template = xml`
    <div>
        <input />
        <input t-ref="myinput"/>
    </div>`;

  constructor(...args) {
    super(...args);
    useAutofocus("myinput");
  }
}

Описание элементов

Одно правило

Есть только одно правило: каждый хук для компонента должен вызываться в конструкторе, в методе setup или в полях класса:

// ok
class SomeComponent extends Component {
  state = useState({ value: 0 });
}

// все еще ok
class SomeComponent extends Component {
  constructor(...args) {
    super(...args);
    this.state = useState({ value: 0 });
  }
}

// все еще ok
class SomeComponent extends Component {
  setup() {
    this.state = useState({ value: 0 });
  }
}

// не ok: это выполняется после вызова конструктора
class SomeComponent extends Component {
  async willStart() {
    this.state = useState({ value: 0 });
  }
}

Как видите, хуку useState не нужно указывать ссылку на компонент. Это потому что есть способ получить ссылку на текущий компонент: статическое свойство Component.current — это ссылка на экземпляр компонента, который создается в данный момент.

Хуки необходимо вызывать в конструкторе, чтобы убедиться, что ссылка установлена правильно. Такой подход хорош как с точки зрения производительности (Owl может использовать его для своей оптимизации) так и чистой архитектуры (это облегчает разработчикам понимание того, что на самом деле происходит в компоненте).

useState

Хук useState, безусловно, самый важный хук для компонентов Owl: это то, что позволяет компоненту быть реактивным т.е. реагировать на изменение состояния.

Хук useState должен принимать объект или массив и будет возвращать его наблюдаемую версию (используя Proxy).

const { useState } = owl.hooks;

class Counter extends owl.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++;
  }
}

Важно помнить, что useState работает только с объектами или массивами. Это необходимо так как Owl должен реагировать на изменение состояния.

onMounted

onMounted — это не пользовательский хук, а строительный блок, предназначенный для помощи в создании полезных абстракций. onMounted регистрирует обратный вызов, который будет вызываться при монтировании компонента (см. пример вверху этой страницы).

onWillUnmount

onWillUnmount — это не пользовательский хук, а строительный блок, предназначенный для помощи в создании полезных абстракций. onWillUnmount регистрирует обратный вызов, который будет вызываться при размонтировании компонента (см. пример вверху этой страницы).

onWillPatch

onWillPatch — это не пользовательский хук, а строительный блок, предназначенный для помощи в создании полезных абстракций. onWillPatch регистрирует обратный вызов, который будет вызываться непосредственно перед патчем компонента.

onPatched

onPatched — это не пользовательский хук, а строительный блок, предназначенный для помощи в создании полезных абстракций. onPatched регистрирует обратный вызов, который будет вызываться сразу после патчем компонента.

onWillStart

onWillStart — это асинхронный хук. Это означает, что функция, зарегистрированная в хуке, будет запущена непосредственно перед первым рендерингом компонента и может вернуть промис, чтобы выразить тот факт, что это асинхронная операция.

Обратите внимание, что если есть более одного зарегистрированного обратного вызова onWillStart, то все они будут выполняться параллельно.

Его можно использовать для загрузки некоторых исходных данных. Например, следующий хук автоматически загрузит данные с сервера и вернет объект, который будет готов каждый раз когда компонент будет отрендерен:

function useLoader() {
  const component = Component.current;
  const record = useState({});
  onWillStart(async () => {
    const recordId = component.props.id;
    Object.assign(record, await fetchSomeRecord(recordId));
  });
  return record;
}

Обратите внимание, что в этом примере значение записи не обновляется при каждом обновлении пропсов. В этой ситуации нам нужно использовать хук onWillUpdateProps.

onWillUpdateProps

Как и onWillStart, onWillUpdateProps является асинхронным хуком. Он предназначен для запуска при каждом обновлении пропсов компонента. Это может быть полезно для выполнения некоторых асинхронных задач, таких как получение обновленных данных.

function useLoader() {
  const component = Component.current;
  const record = useState({});

  async function updateRecord(id) {
    Object.assign(record, await fetchSomeRecord(id));
  }

  onWillStart(() => updateRecord(component.props.id));
  onWillUpdateProps((nextProps) => updateRecord(nextProps.id));

  return record;
}

Обратите внимание, что если есть более одного зарегистрированного обратного вызова onWillUpdateProps, то все они будут выполняться параллельно.

useContext

См. useContext в документации.

useRef

Хук useRef полезен, когда нам нужен способ взаимодействия с некоторой внутренней частью компонента, отрендеренного Owl. Он может работать как с DOM-узлом, так и с компонентом, помеченным директивой t-ref:

<div>
    <div t-ref="someDiv"/>
    <SubComponent t-ref="someComponent"/>
</div>

В этом примере компонент сможет получить доступ к div и компоненту SubComponent с помощью хука useRef:

class Parent extends Component {
  subRef = useRef("someComponent");
  divRef = useRef("someDiv");

  someMethod() {
    // здесь, если компонент смонтирован, ссылки активны:
     // - this.divRef.el является HTMLElement div
     // - this.subRef.comp является экземпляром подкомпонента
     // - this.subRef.el является корневым HTML-узлом подкомпонента (т.е. this.subRef.comp.el)
  }
}

Как показано в приведенном выше примере, доступ к элементам html осуществляется с помощью ключа el, а доступ к ссылкам на компоненты осуществляется с помощью comp.

Примечания:

  • если используется на компоненте, ссылка будет установлена в переменной refs между willPatch и patched,
  • для компонента доступ к ref.el приведет к получению корневого узла компонента.

Директива t-ref также принимает динамические значения с интерполяцией строк (например, директивы t-attf- и директивы t-component). Например,

<div t-ref="component_{{someCondition ? '1' : '2'}}"/>

Здесь ссылки должны быть установлены следующим образом:

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

Ссылки гарантированно активны только тогда, когда смонтирован родительский компонент. Если это не так, доступ к el или comp вернет null.

useSubEnv

Окружение иногда полезно для обмена общей информацией между всеми компонентами. Но иногда мы хотим объединить эти знания на дочернее дерево.

Например, если у нас есть компонент представления формы, возможно, мы хотели бы сделать некоторый объект модели доступным для всех дочерних компонентов, но не для всего приложения. Здесь может быть полезен хук useSubEnv: он позволяет компоненту добавлять некоторую информацию в окружение таким образом, что только компонент и его дочерние элементы могут получить к ней доступ:

class FormComponent extends Component {
  constructor(...args) {
    super(...args);
    const model = makeModel();
    useSubEnv({ model });
  }
}

useSubEnv принимает один аргумент: объект, который содержит некоторый ключ/значение, которое будет добавлено в родительское окружение. Обратите внимание, что он будет расширять, а не заменять родительское окружение. И, конечно же, родительское окружение не подвергентся изменениям.

useExternalListener

Хук useExternalListener помогает решить очень распространенную проблему: добавление и удаление слушателя на цели всякий раз, когда компонент монтируется/размонтируется. Например, выпадающему меню (или его родителю) может потребоваться прослушивание события click в window чтобы закрыть его:

useExternalListener(window, "click", this.closeMenu);

useStore

Хук useStore — это точка входа компонента для подключения к хранилищу. Дополнительную информацию см. в документации по объекту Store(Хранилище).

useDispatch

Хук useDispatch — это способ для компонентов получить ссылку на функцию dispatch хранилища. Дополнительную информацию см. в документации по объекту Store(Хранилище).

useGetters

Хук useGetters — это способ для компонентов получить ссылку на геттеры хранилища. Дополнительную информацию см. в документации по объекту Store(Хранилище).

useComponent

Хук useComponent полезен как строительный блок для некоторых настраиваемых хуков, которым может потребоваться ссылка на вызывающий их компонент.

useEnv

Хук useEnv полезен в качестве строительного блока для некоторых настраиваемых хуков, которым может потребоваться ссылка на env вызывающего их компонента.

Создание индивидуальных хуков

Хуки — прекрасный способ организовать код сложного компонента по функциям, а не по методам жизненного цикла. Они похожи на миксины, за исключением того, что их можно легко скомпоновать вместе.

Но, как и все хорошие вещи в жизни, хуки следует использовать с умеренностью. Они не являются решением всех проблем.

  • они могут быть излишними: если вашему компоненту нужно выполнить какое-то действие, специфичное для него самого (таким образом, конкретный код не должен быть общим), нет ничего плохого в простом методе класса:

    // может быть перебор
    class A extends Component {
      constructor(...args) {
        super(...args);
        useMySpecificHook();
      }
    }
    
    // ok
    class B extends Component {
      constructor(...args) {
        super(...args);
        this.performSpecificTask();
      }
    }
    

    Обратите внимание, что второе решение легче расширять в дочерних компонентах.

  • их может быть сложнее протестировать: если настроенный хук вводит какую-то зависимость, то его сложнее протестировать, не выполняя каких-либо неочевидных манипуляций. Например, предположим, что мы хотим дать ссылку на роутер в хуке useRouter. Мы могли бы сделать это:

    const router = new Router(...);
    
    function useRouter() {
        return router;
    }
    

    Как видите, это не цепляет внутренности компонента. Он просто возвращает глобальный объект, который сложно имитировать.

    Лучшим способом было бы сделать что-то вроде этого: получить ссылку из среды.

    function useRouter() {
      const env = useEnv();
      return env.router;
    }
    

    Это означает, что мы даем разработчику приложения контроль над созданием роутера, и это хорошо, поэтому они могут настроить его, разделить на классы его, ... И затем, чтобы протестировать наши компоненты, мы можем просто добавить фиктивный маршрутизатор в окружение.