Typescript/React
下記を順にやっていく
下記で挙動チェックしていた。
型アノテーション
引数と返り値に型をつける
返り値の型を推論できるので簡易的な関数なら省略も可
function isPositive(num:number): boolean {
return num >= 0;
}
// 使用例
isPositive(3);
// エラー例
isPositive('123');
const numVar: number = isPositive(-5);
オブジェクト
インタフェースのメリット:
・同じinterfaceを実装しているクラスは、同じメンバーが必ず存在することが保証される。
・関数の引数がオブジェクトの際に型を定義するのに便利。
・コンパイルの支援の為に存在する為、コンパイル後のソースには影響しない。
省略可能な箇所は「age?」という形で書く
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>を関数に与え、実際に利用されるまで型が確定しない関数を作成しています。
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を使わない方がよい理由:
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 (包囲した、取り巻く)宣言です。
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>を用いて型を指定
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
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でどう書くか
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
状態の管理、更新を適切に描画に反映するためのライブラリ
Hooksで守るべきこと
- コンポーネントの中で呼び出されるHooksはいつなんどきでも必ず同じ順番で同じ回数呼び出されること
- ifやforの中にHooksを入れて「場合によってHooksの順番や実行回数が変わる」ことを禁止
UseState
- stateを管理(stateの保持と更新)するためのReactフック
- stateが変更されることで再レンダリングが発生
- 条件分岐などでフックを呼び出してしまうと、フックの順番が変化しバグの原因となるので条件分岐は使わない
- eslint-plugin-react-hooksをeslintに追加する:https://www.npmjs.com/package/eslint-plugin-react-hooks
- setStateの呼び出しが非同期のため、2回setStateでセットしても2回めの値が更新されない(関数を渡すと2回めも更新されるようになる)
- setStateに関数も渡せる(関数を渡すことによりコンポーネントの外に切り出せる)
- stateでおぶじぇくとを利用する場合、setState(state)のようにすると更新されない(object.isでの判定の為)
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
- コンポーネントのレンダー後かアンマウント後に何らかの処理を実行させることができる
- アンマウント=コンポーネントをDOMから削除して破棄すること
- 一般的に「DOMの操作」「ウェブAPIとの通信」などはuseEffect内で行う
- useEffectの第二引数を指定するとその値が更新されたときだけ副作用が実行される
- 空の配列が指定された場合はレンダー後一度だけ実行される
- 副作用内で関数を返すとコンポーネントがアンマウント、もしくは副作用が再実行されたときに実行される(クリーンアップ関数)
useEffect(() => {
getBook()
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [genre, keyword, pageNumber])
UseRef
値の参照、HTML要素の参照に利用される
UseReducer
- ステートの更新ロジックをコンポーネントに非依存な外部のreducer関数に渡すことができる
- 依存する変数が少なくなるので、useCallbackやuseMemoによるパフォーマンスチューニングもしやすくなる
- reducerが新しいstateを返したら再レンダーされる
無限スクロール実装例
【React】useEffectの第2引数って?
- interfaceを使う理由は拡張性で、使いたくない理由も拡張性
- typeの方が直感的に書きやすい・interfaceで出来ることはtypeでも出来ることがほとんど
Reactディレクトリ構造
設計のポイント
- 影響範囲を分かりやすくする(消しやすくする)
- 「コロケーションの原則」に従う為
- コロケーションとは、関連するファイルまたは良く一緒に変更されるファイルは近くに置くという原則
- 「コロケーションの原則」に従う為
- 簡単に単体テストを行える状態になっているか
- ロジックとビューの分離
ディレクトリ構造
- src
- pages:ルーティングのみ
- components:共通コンポーネント
- features/機能名:各機能の名称のディレクトリが並ぶ
- components
- hooks
- types
- index.ts
パスのエイリアスの設定
相対パスで書かなくてよくなるので楽
ts.config.json
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@": ["./src"],
"@/*": ["./src/*"]
}
}
パスのエイリアス設定をjestのテストで書こうとすると「Cannot find module」というエラーが発生する
下記を追加して解決
jest.config.js
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
},
- jsdomがレイアウトエンジンを持たないので、要素の寸法や画面上の位置などのレイアウトに関わる情報をテストできない
- CypressのようなE2Eテストツールであればブラウザを利用してテストするので、このようなケースでも問題なくテストできますが、実行速度とメンテナンスコストに難あり
- Cypress Component Test Runnerだと実行速度・メンテコストともに良い
- まだアルファリリースされたばかりなので整備されてない部分もある
useCallback
- パフォーマンス向上のためのフック
- useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算する
検索機能の実装でuseReducerのカスタムフックどう作るか悩んでいるので参考にしている
React 設計
Reduxの標準的な構成・コンポーネント設計(Atomic Design)
オープンソースのReact / Next.js製Webアプリまとめ
showがtrueであればNav-blackを出力
<div className={`Nav ${show && "Nav-black"}`}>
React実装で意識すべきこと
- 無駄に再レンダリングが走ってないか
- テストしやすい実装になっているか
- テストしやすい実装とは?
再レンダリングのチェック
React Developer Tools
<React.Fragment>とは
コンポーネントが子要素のリストを返すときに親要素をもたせられないのでその代替となるタグのこと
「<></>」で括ることもできるが、keyを渡すことが出来ない。<React.Fragment>であればkeyを渡せる
- 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>
);
});