Yuya-Furusawa / Self-Study

0 stars 0 forks source link

React + TypeScript開発で調べたことのまとめ #1

Open Yuya-Furusawa opened 3 years ago

Yuya-Furusawa commented 3 years ago

参照した記事

React

TypeScript

React + TypeScript

TypeScriptをなぜ使うのか?

Yuya-Furusawa commented 3 years ago

型定義のオプション化

// titleおよびcolorはstringもしくはundefinedとなる
type ButtonProps = {
  title?: string
  color?: string
}

?をつけるとそのプロパティは省略可能になる

全部に?を付けたい場合はPartial<>を使った方が便利

type ButtonProps = Partial<{
  title: string
  color: string
}>
Yuya-Furusawa commented 3 years ago

関数コンポーネントの型定義

const Summary: React.FC<Props, State> = (props) => {
  hogehoge;
};

React.FC<>について、第一引数にpropsの型、第二引数にstateの型を指定する 指定しない場合やstateが無い場合は、<Props, {}><Props><{}, State>という形にする

useStateの型定義

const [count, setCount] = useState<Type>(0);

setCount()Type以外の型を入れようとするとエラーになる

booleanとか使う時に便利

const [isHoshimiya, setIsHoshimiya] = useState<boolean>(true);

だが基本的にTypeScriptでは型推論してくれるのであんまり型指定しなくても大丈夫

// countの型はnumberになる
// setCount()にnumber以外を入れるとエラーになる
const [count, setCount] = useState(0);

参照

Yuya-Furusawa commented 3 years ago

Typeか?Interfaceか?

type Foo = number;
type Hoge = string;
type FooHoge = Foo | Hoge;  //独自の型

//関数のPropsの型を定義するときはinterface
interface Props {
  contents: FooHoge;
};

const Card: React.FC<Props> = (contents) => {
  hogehoge
};

参照

Yuya-Furusawa commented 3 years ago

propsの分割代入

propsを渡すときにコード量を減らせる

(props)ではなく({ hogehoge })と書けば記述量を減らせる

修正前

const App = () => {
  const greeting = 'Hello Function Component!';
  return <Headline value={greeting} />;
}

const Headline = (props) => {
  return <h1>{props.value}</h1>;
}

修正後

const App = () => {
  const greeting = 'Hello Function Component!';
  return <Headline value={greeting} />;
}

const Headline = ({ value }) => {
  return <h1>{value}</h1>;
}

おまけ:スプレッド構文も使える

こんな風に、propsが2つ以上ある時、

const Button = ({ children, onClick }) => {
  return <button onClick={onClick}>{children}</button>
}

スプレッド構文を使ってpropsの記述を減らせる

const Button = (props) => {
  return <button {...props} />
}

ただしこの場合、可読性が下がるのであまりやるべきでは無い!!

参照

Yuya-Furusawa commented 3 years ago

ReactNodeについて

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined

用途

JSX.Elementとの違い

参照

Yuya-Furusawa commented 3 years ago

useEffectについて

用例

// useEffectの場合
useEffect(() => {
  document.title =`${count}回クリックされました`
})

// クラスコンポーネントの場合
componentDidMount(){
  document.title =`${this.state.count}回クリックされました`
}
componentDidUpdate(){
  document.title =`${this.state.count}回クリックされました`
}

第2引数

第2引数に配列を渡すと、「これの値が変わった時だけコンポーネントを再レンダーする!」みたいなことができる

// useEffectを使った場合
useEffect(() => {
  elm.addEventListener('click', () => {})
  return () => {
     elm.removeEventListener('click', () => {})
  }
}, [])

// countに変化があった時だけ関数を実行
useEffect(() => {
  document.title =`${count}回クリックされました`
},[count])

第2引数に空の配列を渡すと、初回のレンダー時のみ関数を実行する(= componentDidMountと同じになる)

// 初回だけ関数を実行
useEffect(() => {
  document.title =`${count}回クリックされました`
},[])

