🦉 Хуки 🦉
Содержание
Введение
Хуки были популяризированы 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; }
Это означает, что мы даем разработчику приложения контроль над созданием роутера, и это хорошо, поэтому они могут настроить его, разделить на классы его, ... И затем, чтобы протестировать наши компоненты, мы можем просто добавить фиктивный маршрутизатор в окружение.