宣言型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つ。
typing
フォームに文字が入力されているときにSubmitボタンが押せるempty
フォームに何も入力されていないときボタンが押せなくなるsubmiting
Submitボタンを押して送信が結果が返ってきるまでボタンが押せなくなるsuccess
送信が成功したとき成功メッセージを表示するerror
送信が失敗したときにエラーメッセージを表示する
それらをコードで表現してみる。
フォームに文字が入力されているときに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>
</>
)
}
こんな感じになる。
※ここで渡されたデザインに視覚状態が加味されていない場合はデザイナーに相談してデザインを作成してもらうなりしてデザインを確定してからコーディングするのが望ましい。(状況にもよるが)ふわふわした状態でエンジニアがよしなに実装してしまうと考慮漏れや人によってデザインが変わる等プロダクトにとっても良くないと思った。
視覚状態が特定できたら、次はその視覚状態を変更するトリガーを決定する。
視覚状態を変更するトリガーを決定する
ここで先ほど特定した視覚状態ををもとにトリガーを特定する。
- フォームが空かどうかで
typing
とempty
を切り替える - Submitボタンをクリックしたかどうかによって
submiting
を切り替える - 送信が成功かどうかで
success
を切り替える - 送信が失敗かどうかで
error
を切り替える
トリガーが特定できたらあとはコードに落とし込む。
フォームが空かどうかでtyping
とempty
を切り替える
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);
});
}