//これと同じ
componentDidMount(){
  document.title =`${this.state.count}回クリックされました`
}

クリーンアップについて

クリーンアップ関数をuseEfffect内でreturnすると2度目以降のレンダリング時に前回の副作用を消してしまうことができる。 イベントリスナの削除やタイマーのキャンセルなどを行うことができる。

useEffect(() => {
   elm.addEventListener('click', () => {})

  // returned function will be called on component unmount 
  return () => {
     elm.removeEventListener('click', () => {})
  }
}, [])

useEffectで非同期処理をするときの注意

useEffectの第一引数として非同期関数を設定できない というのも、第一引数として設定する関数の戻り値はundefinedもしくはcleanup関数でなくてはならない、ため

//これはエラー
//戻り値がPromiseインスタンスになっている
useEffect(async () => {
    const response = await fetch("https://www.googleapis.com/books/v1/volumes?q=AWS");
    const data = await response.json();
    console.log(data);
  },[]);

正しくは以下の通り

//これはOK
useEffect(() => {
    const data = async() => {
      const response = await fetch("https://www.googleapis.com/books/v1/volumes?q=AWS");
      const data = await response.json();
      alert(data.totalItems);
    }
    data()
  }, []);

参照

Yuya-Furusawa commented 3 years ago

Boolean Props

propsにboolean型を渡すことがもちろんできる

trueを渡す時

render() {
    return (
        <Movie released={true} />
    )
}

簡略化可能

render() {
    return (
        <Movie released />  //何を指定しなくてもtrueになる
    )
}

falseを渡す時

render() {
    return (
        <Movie released={false} />
    )
}

何も渡さないとundefinedが渡される

render() {
    return (
        <Movie />  // propsを指定しなかったらundefinedを渡してるのと同じ、真偽値判定はfalseになる
    )
}

参照

Yuya-Furusawa commented 3 years ago

export/import について

exportの仕方には、"named export"と"default export"の2種類がある。

//named export
export const sqrt = Math.sqrt;

import { sqrt } from "./hoge"; // { }を使う
//default export
export default const sqrt = Math.sqrt;

import sqrt from "./hoge"; // { }はいらない

default exportは1つのファイルにつき1つしか指定できない。

さらに、named exportはimportする時に、対応するオブジェクトと同じ名前を使用しなくてはいけない。 しかし、default exportはimport側で任意の名前で呼び出すことができる(ただしこれにより後々リファクタリングがめんどくさくなる可能性も)。

// test.js
let k; export default k = 12;

// 他のファイル
import m from './test'; // k がdefault exportなのでimportする k の代わりに m を使用することができる
console.log(m); 

参照

Yuya-Furusawa commented 3 years ago

クリーンアップ関数について

const Foo = () => {
  useEffect(() => {
    // ここがコールバック関数
    console.log("Fooがマウントされました!");
    // ↓これがクリーンアップ関数
    return () => {
      console.log("Fooがアンマウントされる!");
    };
  }, []);
  return <p>I am foo</p>;
};

useEffectにおけるクリーンアップ関数の実行タイミング

再レンダリング時

  1. 新しいレンダリング
  2. 新しいレンダリングがDOMに反映
  3. 更新されたDOMが画面に反映される
  4. クリーンアップ関数の実行
  5. 新しいコールバック関数の実行

アンマウント時

  1. 新しいレンダリング
  2. 新しいレンダリングがDOMに反映
  3. 更新されたDOMが画面に反映される
  4. クリーンアップ関数の実行

参照

Yuya-Furusawa commented 3 years ago

Reactでのselectフォーム

基本的な構文(Functional Componentの場合)

