LoginSignup
0
2
お題は不問!Qiita Engineer Festa 2023で記事投稿!

ReactHooks(カスタムフック)の基本系を少しずつ発展させていく

Last updated at Posted at 2023-07-13

この記事について

この記事では、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のアプローチを見ていきました。

まとめますと、次のような方針です。

  1. まずはページ最上位に全ての処理を書く。
  2. 関心ごとが同じ部分をカスタムHooksとしてまとめる
  3. Hooks内の操作をまとめる
  4. 変数を少なくする
  5. Hooks内のエラーをまとめる
  6. 追加処理は、関心領域に入れる。

その他に、メモ化などの処理を考えたほうがいいので、そちらも今後見ていきます。

以上です。

0
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2