lollipop.onl

Vueのようなリアクティブを提供する React Hooks

JavaScript
更新日:

はじめに

※この記事は React でも Vue っぽい実装したい!という思いを元に作っています。著者は React も Vue も好きです。

普段 Vue を使っている著者は、ReactでのStateの変更をやや煩雑に感じてしまいます。
この記事では、ReactでもVueのようなリアクティブを提供するHooksの実装を紹介します。

小規模で、フォームフィールドが多いようなアプリケーションではコードの見通しをよくできるのではないかなと思います。

React で煩雑に感じること

ReactでStateを<input />で変更できるようにしようとすると、次のような実装になります。

JavaScriptconst App = () => {
  const [value, setValue] = useState('');

  return (
    <input
      defaultValue={value}
      onInput={(e) => setValue(e.target.value)}
    />
  );
};

defaultValueで初期値を指定して、onInputで入力値を同期するという実装が必要です。

これを Vue で書くと次のようになります。

Plain Text<template>
  <input v-model="value" />
</template>

<script>
  export default {
    data: {
      value: '',
    }
  }
</script>

周辺がごちゃごちゃしますが、本質的にはv-model="value"のみです。
このリアクティブが個人的に思う Vue の強みです。

Vue っぽいリアクティブを React でもやる Hooks

Vue っぽくするためには、<input />に Props を指定したら、 <input /> 側で State を変更できるようにしなければなりません。

ここでは、useReactiveStateという値と更新関数を一つの State に持たせる Hooks を作成します。

/hooks/index.tsimport { useState } from 'react';

export type ReactiveState<T> = {
  value: T;
  update: (value: T) => void;
};

export const useReactiveState = <T>(initialValue: T): ReactiveState<T> => {
  const [value, setValue] = useState<T>(initialValue);

  return {
    value,
    update: setValue,
  };
};

この Hooks は state.value で State の値にアクセスでき、state.update(newValue)で State を更新できます。

Vue っぽいリアクティブを React でもやるコンポーネント

ReactiveState を Props に取り、コンポーネント内で状態の更新まで行うInputライクなコンポーネント、AppInputを作成します。

/components/AppInput.tsximport React, { FC } from 'react';
import { ReactiveState} from '../hooks';

type Props = {
  value: ReactiveState;
};

export const AppInput: FC<Props> = ({ value }) => {
  const onInput = (e: FormEvent<HTMLInputElement>) => {
    if (e.target instanceof HTMLInputElement) {
      value.update(e.target.value);
    }
  };

  return <input
    defaultValue={value.value}
    onInput={onInput}
  />;
};

このコンポーネントでは<input />onInputのハンドラーをコンポーネント内に閉じ込め、コンポーネントから直接 Props を更新しています。

useReactiveState と AppInput を使ってみる

作成したuseReactiveStateAppInputを使ってみましょう。

/containers/App.tsximport { useReactiveState } from '../hooks';
import { AppInput } from '../components/AppInput';

export const App = () => {
  const name = useReactiveState('');

  return (
    <div>
      <AppInput value={name} />
      <p>{name.value}</p>
    </div>
  );
};

useReactiveStateを使用して生成したnameAppInputvalueとして指定しています。
これだけでnameAppInputの入力状態と同期できます。

実際に使った例

パスポートのMRZをシミュレートするツールで、Inputとのデータのやり取りでuseReactiveStateを使用しました。

GitHub lollipop-onl/mrz.lollipop.onl https://github.com/lollipop-onl/mrz.lollipop.onl/blob/master/src/scripts/containers/App/index.tsx

GitHub lollipop-onl/mrz.lollipop.onl https://github.com/lollipop-onl/mrz.lollipop.onl/blob/master/src/scripts/components/AppInput/index.tsx

おわりに

使用箇所が少ないとあまり旨味がありませんが、フォームがたくさんあるようなアプリケーションでは Input ひとつひとつの記述がシンプルになります。

ただ、値の表示がstate.valueとプロパティを指定しなければならないのが直感的ではないので、もっと良い方法がないか模索中です。

編集