Open17

Typescript/React

下記を順にやっていく

https://qiita.com/uhyo/items/e4f54ef3b87afdd65546
https://qiita.com/uhyo/items/0e7821ce494024c98da5

下記で挙動チェックしていた。

https://www.typescriptlang.org

型アノテーション

引数と返り値に型をつける
返り値の型を推論できるので簡易的な関数なら省略も可

function isPositive(num:number): boolean {
    return num >= 0;
}

// 使用例
isPositive(3);

// エラー例
isPositive('123');
const numVar: number = isPositive(-5);

オブジェクト

インタフェースのメリット:
・同じinterfaceを実装しているクラスは、同じメンバーが必ず存在することが保証される。
・関数の引数がオブジェクトの際に型を定義するのに便利。
・コンパイルの支援の為に存在する為、コンパイル後のソースには影響しない。

https://qiita.com/shibukawa/items/463e6fb2e9c6d4d72cbb

省略可能な箇所は「age?」という形で書く

https://qiita.com/shibukawa/items/463e6fb2e9c6d4d72cbb
function showUserInfo(user: User) {
    // 省略
}

interface User {
  name: string
  age?: number
  private?: boolean
}

// 使用例
showUserInfo({
    name: 'John Smith',
    age: 16,
    private: false,
});

// User
showUserInfo({
    name: 'Mary Sue',
    private: false,
});

const usr: User = {
    name: 'Gombe Nanashino',
    age: 100,
};

関数

const isPositive: IsPositiveFunc = num => num >= 0;

//省略記法
//type IsPositiveFunc = (num: number) => boolean;

//完全記法
type IsPositiveFunc = {
  (num: number): boolean;
}

// 使用例
isPositive(5);

// エラー例
isPositive('foo');
const res: number = isPositive(123);

関数(配列)

function sumOfPos(arr:number[]):number {
  return arr.filter(num => num >= 0).reduce((acc, num) => acc + num, 0);
}

// 使用例
const sum: number = sumOfPos([1, 3, -2, 0]);
console.log(sum);

ジェネリクス

抽象的な型引数<T>を関数に与え、実際に利用されるまで型が確定しない関数を作成しています。

https://qiita.com/k-penguin-sato/items/9baa959e8919157afcd4
function test<T>(arg: T): T {
  return arg;
}

test<number>(1); //=> 1
test<string>("文字列"); //=> 文字列

//※ Genericsでも型推論ができるので、引数から型が明示的にわかる場合は省略が可能
test("文字列2"); //=> "文字列2"
function myFilter<T>(arr:T[], predicate: (elm: T) => boolean) {
  const result = [];
  for (const elm of arr) {
    if (predicate(elm)) {
      result.push(elm);
    }
  }
  return result;
}

// 使用例
const res = myFilter<number>([1, 2, 3, 4, 5], num => num % 2 === 0);
const res2 = myFilter<string>(['foo', 'hoge', 'bar'], str => str.length >= 4);

// エラー例
myFilter([1, 2, 3, 4, 5], str => str.length >= 4);

union

eumを使わない方がよい理由:

https://engineering.linecorp.com/ja/blog/typescript-enum-tree-shaking/
https://www.kabuku.co.jp/developers/good-bye-typescript-enum
type Speed = 'slow' | 'medium' | 'fast';

function getSpeed(speed: Speed): number {
  switch (speed) {
    case "slow":
      return 10;
    case "medium":
      return 50;
    case "fast":
      return 200;
  }
}

// 使用例
const slowSpeed = getSpeed("slow");
console.log(slowSpeed)
const mediumSpeed = getSpeed("medium");
console.log(mediumSpeed)
const fastSpeed = getSpeed("fast");
console.log(fastSpeed)

// エラー例
getSpeed("veryfast");

declare

declare を使うと JS のコードとして出力されません。これにより既存の test() のコードを上書きすることなく test() が宣言することができますので、ブラウザや既存の JS によって提供される変数や関数を宣言する事ができます。これが ambient (包囲した、取り巻く)宣言です。

