この記事について
この記事では、ReactHooksの中のカスタムフックを利用する際、
私が考えるリファクタリングの進め方を見ていきます。
「もっとこうしたほうがいいよ」という方法がありましたら、ぜひ、ご教授いただければ幸いです。
対象読者
- React Hooksの書き方に悩んでいる人
- React Hooksの基本方針を知りたい人
対象外の内容
- useEffetcやuseContextなどの処理は含みません
基本形
まずは、ベースとなるアプリから
const TodoApp = () => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const handleAddTodo = (e) => {
...validation処理
const newTodo = { title, description };
// TODO todoを使った処理
}
return (
<div>
<input value={title} onChange={(e)=>setTitle(e.target.value)} />
<input value={description} onChange={(e)=>setDescription(e.target.value)} />
</div>
);
}
これをベースとします。
最終形
const useTodo = () => {
const [todo, setTodo] = useState({ title: "", description: ""});
const [errors, setErrors] = useState({});
const validate = () => {
let newErrors = {};
if (!todo.title) newErrors.title = 'タイトルは必須です';
if (todo.description > 10) newErrors.description = '説明は最低10文字入力してください';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
const addTodo = () => {
// TODO todoを使った処理
}
const setTitle = (title) => {
setTodo((prev) => ({...prev, title}));
}
const setDescription = (description) => {
setTodo((prev) => ({...prev, description }));
}
return { todo, setTitle, setDescription, addTodo, validate, errors };
}
const TodoApp = () => {
const { todo, setTitle, setDescrition, addTodo, validate, errors } = useTodo();
const handleAddTodo = (e) => {
if (!validation) return;
addTodo();
}
return (
<div>
// errorsを使ってエラー処理を行う
<input value={todo.title} onChange={(e)=> setTitle(e.target.value)} />
<input value={todo.description} onChange={(e)=> setDescription(e.target.value)} />
</div>
);
}
最終形はこんな感じ。
行数は増えてますが、再利用性などを考えるとこちらの書き方が良いと思います。
そもそも問題点は?
まず、基本形でいいのではないか?という疑問を見ていきます。
TodoApp規模では問題ないのですが、これが大きな規模になるとやはり問題があります。
例えば、TodoAppにAI機能を付与するとします。
すると、titleやdescriptionを使って、handle系が沢山増え、
また機能を増やしてはhandleが増え、
……最終的にはTodoAppのコードが肥大化していきます。
TodoAppはあくまでもTodo機能的なものを表示する場所に抑えたいです。
そこで一つ指針を立てます。
「Hooksを利用する側が汚れないようにする」というものです。
それを基本指針として、基本系を修正していきます。
では、よろしくお願いいたします。
まずは基本的なカスタムフックを考える
最初は基本的なカスタムフックの考え方です。
基本系を次のように、単純なstateをまとめるだけの、変更を行います。
const useTodo = () => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
return { title, setTitle, descrpition, setDescription };
}
const TodoApp = () => {
const { title, setTitle, description, setDescrition } = useTodo();
const handleAddTodo = (e) => {
...validation処理
const newTodo = { title, description };
// TODO todoを使った処理
}
return (
<div>
<input value={title} onChange={(e)=>setTitle(e.target.value)} />
<input value={description} onChange={(e)=>setDescription(e.target.value)} />
</div>
);
}
これで、todoのstateはuseTodoを見るだけで良くなりました。
stateを一括に管理できるのがHooksの良さです。
処理はカスタムフック内に
これで十分に見えますが、先ほどの処理では、「処理が分散されてる」という問題点があります。
handleAddTodoのnewTodoの部分は、外側で再度作るべきではありません。
これでは、useTodoを使う箇所、全てで同様な処理が起きてしまい、依存性が高くなります。
これらは、useTodo側が持っておくべき関心ごとです。
ということで、次のように変更します。
const useTodo = () => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const addTodo = () => {
const newTodo = { title, description };
// TODO todoを使った処理
}
return { title, setTitle, descrpition, setDescription, addTodo };
}
const TodoApp = () => {
const { title, setTitle, description, setDescrition, addTodo } = useTodo();
const handleAddTodo = (e) => {
...validation処理
addTodo();
}
return (
<div>
<input value={title} onChange={(e)=>setTitle(e.target.value)} />
<input value={description} onChange={(e)=>setDescription(e.target.value)} />
</div>
);
}
useTodoの内にaddTodoという処理を持ってきました。
こうすることで、handleAdd内の処理が分散せず、
useTodoのaddTodoを利用するだけで良くなりました。
変数の多さを直す
次に気になるのはuseTodoがやり取りしている変数の多さです。
こういう時、私は「無限に増やしたらどうなるか」を想定しています。
例えば、TODOの変数を無限に増やしたらどうなるか。
useTodoの戻り値と、それを受ける変数が無限に増えるでしょう。
結果、TodoApp内の可読性が悪くなります。
TodoApp内を汚す可能性は少しでも無くしたいです。
それを解決するために、やり取りする変数をオブジェクトにまとめます。
const useTodo = () => {
const [todo, setTodo] = useState({ title: "", description: ""});
const addTodo = () => {
// TODO todoを使った処理
}
const setTitle = (title) => {
setTodo((prev) => ({...prev, title}));
}
const setDescription = (description) => {
setTodo((prev) => ({...prev, description }));
}
return { todo, setTitle, setDescription, addTodo };
}
const TodoApp = () => {
const { todo, setTitle, setDescrition, addTodo } = useTodo();
const handleAddTodo = (e) => {
...validation処理
addTodo();
}
return (
<div>
<input value={todo.title} onChange={(e)=> setTitle(e.target.value)} />
<input value={todo.description} onChange={(e)=> setDescription(e.target.value)} />
</div>
);
}
setDescriptionなどの関数が増えましたが、
結果的にuseTodoを利用するがわのコードを汚さずに済みます。
基本指針を元にすると、こちらのほうが良いです。
validationをまとめる
現状でも、再利用性が高く、十分に見えますが、
今後を見据えると、おそらくvalidation処理が重複するように見えます。
つまりはuseTodoを使う部分全てに、validationを書かなければいけません。
ということで、それらもuseTodoに移行しましょう。
const useTodo = () => {
const [todo, setTodo] = useState({ title: "", description: ""});
const [errors, setErrors] = useState({});
const validate = () => {
let newErrors = {};
if (!todo.title) newErrors.title = 'タイトルは必須です';
if (todo.description > 10) newErrors.description = '説明は最低10文字入力してください';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
const addTodo = () => {
// TODO todoを使った処理
}
const setTitle = (title) => {
setTodo((prev) => ({...prev, title}));
}
const setDescription = (description) => {
setTodo((prev) => ({...prev, description }));
}
return { todo, setTitle, setDescription, addTodo, validate, errors };
}
const TodoApp = () => {
const { todo, setTitle, setDescrition, addTodo, validate, errors } = useTodo();
const handleAddTodo = (e) => {
if (!validation) return;
addTodo();
}
return (
<div>
// errorsを使ってエラー処理を行う
<input value={todo.title} onChange={(e)=> setTitle(e.target.value)} />
<input value={todo.description} onChange={(e)=> setDescription(e.target.value)} />
</div>
);
}
これで全てのTodoの関心ごとがuseTodoに含まれました!
AI関連の処理を入れたい!
最後に、処理を追加するパターンを見ます。
例えば、Todoに関するAI処理をいら鯛とします。
すると、useTodo内に処理を入れるだけなので、非常に簡単です。
const useTodo = () => {
const [todo, setTodo] = useState({ title: "", description: ""});
const [errors, setErrors] = useState({});
const validate = () => {
let newErrors = {};
if (!todo.title) newErrors.title = 'タイトルは必須です';
if (todo.description > 10) newErrors.description = '説明は最低10文字入力してください';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
const addTodo = () => {
// TODO todoを使った処理
}
const setTitle = (title) => {
setTodo((prev) => ({...prev, title}));
}
const setDescription = (description) => {
setTodo((prev) => ({...prev, description }));
}
// 追加処理
const analyzeTodo = () => { ... }
return { todo, setTitle, setDescription, addTodo, validate, errors, analyzeTodo };
}
あとは、利用したい箇所でanalyzeTodoを呼び出します。
まとめ
今回は私なりのHooksのアプローチを見ていきました。
まとめますと、次のような方針です。
- まずはページ最上位に全ての処理を書く。
- 関心ごとが同じ部分をカスタムHooksとしてまとめる
- Hooks内の操作をまとめる
- 変数を少なくする
- Hooks内のエラーをまとめる
- 追加処理は、関心領域に入れる。
その他に、メモ化などの処理を考えたほうがいいので、そちらも今後見ていきます。
以上です。