devlog

主に web 開発とかプログラミングについて書きます

【翻訳】TypeScriptとReactHooksをつかった型安全なstateモデリング

社内の英語勉強会の宿題として、毎週英語記事を1つ翻訳することになった。 このブログを投稿してから、数日後に社内でレビューしてもらう予定。

翻訳、がんばりまっちゅん。(インスタの画像の松村沙友理さん)

f:id:nkgr:20191007193119p:plain

今週の元ネタ

Twitterで流れてきたやーつ

thoughtbot.com

ここから翻訳記事

React Hooks の力によって、class componentやglobal state(Reduxのような)を使うことなく、React Componentの中で再利用可能で構成可能な stateを管理することができるようになった。 TypeScriptのinterfaceの柔軟性と組み合わせることで、コンポーネントの状態やコンテキストを型安全に利用する方法はいくつかあります。

この記事では、React Hooks で typescript strict モード を使ういくつかの利点を示します。そして、あなたのTypeScript + React プロジェクトで使うことを検討することをお勧めします。Any型を回避し常に型を返却するようにするような一見些細なことで、型システムが、実行時エラーになる前にミスをキャッチできます。ReactのState管理のデバッグ時これは特に有効です。Reactではどこで実行時エラーが発生したかを追跡するのが特に難しい場合があります。

私たちの目標は TypeScriptの厳密な型利用を維持しながら React Hooks を使ってState管理をする方法を探求することです。(特にuseStateとuseContext) hooks や stateのデータを型安全に提供することに加え、型はアプリケーションのstateがどのように管理されているか明確にする生きたドキュメントとして機能します。

では、React Hooksを使ってユーザープロフィールstate管理をするためのシンプルな例から始めましょう。

const ProfilePage = (): ReactElement => {
  const [firstName] = useState("Foo");
  const [lastName] = useState("Bar");
  const [title] = useState("Software developer");

  return (
    <dl>
      <dt>First name:</dt> <dd>{firstName}</dd>
      <dt>Last name:</dt> <dd>{lastName}</dd>
      <dt>Title:</dt> <dd>{title}</dd>
    </dl>
  );
};

上記の例では、3つの useState が使われており、オブジェクトを使用して値を保持する単一のhook に簡略化できます。これを行う際に、オブジェクトのインターフェースも定義し、state の型モデルを適応できるようにします。

interface Profile {
  firstName: string;
  lastName: string;
  title: string;
}

const ProfilePage = (): ReactElement => {
  const [profile] = useState<Profile>({
    firstName: "Foo",
    lastName: "Bar",
    title: "Software developer",
  });

  return ( . . . );
};

Profile のためのインターフェースを作成することの利点の一つは、stateがどのように見えるかを参照できるようになったことです。useState<Profile>に渡すオブジェクトはどこからでも取得でき、Profile``` だとわかることができます。

またこのインターフェースを使うを使うことで、デフォルトの値の上書きを許容する型安全なカスタム hook を作ることができます。

interface ProfileState {
  profile: Profile;
  setProfile: React.Dispatch<React.SetStateAction<Profile>>;
}

export const useProfile = (overrides?: Partial<Profile>): ProfileState => {
  const defaultProfile: Profile = {
    firstName: "Foo",
    lastName: "Bar",
    title: "Software developer",
  };

  const [profile, setProfile] = useState<Profile>({
    ...defaultProfile,
    ...overrides,
  });

  return { profile, setProfile };
};

const ProfilePage = (): ReactElement => {
  const { profile } = useProfile({
    title: "Designer",
  });

  return (. . .);
};

この例では、 useProfile の引数に Partial<Profile> を受け入れる カスタム react hookを定義しています。もし TypeScript Partial というユーティリティ型 に詳しくない場合、既存のタイプの全ての利用可能なサブセットを表現する新しい型を定義します。 だから、Partial<Profile>は、Profileインターフェース(firstName, lastName, title)の全てのキーの組み合わせを渡すことができます。

userProfile の定義では、staticなデフォルトののセットを定義します。これは、overrides 引数を私ことで簡単い上書きできます。全てのStateをProfile またはPertial<Profile>として構成しているため、useProfile``hook の実装と呼び出し元の両方が正しいkeyとvalue を渡していると自信を持って言えます。

また ProfileState インターフェースを定義し、明確に ``userProfile```hook の戻り値の型を宣言している。これには、別のhookの中で構成したり、コンテキストの中で利用する場合に、戻り値を参照する具体的な方法があるという利点がある。

もし、複数のコンポーネントをまたがって、state のバケットを共有したい場合、おろらくもっとも明確な方法はuseContext をつかって hook を再実装することだろう。

const defaultProfile: Profile = {
  firstName: "Foo",
  lastName: "Bar",
  title: "Software developer",
};

const defaultProfileState: ProfileState = {
  profile: defaultProfile,
  setProfile: (): void => {},
};

export const ProfileContext = createContext<ProfileState>(defaultProfileState);

export const useProfileContext = (): ProfileState => {
  return useContext(ProfileContext);
};

const ProfilePage = (): ReactElement => {
  const { profile } = useProfileContext();

  return (. . .);
};

上記の例では hook の名前を useProfile から useProfileContext に変更し、React useContext hook を使って実装している。デフォルトのprofile state を定数に移動した。それ以外の点では、コンポーネントの実装はほとんど同じままです。以前はuserProfile hook の中にあったロジックは context provider に移動しました。

interface ProfileContextProviderProps {
  defaults?: Partial<Profile>;
  children?: ReactNode;
}

export const ProfileContextProvider = (
  props: ProfileContextProviderProps,
): ReactElement => {
  const [profile, setProfile] = useState<Profile>({
    ...defaultProfile,
    ...props.defaults,
  });

  return (
    <ProfileContext.Provider
      value={{
        profile,
        setProfile,
      }}>
      {props.children}
    </ProfileContext.Provider>
  );
};

最終的に、context profider を定義しそれをアプリの中で利用できます。

const App = (): ReactElement => {
  const defaults: ProfileContextProviderProps = {
    title: "Designer",
  };

  return (
    <ProfileContextProvider defaults={defaults}>
      <ProfilePage />
    </ProfileContextProvider>
  );
};

型の冗長性を犠牲にして、Reactコンポーネントの状態の強く型付けされたモデルを作成しました。stateにする値は、存在して欲しい型であり、カスタムフックのインターフェイスを変更する自己文書化された簡単な方法があると確信できます。

所感

  • 所要時間 5 ポモドーロ
  • Google翻訳にだいぶ頼った。
  • 書いてある日本語がすでに不明
  • 読むときはだいぶ端折って意味を理解している風なのがよくわかる。ちゃんと読んで翻訳しようとするとめっちゃ大変。
  • もっと効率よく翻訳できるように英語力つけよ。