const FlavorForm = () => {
  const [flavor, setFlavor] = useState('coconut');

  handleSubmit(event) {
    alert('Your favorite flavor is: ' + flavor);
    event.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Pick your favorite flavor:
        <select value={flavor} onChange={(e) => setFlavor(e.target.value)}>
          <option value="grapefruit">Grapefruit</option>
          <option value="lime">Lime</option>
          <option value="coconut">Coconut</option>
          <option value="mango">Mango</option>
        </select>
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

参照

Yuya-Furusawa commented 3 years ago

Reactのstate更新タイミング

参照

Yuya-Furusawa commented 3 years ago

React Routerについて

使い方

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
import { Home, About, Dashboard } from './component';

const App = () => {
  return (
    <BrowserRouter>
      <div>{document.title}</div>
        <Switch>
          <Route exact path='/' component={Home} />
          <Route exact path='/about' component={About} />
          <Route exact path='/dashboard' component><Dashboard /></Route>
        </Switch>
      <Link to='/'>Back To Home</Link>
    </BrowserRouter>
  )
}

react-routerとreact-router-domの違い

参照

Yuya-Furusawa commented 3 years ago

React Contextについて

Contextを使うと、propsのバケツリレーを防ぐことができる

// まずはコンテクストを作成
// 引数はdefault value
const ResourceContext = React.createContext("");

export const RootComponent = () => {
  const resource = getResource(); // 何らかのデータを取ってくる

  return (
    // Providerコンポーネントで包むことによって、Provider配下のコンテキストを決定する
    // このvalueが今までバケツリレーしていたprops
    <ResourceContext.Provider value={resource.name}>
      <NavigationComponent />
      <BodyComponent />
    </ResourceContext.Provider>
  );
};

export const NavigationComponent = (props) => {
  // useContextのhooksを使う
  // 値はResourceContext.Providerのvalueで決定される
  const resourceName = React.useContext(ResourceContext);

  return (
    <header>
      <TitleComponent title={resourceName} />
      <NannkanoButtonComponent  />
      <NannkanoMenuComponent />
    </header>
  );
};

export const TitleComponent = (props) => (
  <div>{props.title}</div>
);

export const BodyComponent = (props) => {
  // ココも!
  const resourceName = React.useContext(ResourceContext);

  return (
    <main>
      <ResourceTitleComponent name={resourceName} />
      ...
    </main>
  );
};

export const ResourceTitleComponent = (props) => (
  <article>{props.name}</article>
);

使い方としては、

  1. createContextでコンテクストを作成する
  2. MyContext.Providerでpropsを渡したいコンポーネントを包む
  3. useContextでpropsを取り出して使う みたいな感じ

参照

Yuya-Furusawa commented 3 years ago

useReducerについて

import React, { useReducer } from 'react';

function Counter() {
  // useReducerの第1引数はstateとactionを引数に持つ関数、第2引数はstateの初期値
  // sumはstate
  const [sum, dispatch] = useReducer((state, action) => {
    return state + action;
  }, 0);

  return (
    <>
      {sum}
      // dispatchはuseReducerに渡した関数のこと
      // dispatchにわたす引数はその関数でいうaction
      <button onClick={() => dispatch(1)}>
        Add 1
      </button>
    </>
  );
}

typeとかを使ってReduxっぽく応用することが多いっぽい?

import React, {useReducer} from 'react ';

const [state, dispatch] = useReducer(authReducer, initialState);

function authReducer(state, action){
  // typeによって処理を変える
  switch (action.type) {
    case 'LOGIN':
      return {
        ...state,
        user: action.payload
      };
    case 'LOGOUT':
      return {
        ...state,
        user: null
      };
    default:
      return state;
  }
}

function login(userData){
  // typeは'LOGIN'としてdispatch(=authReducer)関数を呼び出す
  dispatch({
    type: 'LOGIN',
    payload: userData
  });
}

function logout(){
  // typeは'LOGIN'としてdispatch(=authReducer)関数を呼び出す
  dispatch({ type: 'LOGOUT' });
}

参照

Yuya-Furusawa commented 3 years ago

React Routerを用いてURLパラメターを取得する

まずは動的なルートを定義する

import { BrowserRouter as Router, Route } from 'react-router-dom';

<Router>
  <Route path='/posts/:postId' component={SinglePost} />
</Router>

このとき、以下のように取得できる

const SinglePost = (props) => {
  const postId = props.match.params.postId;
}

ちなみに型定義する場合は、

import { RouteComponentProps } from 'react-router-dom';

type PostProps = RouteComponentProps<{
 // パラメータの型を指定する
 postId: string;
}>;

const SinglePost: FC<PostProps> = (props) => {
  const postId = props.match.params.postId;
}

参照

Yuya-Furusawa commented 3 years ago

useRefについて

そもそもRefはあまり使うべきでは無いらしい、以下のときに使う(公式より)

useRefを用いると要素への参照を行うことができる

//100は初期値
const number = useRef(100);
//current属性で値を取得
console.log(number.current); // 100

DOMの参照で使われることが多い模様

const inputElement = useRef(null)

//例: inputElement.currentで <input type="text" /> を参照
<input ref={inputElement} type="text" />
console.log(inputElement.current); // <input type="text" />

useRef(というかRef)を用いるとコンポーネントの再描画はせずに、内部の値だけ更新する、みたいなことができる

参照

Yuya-Furusawa commented 3 years ago

React Portalについて

public/index.html

<body>
  (...省略)
  <div id="modal"></div>
  <div id="root"></div>
</body>

src/ModalPortal.js

import ReactDOM from 'react-dom';

const ModalPortal = ({ children }) => {
  const el = document.getElementById('modal');
  // id="modal"に対してPortalを作成する
  return ReactDOM.createPortal(children, el);
};

export default ModalPortal;

src/App.js

import "./App.css";
import Modal from "./Modal";
import ModalPortal from "./ModalPortal";

export default function App() {
  return (
    <div className="App">
      <h1>Hello World</h1>
      // Portalで囲った部分(Modal)が別のDOMにレンダリングされる
      <ModalPortal>
        <Modal />
      </ModalPortal>
    </div>
  );
}

参照

Yuya-Furusawa commented 3 years ago

propsの型定義

propsの分割代入を行うときにどうやって型定義を行うか

// 型を定義
type AppProps = {
  message: string;
};

// 分割代入をしない場合
const App = (props: AppProps) => <div>{message}</div>;

// 定義した型を使って型定義
const App = ({ message }: AppProps) => <div>{message}</div>;

// 返り値の型を指定する場合
const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;

// 定義した型を使わない場合、ただし可読性は下がる
const App = ({ message }: { message: string }) => <div>{message}</div>;

参照

Yuya-Furusawa commented 3 years ago

イベントハンドラ

イベントハンドラの渡し方に注意

function showMessage(){
  console.log('message);
}

// ボタンクリック時に関数が実行される
<button onClick={showMessage}>ボタン</button>
// 無名関数を使ってもOK
<button onClick={() => showMessage()}>ボタン</button>

// 引数がある場合
// 関数の方に引数を入れる
<button onClick={() => showMessage(mes)}>ボタン</button>

以下のパターンはNG JSXが評価されるタイミングで実行されてします

<button onClick={showMessage()}>ボタン</button>

// 引数あるときも注意
<button onClick={showMessage(mes)}>ボタン</button>

参照

Yuya-Furusawa commented 3 years ago

string型とString型について

TypeScriptを使うときにstring型とString型を混在して使っていたので整理

string

const str1 = "string";
console.log(typeof str1); // string

String

const str2 = String("string");
console.log(typeof str2); // String

どっちを使えば良いか?

参照

Yuya-Furusawa commented 3 years ago

Reactはいつレンダリングを行うか?

基本的には

  1. stateが更新されたとき
  2. propsが更新されたとき
  3. 親コンポーネントが更新されたとき(propsが変化してなくても)

にレンダリングが行われる。 しかし、3番目により、意識しないと無駄なレンダリングにつながってしまう。 その結果、UIの描写に時間がかかったりしてしまう。

レンダリングをスキップするためにはmemouseCallbackuseMemoを使ってメモ化を行う。 (だが、実際にはmemoなどを使うよりはdivタグを減らすなどのほうがパフォーマンスは上がるみたい) (メモ化:同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすること)

memo

React.memoはコンポーネントをラップする。propsが同じだったら親コンポーネントが更新されても再描画せずに、以前の結果を描画する。 stateが更新されたときは新しくレンダーする。

useCallback

コンポーネントを再描写するとそのコンポーネントの中で定義されている関数も新たに作り直される。そしてその関数をpropsとして子コンポーネントに渡すと、関数自体は変わって無くてもpropsが変更されたことになるので、子コンポーネントが再描画されてしまい、不必要な再描画を招く。 useCallbackは関数自体をメモ化する。依存する引数が変化しなければ、以前と同じ関数を返す。

useMemo

useCallbackは関数自体をメモ化するが、useMemoは関数の結果をメモ化する。 「計算負荷は高いが、何回やっても結果は変わらない」みたいなときに使う。 (関係のない部分が変更されてコンポーネントが再描写されるたびにコストの高い計算が行われることを防ぐ)

参照

Yuya-Furusawa commented 3 years ago

TypeScriptで型定義ファイルが無い時の対処法

例えば、あるモジュールをimportしたときに

import hoge from 'fugafuga';

こんなエラーが出る時がある

Could not find a declaration file for module 'fugafuga'.
Try npm install @types/fugafuga if it exists or add a new declaration (.d.ts) file containing declare module 'fugafuga';

これはモジュールの型定義ファイルが無くTypeScriptに対応できていないことによる メジャーなパッケージなら@types/fugafugaという型定義ファイルがあるのでそれをインストールすればOK @typesがなければ自分で型定義ファイルを自作する必要がある

型定義ファイルの作成

.d.tsファイルを作成する(.tsファイルでもOK) ファイルの拡張子が.d.tsの場合、各ルートレベルの定義にはdeclareというキーワードを前に付ける必要がある

// fugafuga.d.ts
declare module 'fugafuga';

こうすればエラーは解消される が、moduleをimportすると型は暗黙的にanyになる

ちゃんと型付けを行う時はこんな感じ

// fugafuga.d.ts
declare module 'fugafuga' {
    export function getRandomNumber(): number
} 
import { getRandomNumber } from 'fugafuga';
const x = getRandomNumber(); // x is inferred as number

自分が使うオブジェクトに対してだけ型付けを行えば十分

型定義ファイルの配置・名前について

declare moduleをした(これをアンビエントモジュール宣言という)モジュールはどこに配置しても大丈夫 アンビエントモジュール宣言はプロジェクト全体に適用される しかしあくまでもTSのコンパイルが通る必要があるため、tsconfig.jsonで設定したコンパイルの箇所には配置する必要あり

型定義ファイルの名前は何でもよい(中身が重要)が、実務的には宣言するモジュールごとにファイルを分け、src/@types/以下に配置することが多い 面倒な場合はglobal.d.tsみたいにすることもある

※注意1 トップレベルにimportしてしまうとアンビエントモジュール宣言にならずグローバルにならない 他のファイルからexportしたものを宣言に使うときはdeclareブロックの中でimportする必要あり 参照:Import class in definition file (*d.ts) - stackoverflow

※注意2 独自に.d.tsファイルや.tsファイルを作りアンビエントモジュール宣言を行わなくても型定義をすることはできる しかしdeclare moduleが一番優先されるので、基本的にはこれを使うのが良さそう アンビエントモジュール宣言を用いない場合は、ファイルの配置箇所・ファイル名には気をつける必要あり 参照:TypeScript の型定義ファイルの探索アルゴリズム

参照

Yuya-Furusawa commented 3 years ago

TypeScriptのアクセス制御

TypeScriptではclass内のオブジェクト(メンバーや関数など)にprivateprotectedpublicの修飾子を付けてアクセス制御ができる

class SmallDog {
  private secretPlace: string; //アクセス制御

  dig(): string {
    return this.secretPlace;
  }

  bury(treasure: string) {
    this.secretPlace = treasure;
  }
}

const miniatureDachshund = new SmallDog();
miniatureDachshund.bury("骨");

// インスタンスからはアクセスできない
// error TS2341: Property 'secretPlace' is private and only accessible within class 'SmallDog'.
miniatureDachshund.secretPlace;

console.log(miniatureDachshund.dig()); // 骨

ちなみにコンストラクタの引数としてプロパティを宣言できる

class SmallDog {
  constructor(private secretPlace: string) {
  }
  ...
}

参照

Yuya-Furusawa commented 3 years ago

クラスの型

TypeScriptでは、クラスを定義すると同時に同名の型も定義される。

class Foo {
  method(): void {
    console.log('Hello, world!');
  }
}

const obj: Foo = new Foo();

クラスFooが定義されたことで型Fooも同時に定義される 最後のobj: FooFooは型のFoonew Foo()FooはクラスFooの実体)

ちなみにここの型Fooは以下と同じ

interface MyFoo {
  method: ()=> void;
}

const obj: MyFoo = new Foo(); //エラーは出ない

参照

Yuya-Furusawa commented 2 years ago

TypeScriptとJSON

JSONをparseした結果はany型として扱われてしまう。 そのため予期しない型のデータが取得された場合、予期しないところでエラーが発生する。 型推論をさせてしまうのもダメ。 回避するためにはデータの取得段階でvalidationを掛ける必要がある。

validationに使えるのは@mojotech/json-type-validationというライブラリ

参照

Yuya-Furusawa commented 2 years ago

Reactのマウント・レンダリング

この図が分かりやすい

スクリーンショット 2021-12-11 15 02 34

なので、useStateの初期値はマウント時にしか読み込まれない(constructorのstateなので) コンポーネントがレンダリングされるたびにstateが初期値に戻ったりしない 初期値を頻繁に使いたいならpropsとして扱うのがベター

コンポーネント内の関数はレンダリングの度に実行されるので注意 副作用のあるような関数はレンダリング時ではなくマウント時に実行させるのが良い マウント時のみ実行させたいならuseEffectを使う

Yuya-Furusawa commented 2 years ago

TypeScriptで関数の型付け

ちゃんと型付けする場合はこんな感じ

function lengthOrDefault(str: string | null, defaultLength: number): number {
  return str != null ? str.length : defaultLength;
}

戻り値の型推論

戻り値の型付けを省略した場合はreturnされている式から型推論される

// foo は (num: number) => "hoge" | "fuga" 型
function foo(num: number) {
  if (num >= 0) {
    return "hoge";
  } else {
    return "fuga";
  }
}

return文が無いときはvoid型が推論される。 return文はあるけどreturnせずに終了する場合があるときはundefinedになる。

// fooの返り値は "hoge" | undefined 型
function foo(num: number) {
    if (num >= 0) {
        return "hoge";
    }
}

undefinedにするのではなくエラーにしたい場合はtsconfig.jsの設定を変更すればOK

引数の型推論

「変数の型は宣言時に決まる」ため、関数が使われる際に推論は行われない。 型注釈がない場合、any型として推論される。

// 引数はany型として扱われてしまう。
// 設定によってはimplicit anyということでエラー
function foo(num) {
  if (num >= 0) {
    return "hoge";
  }
}

なので、基本的には引数は型注釈を与える必要がある。 ただし、contextual typeがあるときは引数に型注釈を与える必要はない。

type Func = (arg: number) => number;

const double: Func = function(num) {
  // 引数 num の型は number 型と推論されている
  return num * 2;
};

参照

Yuya-Furusawa commented 2 years ago

hydrate

ReactでSSRをする際に使う。すでに存在するmarkupにevent listenerなどをアタッチする。 (hydrate = 水を与える、event listenerの無い干からびたHTMLにevent listenerという水を与えるイメージ)

レンダーされる内容がサーバーとクライアントで一致していなくてはならない。 なのでReactDOMServerなどを用いてマークアップを用意する必要がある

参照