Как создать React компонент TextareaAutoSize с автоматическим изменением высоты

Мы рассмотрим, как создать компонент Textarea, который автоматически изменяет свою высоту в зависимости от количества введенного текста.
Готовый код доступен на GitHub

Определение констант и типов

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

export const PADDING_COMPENSATION = 24; export const INITIAL_SCROLL_LINE_HEIGHT = 1; export const MAX_ROWS = 1000; export const INITIAL_ROWS = 3;

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

Определим типы для значений и пропсов нашего компонента TextareaAutoSize.

export type TextAreaValue = { value: string }; export type TextareaAutoSizeProps = { /** максимальное количество строк при достижении которого textarea перестает увеличиваться * по высоте и появляется скролл * */ maxRows?: number; /** кастомный value поля textarea */ value: TextAreaValue; /** кастомный обработчик события onChange поля textarea */ onChange: (data: TextAreaValue) => void; rows?: number; };

Теперь напишем сам компонент - TextareaAutoSize

import { ChangeEvent, FC } from "react"; import cx from "classnames"; import { useAutoSize } from "../utils/useAutoSize"; import styles from "./index.module.scss"; import { TextareaAutoSizeProps } from "../types"; import { INITIAL_ROWS } from "../constants"; export const TextareaAutoSize: FC<TextareaAutoSizeProps> = ({ maxRows, value, onChange, rows = INITIAL_ROWS, }) => { const { isOverflowAuto, setTextAreaHeight, textAreaHeight, textAreaRef } = useAutoSize(maxRows, value); const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => { onChange({ value: e.target.value }); setTextAreaHeight("auto"); }; const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => { const target = event.target as HTMLTextAreaElement; onChange({ value: target.value }); setTextAreaHeight("auto"); }; return ( <textarea ref={textAreaRef} className={cx(isOverflowAuto && styles["overflow-auto"], styles.textarea)} onChange={handleChange} onPaste={handlePaste} rows={rows} style={{ height: textAreaHeight, }} value={value?.value} /> ); };

И добавим стили

.textarea { resize: none; box-sizing: border-box; padding: 12px; font-weight: 400; font-size: 15px; line-height: 20px; width: 100%; } .overflow-auto { overflow: auto; }

Основные ключевые моменты без которых наша логика не будет нормально работать, это обязательное задание в стилях line-height, так как если не задать данный стиль то браузер вычислит его значение в normal, конечно же хук useAutoSize, который мы разберем ниже и атрибут style где мы сами управляем высотой нашего элемента.

Хук useAutoSize

import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { TextAreaValue } from "../types"; import { INITIAL_SCROLL_LINE_HEIGHT, MAX_ROWS, PADDING_COMPENSATION, } from "../constants"; export const useAutoSize = ( maxRows: number = MAX_ROWS, valueText: TextAreaValue ) => { const [isOverflowAuto, setIsOverflowAuto] = useState(false); const [currentLineHeight, setCurrentLineHeight] = useState( INITIAL_SCROLL_LINE_HEIGHT ); const [currentScrollHeight, setCurrentScrollHeight] = useState( INITIAL_SCROLL_LINE_HEIGHT ); const [textAreaHeight, setTextAreaHeight] = useState("auto"); const textAreaRef = useRef<HTMLTextAreaElement | null>(null); const currentMaxHeightRef = useRef<number>(0); const currentMaxRows = maxRows ?? MAX_ROWS; useEffect(() => { if (!textAreaRef.current) return; const lineHeight = parseInt( getComputedStyle(textAreaRef.current)?.lineHeight, 10 ); const scrollHeight = textAreaRef.current?.scrollHeight; setCurrentLineHeight(lineHeight); setCurrentScrollHeight(scrollHeight); currentMaxHeightRef.current = lineHeight * currentMaxRows; }, [currentMaxRows, maxRows]); useLayoutEffect(() => { if (!valueText.value) { setTextAreaHeight(`${currentScrollHeight}px`); return; } const currentCountRows = Math.floor( (Number(textAreaRef.current?.scrollHeight) - PADDING_COMPENSATION) / currentLineHeight ); if (currentCountRows < currentMaxRows) { setTextAreaHeight(`${textAreaRef.current?.scrollHeight}px`); setIsOverflowAuto(false); } else { setTextAreaHeight(`${currentMaxHeightRef.current}px`); setIsOverflowAuto(true); } }, [valueText, currentLineHeight, currentScrollHeight, currentMaxRows]); return { isOverflowAuto, setTextAreaHeight, textAreaHeight, textAreaRef, }; };

