home

宣言型UI-Reactドキュメント

今は当たり前にReactを書いているがドキュメントをまともに読んだことがなかったので改めて読んでみることにした。恥ずかしながらReactはプログラミングを始めたころProgateで勉強した程度。確かclassだった気がする。実務では分からないことがあったら都度ググる(今はChatGPT)いわゆるその場しのぎだったかもしれない。ドキュメントを読んでみてたくさん学びがあった。その中でもReactが宣言型UIという部分がとても参考になったので今回はそれについてメモ程度に書いてみる。そもそも今までReactが宣言型UIなんて知らなかったし、もちろんそれを意識してReactを書いたことがなかった。そもそも宣言型UIってなに…って感じ。が、ある程度理解し宣言型UIを意識すると効率的にコンポーネント作成ができる様になった。具体的にはUIを宣言的に考えることであらかじめUIパターンを洗い出すようになり、そこでデザインパターンの不足や不備に気づける。そのおかげで何度も着工前にデザインの不備に気づき無駄なコーディングを削減できた。UIを宣言的に考えることはデザイナーの視点も取り入れることかもしれない。

Reactドキュメント ↓

https://ja.react.dev/learn/reacting-to-input-with-state

宣言型UIってなに

宣言型プログラミングとは、UI を細かく管理する(命令型)のではなく、視覚状態ごとに UI を記述することを意味する。

この視覚状態というのがかなり大事なので噛み砕いて説明していく。

視覚状態を特定する

例えば以下のフォームのデザインを渡されたときで考える。(画像はドキュメント引用)

宣言型UIでは、まずこのフォームの視覚状態を特定する。

視覚状態というのはデザインパターンのようなものかと思っている。例えばSubmitボタンで考えると、フォームに何も入力していないときは送信できないようにボタンを押せなくする必要がある。逆にフォームに文字が入力されていれば送信できるようにボタンを押せるようにする必要がある。このように状態によってUIが変わることを視覚状態が変わると思った方がいい。

このフォーム場合、想定できる視覚状態は以下の5つ。

それらをコードで表現してみる。

フォームに文字が入力されているときにSubmitボタンが押せる

export default function Form({ status }) {
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Submit
        </button>
      </form>
    </>
  )
}

フォームに何も入力されていないときボタンを押せなくする

export default function Form({ status }) {
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button disable={status === 'empty'}>
          Submit
        </button>
      </form>
    </>
  )
}

Submitボタンを押して送信が結果が返ってきるまでボタンを押せなくする

export default function Form({ status }) {
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button disable={status === 'empty' || 'submiting'}>
          Submit
        </button>
      </form>
    </>
  )
}

送信が成功したとき

export default function Form({ status }) {
	if (status === 'success') {
	  return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button disable={status === 'empty' || 'submiting'}>
          Submit
        </button>
      </form>
    </>
  )
}

送信が失敗したとき

export default function Form({ status }) {
	if (status === 'success') {
	  return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button disable={status === 'empty' || 'submiting'}>
          Submit
        </button>
        {status === 'error' &&
          <p>
            Good guess but a wrong answer. Try again!
          </p>
        }
      </form>
    </>
  )
}

こんな感じになる。

※ここで渡されたデザインに視覚状態が加味されていない場合はデザイナーに相談してデザインを作成してもらうなりしてデザインを確定してからコーディングするのが望ましい。(状況にもよるが)ふわふわした状態でエンジニアがよしなに実装してしまうと考慮漏れや人によってデザインが変わる等プロダクトにとっても良くないと思った。

視覚状態が特定できたら、次はその視覚状態を変更するトリガーを決定する。

視覚状態を変更するトリガーを決定する

ここで先ほど特定した視覚状態ををもとにトリガーを特定する。

トリガーが特定できたらあとはコードに落とし込む。

フォームが空かどうかtypingemptyを切り替える

const [answer, setAnswer] = useState('');
// フォームが空かどうかは以下で判定
answer.length === 0

Submitボタンをクリックしたかどうかによって submiting ****を切り替える

const [status, setStatus] = useState('typing');

function handleSubmit(e) {
	e.preventDefault();
	// submitしたらstatusを'submitting'にする
	setStatus('submitting');
}

送信が成功したかどうかsuccess を切り替える

送信が失敗したかどうかerror を切り替える

const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');

async function handleSubmit(e) {
  e.preventDefault();
  setStatus('submitting');
  try {
    await submitForm(answer);
    // 送信成功したらstatusを'success'にする
    setStatus('success');
  } catch (err) {
	  // 送信失敗したらstatusを'typing'にしerrorをセットする
    setStatus('typing');
    setError(err);
  }
}

これらを全て加味すれば出来上がり

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Monkey Logo

konpay.eth

福井県出身のエンジニアです。31歳です。田舎からフロントエンドを開発しています。TpeScript, React をよく書きます。バックエンドは Node.js をお遊び程度で書いています。将来的には TypeScript と Node.js でフルスタック開発をしたいです。仮想通貨や NFT など Web3.0 領域が好きです。