Репо, связанное с этой статьей, можно найти здесь
Недавно на работе я разработал компонент 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/>
- каждый из них использует свой собственный крючок и полностью независим друг от друга.
Спасибо за прочтение!
Репо, связанное с этой статьей, можно найти здесь