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

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

Правила для пользовательских React хуков довольно просты:

Пользовательский хук — это функция JavaScript, имя которой начинается с "use" и которая может вызывать другие хуки.

Обычные хуки

Рассмотрим эту очень наивную реализацию пользовательского хука, который инкапсулирует отображение разрешений для некоторого общего сервиса документов:

import React from 'react';
import { Permissions, usePermissions } from '@hooks/permissions';

const useIsPermitted = () => {
  // usePermissions is another custom hook that returns an array of permissions
  const permissions = usePermissions();
  return {
    isEditPermitted: permissions.includes(Permissions.EDIT_SITE_PERMISSION),
  }
}

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

import React from 'react';

export const EditButton = () => {
  const { isEditPermitted } = useIsPermitted();
  return <button disabled={!isEditPermitted}>Edit</button>
}

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

import React, { useMemo } from 'react';
import { Permissions, usePermissions } from '@hooks/permissions';

const useIsPermitted = () => {
  // usePermissions is another custom hook that returns an array of permissions
  const permissions = usePermissions();
  return useMemo(() => ({
    isEditPermitted: permissions.includes(Permissions.EDIT_SITE_PERMISSION),
  }), [permissions]);
}

Таким образом, разрешения будут повторно сопоставлены только при изменении массива разрешений.

Задача решена! Или нет?

Официальная документация React может подсказать нам, что же может быть не так с этим подходом:

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

Как пользовательский хук получает изолированное состояние? Каждый вызов хука получает изолированное состояние. Поскольку мы вызываем useFriendStatus напрямую, с точки зрения React наш компонент просто вызывает useState и useEffect. И, как мы узнали ранее, мы можем вызывать useState и useEffect много раз в одном компоненте, и они будут полностью независимы.

Фактически это означает, что если мы вызываем useIsPermitted из двух разных компонентов (или даже дважды из одного и того же компонента), логика будет выполняться для каждого случая вызова useIsPermitted, даже если внутри мы используем useMemo.

Пользовательские хуки с контекстом

Решением этой проблемы может стать комбинирование пользовательского хука с контекстом.  

Давайте пересмотрим реализацию нашего хука useIsPermitted:

import React, { createContext, useMemo } from 'react';
import { Permissions, usePermissions } from '@hooks/permissions';

export const PermissionsContext = createContext({});

export const IsPermittedProvider: React.FC = ({ children }) => {
  const permissions = usePermissions();
  const permissionsDictionary = useMemo(() => ({
    isEditPermitted: permissions.includes(Permissions.EDIT_SITE_PERMISSION),
  }), [permissions]);
  
  return (
    <PermissionsContext.Provider
      value={permissionsDictionary}
    >
      {children}
    </PermissionsContext.Provider>
  );
};
import React, { useContext } from 'react';
import { PermissionsContext } from '@contexts/permissions';

export const useIsPermitted = () => useContext(PermissionsContext);

Теперь логика привязана к конкретному провайдеру контекста. Это означает, что если мы используем данного провайдера только однажды в корне приложения, то логика будет выполнена один раз:

import React from 'react';
import { App } from './app';

<PermissionsProvider>
  <App />
</PermissionsProvider> 

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

Что и когда использовать

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

Есть место для обоих подходов, но важно понимать последствия каждого из них.  

Вот краткий перечень, который поможет вам выбрать правильный метод:

Используйте обычный хук, когда:

  • Состояние пользовательского хука должно быть изолированным (разным для каждого экземпляра) ИЛИ

  • В пользовательском хуке не выполняются тяжелые вычисления ИЛИ

  • Вы используете хук только один раз в приложении (как способ передачи данных по дереву рендеринга).

Используйте хук с контекстом, когда:

  • Все поддерево должно разделять состояние хука ИЛИ

  • Внутри хука выполняются тяжелые вычисления, и вы хотите, чтобы они выполнялись как можно реже.


Материал подготовлен в рамках курса «JavaScript Developer. Professional».

Node.js — это асинхронная среда исполнения JavaScript, в основе которой находятся такие понятия как «Event Loop» и событийно-ориентированная архитектура. 2 года назад основатель Node.js Ryan Dahl заявил о создании Deno (палиндром Node) — альтернативной серверной среды исполнения JavaScript и TypeScript. Некоторые концепции остались схожими, а некоторые были придуманы и воплощены с нуля.

Всех желающих приглашаем на двухдневный онлайн-интенсив «Знакомство и сравнение возможностей Deno и Node.js». Цель интенсива — рассмотреть обе среды с точки зрения ключевых возможностей, паттернов, инструментов. Мы сравним примеры небольших, но актуальных приложений и попробуем понять, что представляет из себя современное Server Side окружение JavaScript / TypeScript. 

РЕГИСТРАЦИЯ

Комментарии (0)