https://developer.hatenastaff.com/entry/2016/06/27/140931#declare-キーワード
addEventListener("foobar", () => {});
addEventListener("event", () => {}, true);
addEventListener("event2", () => {}, {});
addEventListener("event3", () => {}, {
  capture: true,
  once: false
});

interface AddEventListenerOptionsObject {
  capture?: boolean;
  once?: boolean;
  passive?: boolean;
}
declare function addEventListener(
  type: string,
  handler: () => void,
  options?: boolean | AddEventListenerOptionsObject
): void;

返り値にプロパティを追加する

インターセクション型でオブジェクトに新しいプロパティを増やしたい場合の典型的な方法
ですが、返り値の型アノテーションを書かなくても勝手に推論してくれるのでエラーにはならない

function giveId<T>(obj:T):T & {id: string} {
  const id = "本当はランダムがいいけどここではただの文字列";
  return {
    ...obj,
    id
  };
}

type obj<T> = {
  obj: T
}

// 使用例
const obj1: {
  id: string;
  foo: number;
} = giveId({ foo: 123 });
const obj2: {
  id: string;
  num: number;
  hoge: boolean;
} = giveId({
  num: 0,
  hoge: true
});

// エラー例
const obj3: {
  id: string;
  piyo: string;
} = giveId({
  foo: "bar"
});

useState

  • useStateに初期値を設定する場合、初期値から型が明確であれば型推論によって型を推測してくれるため、無理に型をつける必要はない
  • useStateに対して明示的に型を指定する場合は、ジェネリック型<T>を用いて型を指定

https://zenn.dev/ogakuzuko/articles/react-typescript-for-beginner
import React from "react"

// ステートが持つオブジェクトの構造を型定義
type UserData = {
  id: number
  name: string
}

const StateSample: React.VFC = () => {
  // useStateのジェネリック型<T>に、上で定義した`UserData型`を指定
  const [user, setUser] = useState<UserData>({}) // NG 初期値が型の構造を満たしていないのでエラー
  const [user, setUser] = useState<UserData>({ id: 1122, name: "aiko" }) // OK

  return (
    <div>
      <h1>{user.id}</h1>
      <h1>{user.name}</h1>
    </div>
  )
}
const [numState, setNumState] = useState(0);
// setNumStateは新しい値で呼び出せる
setNumState(3);
// setNumStateは古いステートを新しいステートに変換する関数を渡すこともできる
setNumState(state => state + 10);

// 型引数を明示することも可能
const [anotherState, setAnotherState] = useState<number | null>(null);
setAnotherState(100);

// エラー例
setNumState('foobar');

type UseStateUpdateArgument<T> = T | ((oldValue: T) => T);
declare function useState<T>(
  initialValue: T
): [T, (updator: UseStateUpdateArgument<T>) => void];

React.VFC

  • Function Componentを定義するための型としてReact.FCが提供されている
  • React.FCのデメリット:children(タグの間の要素)の有無がわからない

.json()

  • JSON テキストファイルの内容を JSON.parse した結果や、fetch API のレスポンスを json() 関数にかけた結果は、型情報のない any 型のオブジェクトになる
  • TypeScript コードから、このオブジェクトのプロパティを参照しようとすると、「定義されていないプロパティを参照している」という感じの ESLint エラーになる
  • 型情報がないデータをそのまま扱おうとすると、VS Code などでプロパティ名の入力補完機能が働かない
  • any 型オブジェクトを TypeScript コードから扱うには、JSON データに一致する型情報(あるいはそのサブセット)を定義し、型アサーション (as) でその型を指定
    https://maku.blog/p/2hr3eqx/
type IGitHubUser = {
  name: string
  login: string
  location: string
}
const user = await res.json() as IGitHubUser