Хук useAutoSize принимает два параметра:

  • maxRows: Максимальное количество строк, после которого текстовое поле перестает увеличиваться по высоте и включается прокрутка. По умолчанию это значение равно MAX_ROWS.
  • valueText: Объект, содержащий значение текстового поля.

Затем мы объявляем состояния и рефы:

  • isOverflowAuto: Флаг для управления включением/выключением прокрутки.
  • currentLineHeight: Текущая высота строки текстового поля.
  • currentScrollHeight: Текущая высота прокрутки.
  • textAreaHeight: Высота текстового поля.
  • textAreaRef: Реф для текстового поля.
  • currentMaxHeightRef: Максимальная высота текстового поля, вычисляется исходя из высоты строки и максимального количества строк.

Этот useEffect

useEffect(() => { if (!textAreaRef.current) return; const lineHeight = parseInt(getComputedStyle(textAreaRef.current)?.lineHeight, 10); // Получаем высоту строки из стилей const scrollHeight = textAreaRef.current?.scrollHeight; // Получаем высоту прокрутки setCurrentLineHeight(lineHeight); // Устанавливаем текущую высоту строки setCurrentScrollHeight(scrollHeight); // Устанавливаем текущую высоту прокрутки currentMaxHeightRef.current = lineHeight * currentMaxRows; // Вычисляем и сохраняем максимальную высоту }, [currentMaxRows, maxRows]);

выполняется при монтировании компонента и всякий раз, когда изменяется значение maxRows. В нем мы:

  • Получаем высоту строки (lineHeight) из стилей текстового поля. Именно поэтому нам необходимо задать lineHight в px иначе браузер вернет normal и мы в итоге получим NaN.
  • Получаем текущую высоту прокрутки (scrollHeight).
  • Устанавливаем эти значения в соответствующие состояния.
  • Вычисляем и сохраняем максимальную высоту текстового поля.

В хуке useLayoutEffect

useLayoutEffect(() => { if (!valueText.value) { setTextAreaHeight(`${currentScrollHeight}px`); return; } const currentCountRows = Math.floor( (Number(textAreaRef.current?.scrollHeight) - PADDING_COMPENSATION) / currentLineHeight ); if (currentCountRows < currentMaxRows) { setTextAreaHeight(`${textAreaRef.current?.scrollHeight}px`); setIsOverflowAuto(false); } else { setTextAreaHeight(`${currentMaxHeightRef.current}px`); setIsOverflowAuto(true); } }, [valueText, currentLineHeight, currentScrollHeight, currentMaxRows]);

мы измененяем высоту текстового поля при изменении текста. Кстати я выбрал useLayoutEffect потому что без него, так как мы постоянно при вводе текста делаем setTextAreaHeight("auto") браузер не успевает пересчитать обновленные значения и установить новую высоту для textarea и происходят сайдэффекты в виде дерганий поля. Можете сами попробовать.

Итак хук useLayoutEffect выполняется каждый раз при изменении текста в текстовом поле. В нем мы:

  • Проверяем, если значение текстового поля пусто, то устанавливаем высоту поля равной текущей высоте прокрутки.
  • Вычисляем текущее количество строк в текстовом поле.
  • Если количество строк меньше максимального значения (currentMaxRows), то: Устанавливаем высоту текстового поля равной текущей высоте прокрутки. Отключаем прокрутку.
  • Если количество строк больше или равно максимальному значению, то: Устанавливаем высоту текстового поля равной максимальной высоте. Включаем прокрутку.

Возвращаемые значения из хука useAutoSize

Хук возвращает объект с четырьмя значениями:

  • isOverflowAuto: Флаг для управления включением/выключением прокрутки.
  • setTextAreaHeight: Функция для установки высоты текстового поля.
  • textAreaHeight: Текущая высота текстового поля.
  • textAreaRef: Реф для текстового поля.

Хук useAutoSize можно использовать для управления высотой текстового поля в зависимости от количества введенного текста и для включения/выключения прокрутки.

Итог

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

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

1 комментарий

Интересный пример использования хуков в React для автоматической подстройки высоты текстового поля

Ответить