みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Reactでtextareaの高さをテキスト量に応じて自動で調整する 📝




結論

react-textarea-autosize に任せる。

github.com


自作編

基本的な方針

  1. textareaのvalueの変更を検知する
  2. textareaのheightをautoにする
  3. textareaのscrollHeightを計測する
  4. textareaのheightにscrollHeightを代入する

textareaの初期値がautoであれば上記方針で基本的には問題ないです。


JavaScript(抜粋)
const TextArea = () => {
  const [ value, setValue ] = React.useState('');
  const [ height, setHeight ] = React.useState(0);
  const textAreaRef = React.useRef(null);

  React.useEffect(() => {
    if (textAreaRef.current) {
    	setHeight(0); // テキストエリアの高さを初期値に戻す
    }
  }, [value]);

  React.useEffect(() => {
    // 高さが初期値の場合にscrollHeightを高さに設定する
    if (!height && textAreaRef.current) {
      setHeight(textAreaRef.current.scrollHeight);
    }
  }, [height]);

  function handleChangeValue(value) {
    setValue(value);
  }

  return (
    <textarea
      ref={ textAreaRef }
      value={ value }
      onChange={ (evt) => handleChangeValue(evt.target.value) }
      style={{ height: height ? `${ height }px` : 'auto' }}
    />
  );
};



DEMO

実際に試してみるとわかるのですが、高さが一瞬初期値に戻るため、入力のたびにガクガクします。
基本的な方針はOKなのですが、これでは見栄えが良くありません。


応用的な方針

基本的な方針はそのままに、高さ計算用のDOMと表示用のDOMを分けてみます。


JavaScript(抜粋)
const TextArea = () => {
  const [ value, setValue ] = React.useState('');
  const [ height, setHeight ] = React.useState(0);
  const textAreaRef = React.useRef(null);
  const invisibleTextAreaRef = React.useRef(null);

  React.useEffect(() => {
    if (invisibleTextAreaRef.current) {
      setHeight(invisibleTextAreaRef.current.scrollHeight);
    }
  }, [value]);

  function handleChangeValue(value) {
    setValue(value);
  }

  return (
    <React.Fragment>
      <textarea // 表示用テキストエリア
        ref={ textAreaRef }
        value={ value }
        onChange={ (evt) => handleChangeValue(evt.target.value) }
        style={{ height: height ? `${ height }px` : 'auto' }}
      />
      <textarea // 高さ計算用テキストエリア
        ref={ invisibleTextAreaRef }
        value={ value }
        onChange={ () => {} }
        tabindex="-1" // tabでフォーカスが当たらないようにする
        style={{ position: 'fixed', top: -999 }} // 見えない範囲へ移動
      />
    </React.Fragment>
  );
};



DEMO

ガクガクしなくなりました。一見問題なさそうです。


他人のふんどし編

応用的な方針だと、textareaを常に2つでワンセットで扱うため、単純に数が倍になってしまいます。
textareaをたくさん設置するページだと、無駄なtextareaまみれになってしまいますね。
また、幅、高さを調整する場合、計算用、表示用両方を同じ値にしなければならないので、Propsで幅、高さを受け取るなどの工夫も必要そうです。

面倒だけど、もろもろ調整するかと思っていたそのとき、便利なライブラリを教えてもらいました。

github.com

ソースを確認したところ、方針としては計算用、表示用のtextareaを用意するというものでしたが、計算用は1つだけ用意し、すべてのテキストエリアの高さを任せているようです。非常に合理的です。

結果として、今後はこちらのライブラリを活用していこうと思いました。