https://stackoverflow.com/questions/41103360/how-to-use-fetch-in-typescript
https://maku.blog/p/2hr3eqx/
function mapFromArray<T, K extends keyof T>(arr: T[], key: K):Map<T[K], T> {
  const result = new Map();
  for (const obj of arr) {
    result.set(obj[key], obj);
  }
  return result;
}

// 使用例
const data = [
  { id: 1, name: "John Smith" },
  { id: 2, name: "Mary Sue" },
  { id: 100, name: "Taro Yamada" }
];
const dataMap = mapFromArray(data, "id");
console.log(dataMap);

/*
dataMapは
Map {
  1 => { id: 1, name: 'John Smith' },
  2 => { id: 2, name: 'Mary Sue' },
  100 => { id: 100, name: 'Taro Yamada' }
}
というMapになる
*/

// エラー例
//mapFromArray(data, "age");

onChangeの引数等Typescriptでどう書くか

https://qiita.com/Dragon-taro/items/121df12f0e6b23839197

SELECTの場合

const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
    if (!(e.target instanceof HTMLSelectElement)) {
      return;
    }

    if (e.currentTarget.name=="genre")
    {
      const genre:string = e.currentTarget.value;
      setGenre(genre);
      setPageNumber(1);
    }

    if (e.currentTarget.name=="story")
    {
      const story:string = e.currentTarget.value;
      setStory(story);
      setPageNumber(1);
    }
  }
return (
    <select onChange={handleChange} name="genre" >
)

React

状態の管理、更新を適切に描画に反映するためのライブラリ

https://qiita.com/102Design/items/f829911cd76734cbc882

Hooksで守るべきこと

  • コンポーネントの中で呼び出されるHooksはいつなんどきでも必ず同じ順番で同じ回数呼び出されること
  • ifやforの中にHooksを入れて「場合によってHooksの順番や実行回数が変わる」ことを禁止

UseState

import React, { useState } from "react";

const minusCount = (count) => count - 1;
const plusCount = (count) => count + 1;

export default function App() {
  // count という state と setCount という count を更新する関数を定義。
  // 今回、useState に 10 を渡しているため count の初期値は 10 になる。
  const [count, setCount] = useState(10);

  const decrement = () => {
    // 引数に渡した関数((currentCount) => currentCount - 1)の
    // 引数(currentCount)には現在の count が渡される。
    // 関数の戻り値が新しい count になるため、
    // decrement が実行される度に count が 1 減る。
    setCount(minusCount(count));
  };

  const increment = () => {
    // 引数に渡した関数((currentCount) => currentCount + 1)の
    // 引数(currentCount)には現在の count が渡される。
    // 関数の戻り値が新しい count になるため、
    // increment が実行される度に count が 1 増える。
    setCount(plusCount(count));
  };

  return (
    <>
      <p>Count: {count}</p>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </>
  );
}
import React, { useState } from "react";

export default function App() {
  const [vote, setVote] = useState({ kinoko: 0, takenoko: 0 });

  const voteKinoko = () => {
    setVote({...vote, kinoko: vote.kinoko + 1});
  };

  const voteTakenoko = () => {
    setVote({...vote, takenoko: vote.takenoko + 1});
  };

  return (
    <>
      <p>きのこ: {vote.kinoko}</p>
      <p>たけのこ: {vote.takenoko}</p>
      <button onClick={voteKinoko}>きのこ</button>
      <button onClick={voteTakenoko}>たけのこ</button>
    </>
  );
}

UseEffect

