🐶

TypeScript & Context APIのdefaultValueの書き方(use***がうまく機能しない時)

2021/07/30に公開

TypeScriptを書くようになって約6ヶ月経ちました。
初投稿&走り書きとなるので今後新たな知見を得たらカイゼンしていこうと思います。

背景

ReactのContext APIを使ってProviderコンポーネントを作成し、その後useContextを各コンポーネントで使用する代わりにカスタムHookを作成しそれぞれのコンポーネントで使用する、というケースです。

エラーの内容

CounterProviderの中で作成したuseCountというHookを使おうとした時、下記のエラーが発生しました。


与えられた型にcountプロパティが存在しません!

※8/11追記: こちらのエラーが表示されるようになったのはyarn create next-app app_name --typescriptをした際にtsconfig.jsonstricttrueになるからでした。

as をつかって型を与える方法

CountProvider.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';

interface ContextInterface {
  count: number;
}
const CountContext = createContext({} as ContextInterface); 
// Type Assertionで型を与える

// Providerなど省略

export const useCount = () => useContext(CountContext);
Test.tsx
function Test() {
  const { count } = useCount();
  // エラーが発生しない

  return (
    <div>
      <h1>{count}</h1>
    </div>
  );
}

まずはType AssertionでdefaultValueに対し型を与える方法です。この場合、useContext(今回はカスタムフックであるuseCount)をエラーを生じさせずに使用することができます。

ただし、この書き方だと下記の問題が発生します

<>
       {/*useCountを使用しているコンポーネントがCounterProviderの外にある*/}
      <Test /> 
      <CountProvider>
         {/*useCountを使用しないコンポーネント*/}
        <SomeComponent />  
      </CountProvider>
</>

もしなんらかの理由でuseCountを使いたいコンポーネントがCountProviderの外にあっても何のエラーも表示されません。 上の場合<Test />コンポーネント内部でuseCountを使っていますが、CountProviderの外に位置するため正しくバリューを取得できず、且つエラーも表示されません。

undefinedをdefaultValueにする

👆の解決策としては下記のコードを書きます。

const CountContext = createContext<ContextInterface | undefined>(undefined);

このままだと同じくProperty 'count' does not exist on type 'ContextInterface | undefined'.ts(2339)という警告が出てしまうので、undefinedのチェックをカスタムフック内に書きます。

useCount.ts
export const useCount = () => {
  const context = useContext(CountContext);

  if (context === undefined) {
    throw new Error('useCount must be used within a CountProvider');
  }
  return context;
};

こちらの書き方だと警告が消え、さらにCountProviderの外でuseCont(useContext(CountContext))を使用するとエラーを表示させることができます。

サンプルコード全文

import React, { createContext, useState, useContext, ReactNode } from 'react';

interface ContextInterface {
  readonly count: number;
}

interface Props {
  children: ReactNode;
}

const CountContext = createContext<ContextInterface | undefined>(undefined);

const CountProvider = (props: Props) => {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={{ count }}>
      {props.children}
    </CountContext.Provider>
  );
};

const useCount = () => {
  const context = useContext(CountContext);

  if (context === undefined) {
    throw new Error('useCount must be used within a CountProvider');
  }
  return context;
};

const HomePage = () => {
  return (
    <section>
      <CountProvider>
        <Test />
      </CountProvider>
    </section>
  );
};

function Test() {
  const { count } = useCount();

  return (
    <div>
      <h1>{count}</h1>
    </div>
  );
}

export default HomePage;

Zennでの初投稿となります!間違いや「もっとこうした方が良いよ!」という点がありましたらぜひコメントを頂けると学びになります!

参考記事:
https://kentcdodds.com/blog/how-to-use-react-context-effectively#typescript

https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/context/#extended-example

Discussion