Репо, связанное с этой статьей, можно найти здесь

Недавно на работе я разработал компонент React с фиксированной высотой, но содержимое которого может иметь большую высоту, чем родительский div. Div содержит переменное количество входов, количество которых определяется пользователем, который может добавить вход, нажав кнопку. Я применил свойство overflow: scroll css, чтобы пользователь мог прокрутить вниз и использовать добавленный ввод. Вот упрощение компонента:

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

Начните с создания двух ссылок. Давайте вызовем первый prevLengthRef, и мы установим для него начальную длину нашего массива inputs. Затем мы создадим экземпляр другой ссылки с именем containerRef и инициируем его с помощью null. Наконец, мы создадим еще одну переменную с именем prevLength и установим для нее значение prevLengthRef.current. На данный момент это кажется излишним, но через мгновение это обретет смысл.

function View() {
  const [inputs, setInputs] = useState([])
  const prevLengthRef = useRef(inputs.length)
  const containerRef = useRef(null)
  const prevLength = prevLengthRef.current
  ...
}

Далее мы воспользуемся useEffect, чтобы сравнить значения prevLengthRef и inputs.length, чтобы определить, следует ли выполнять автопрокрутку после рендеринга. Мы хотим, чтобы это сравнение производилось каждый раз, когда изменяется длина массива inputs. Итак, давайте поместим это значение в наш массив deps.

function View() {
  ...
  useEffect(() => {
    //some logic to perform autoscroll
  }, [inputs.length])
  ...
}

Мы заявляем, что хотим активировать обратный вызов и, возможно, автопрокрутку, в любое время, когда свойство .length для inputs изменится. На самом деле мы действительно хотим, чтобы автопрокрутка происходила только при увеличении длины . Мы не можем выполнить такую ​​проверку в массиве dep, поэтому мы поместим эту логику в обратный вызов. Здесь мы воспользуемся объявленной выше, казалось бы, избыточной переменной - prevLength. Эта переменная содержит ссылку на значение inputs.length предыдущего рендеринга. Внутри обратного вызова useEffect мы сравниваем это значение с текущим значением inputs.length, и если оно меньше, мы знаем, что добавили элемент ввода, и выполняем автопрокрутку. В конце обратного вызова мы устанавливаем для свойства .current нашего ref значение inputs.length в ожидании следующего рендеринга и сравнения.

function View() {
  ...
  const prevLength = prevLengthRef.current
  useEffect(() => {
    if (prevLength > length) {
      //some logic to perform autoscroll   
    }
    prevLengthRef.current = length
  }, [inputs.length])
  ...
}

Большинство фигур уже есть. Теперь нам нужно написать логику для фактического изменения нашей DOM и прокрутки вниз нашего div. Первое, что нужно сделать, это передать containerRef как свойство ref в нашем контейнере ввода.

function View() {
  ...
  return (
    <div className="view-container">
      <div ref={containerRef} className="inputs-container">
  ...
}

Внутри нашего useEffect мы будем выполнять операцию с ref. Все очень просто. Свойство .current нашего containerRef указывает на узел div и, как таковое, включает свойства .scrollHeight и .scrollTop. scrollTop - это измерение расстояния от верха элемента до его самого верхнего видимого содержимого. scrollHeight - это значение, представляющее высоту элементов, включая пиксели, которые не находятся в поле зрения. Если мы установим scrollTop в scrollHeight, это создаст эффект автопрокрутки ✨.

function View() {
  ...
  const prevLength = prevLengthRef.current
  useEffect(() => {
    if (prevLength > length) {
     const container = containerRef.current
     container.scrollTop = container.scrollHeight
    }
    prevLengthRef.current = length
  }, [inputs.length])
  ...
}

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

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

Абстрагирование этого в обобщенную функцию требует, чтобы вы спросили себя;

Какая информация, относящаяся к компоненту, должна быть передана ловушке?

Какую информацию генерирует ловушка, которую необходимо использовать компоненту для достижения желаемого эффекта?

Какая логика действительно универсальна и может оставаться в пределах ловушки?

Глядя на наш внутрикомпонентный набор хуков, составляющих автопрокрутку, мы видим, что значение, запускающее набор событий, равно inputs.length. Это то, что мы установили для prevLength, это значение в массиве useEffect dep, и это значение внутри нашего условного выражения, которое определяет, должна ли выполняться автопрокрутка - и значение создается на основе количества элементов в данном компоненте. Следовательно, это значение - информация, необходимая для передачи в наш настраиваемый хук - аргумент функции.

function useAutoscroll(length) {
  // ???
}

Отлично - мы определили, каким должен быть наш ввод для хука. Давайте проследим, что на выходе должно быть. Результатом является значение, созданное специально для компонента, который его использует. Глядя на наш компонент <View/>, мы видим, что фактическое значение, генерируемое нашими хуками, которые использует jsx, - это containerRef. Это ref, который мы передали в наш div, который позволяет использовать автопрокрутку. Это ссылка на вывод нашей пользовательской функции перехвата. Давайте дадим ему более общее название.

function useAutoscroll(length) {
  const elementRef = useRef(null)
  // ???
  return elementRef
}

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

Теперь давайте воспользуемся в нашем <View2/> компоненте - компоненте, который почти идентичен <View/>, но использует раскрывающиеся списки вместо входных данных.

Мы собрали практически всю логику нашего хука в специальный хук. Он сократил строки кода для нашего компонента, действительно прост в использовании, и теперь его можно использовать для любого компонента. Здесь он работает вместе с <View/> - каждый из них использует свой собственный крючок и полностью независим друг от друга.

Спасибо за прочтение!

Репо, связанное с этой статьей, можно найти здесь