https://ja.reactjs.org/docs/hooks-effect.html
https://sbfl.net/blog/2019/11/12/react-hooks-introduction/
  • コンポーネントのレンダー後かアンマウント後に何らかの処理を実行させることができる
  • アンマウント=コンポーネントをDOMから削除して破棄すること
  • 一般的に「DOMの操作」「ウェブAPIとの通信」などはuseEffect内で行う
  • useEffectの第二引数を指定するとその値が更新されたときだけ副作用が実行される
  • 空の配列が指定された場合はレンダー後一度だけ実行される
  • 副作用内で関数を返すとコンポーネントがアンマウント、もしくは副作用が再実行されたときに実行される(クリーンアップ関数)
  useEffect(() => {
    getBook()
    window.addEventListener("scroll", handleScroll)

    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [genre, keyword, pageNumber])

UseRef

https://www.to-r.net/media/react-tutorial-hooks-useref/
値の参照、HTML要素の参照に利用される

UseReducer

https://www.to-r.net/media/react-tutorial-hooks-usereducer/
https://ja.reactjs.org/docs/hooks-reference.html#usereducer
  • ステートの更新ロジックをコンポーネントに非依存な外部のreducer関数に渡すことができる
  • 依存する変数が少なくなるので、useCallbackやuseMemoによるパフォーマンスチューニングもしやすくなる
  • reducerが新しいstateを返したら再レンダーされる

https://zenn.dev/luvmini511/articles/6c6f69481c2d17
  • interfaceを使う理由は拡張性で、使いたくない理由も拡張性
  • typeの方が直感的に書きやすい・interfaceで出来ることはtypeでも出来ることがほとんど

Reactディレクトリ構造

設計のポイント

  • 影響範囲を分かりやすくする(消しやすくする)
    • 「コロケーションの原則」に従う為
      • コロケーションとは、関連するファイルまたは良く一緒に変更されるファイルは近くに置くという原則
  • 簡単に単体テストを行える状態になっているか
    • ロジックとビューの分離

ディレクトリ構造

  • src
    • pages:ルーティングのみ
    • components:共通コンポーネント
    • features/機能名:各機能の名称のディレクトリが並ぶ
      • components
      • hooks
      • types
      • index.ts

https://zenn.dev/meijin/articles/bulletproof-react-is-best-architecture
https://zenn.dev/yoshiko/articles/99f8047555f700
https://js-challenge.dev/posts/react-project-file-folder-structure/
https://ja.reactjs.org/docs/faq-structure.html

パスのエイリアスの設定
相対パスで書かなくてよくなるので楽

https://stackoverflow.com/questions/52842770/what-does-the-symbol-mean-in-a-react-import-statement

ts.config.json

"compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@": ["./src"],
      "@/*": ["./src/*"]
    }
}

パスのエイリアス設定をjestのテストで書こうとすると「Cannot find module」というエラーが発生する

https://www.suzu6.net/posts/327-ts-jest-modulenamemapper/

下記を追加して解決

jest.config.js

    moduleNameMapper: {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
  • jsdomがレイアウトエンジンを持たないので、要素の寸法や画面上の位置などのレイアウトに関わる情報をテストできない
  • CypressのようなE2Eテストツールであればブラウザを利用してテストするので、このようなケースでも問題なくテストできますが、実行速度とメンテナンスコストに難あり
  • Cypress Component Test Runnerだと実行速度・メンテコストともに良い
  • まだアルファリリースされたばかりなので整備されてない部分もある

https://tech.prog-8.com/entry/2021/05/14/161041

useCallback

  • パフォーマンス向上のためのフック
  • useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算する

<React.Fragment>とは

コンポーネントが子要素のリストを返すときに親要素をもたせられないのでその代替となるタグのこと
「<></>」で括ることもできるが、keyを渡すことが出来ない。<React.Fragment>であればkeyを渡せる

https://ja.reactjs.org/docs/fragments.html

https://qiita.com/seira/items/8a170cc950241a8fdb23
https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2
  • React.memo:コンポーネントが返した React 要素を記録し、再レンダーされそうになった時に本当に再レンダーが必要かどうかをチェックして、必要な場合のみ再レンダー
  • propsに変更がない場合に再描写を抑える
  • 通常のコンポーネントに対しては、わざわざReact.memoを利用する必要はない
const Todo = memo(({ todos }) => {
  console.log('Todo');
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoList todo={todo} key={index} />
      ))}
    </ul>
  );
});
ログインするとコメントできます