React hooks + typescript useReucerの使い方
はじめに
今回は、React
では欠かせない便利機能の一つhooks
についての紹介です。
React
のjs
の記事は、色々情報はありますがtypescript
の記事がなかなかネットで調べても
検索に出てこなくて、非常に困りナレッジとして残す意味も含めて、今回記事にしています。
今回は、以下の記事を参考にさせて頂きました。以下の記事を書いた方は、会社の先輩でいつもお世話になっています。今回もお世話になります。
参考文献
では、早速本題に移りましょう!
前提知識
ここでは、前提知識について記しています。
-
typescript
の基本的が分かる。(型の書き方や関数の書き方さえ、わかれば十分です。) -
hooks
が少しわかる。(useState
がわかれば大丈夫です。)
useReducer って何?
ここでは、useReducer
がどういったものか説明します。
useReducer
は、useState
の機能に似ていて、同じく状態を管理することができるhooks
です。これらは状態の管理をするという意味では同じものですが、大きな違いがあります。
それは、管理の仕方です。
useState
で場合、例として以下のように宣言します。
const [state, setState] = useState("");
状態変数であるstate
の値を書き換える際は、更新用関数setState
で、state
の値を更新します。それに対して、useReducer
の場合、以下のように宣言します。
const [state, dispatch] = useReducer(reducer, initial);
形が少しだけ似てますが、なんか違いますね。
ここで、もう抵抗が出る人もいるかもしれません。(自分がそうでした。)
state
の値を書き換えるには、自身で状態を更新するための関数(上記の場合、reducer
の中身に処理を記述)を作成し、dispatch
で更新用の関数を呼び出すことでstate
の更新を行います。多分、「何言ってるんだろう??この人は。」っていう状態だと思います。
実際に使ってみると、なんとなく意味が理解できるとは思います。
実際に、ハンズオン形式で一緒にやってみましょう。
今回は、ユーザーが任意の値で state の数字を、ボタンを押して足す、引くができるものを作成します。完成品はこんな感じです。
コードをコピペすれば、物ができるようにはしますので、一緒に頑張りましょう!
環境構築
好きな場所で、まずはプロジェクトを作成しましょう。以下のコマンドを入力してください。入力後、ファイル名を何にするか聞かれます。ファイル名はご自由にしてください。
npx create-next-app --ts
その他の環境
- VScode : 1.72.2
- next : 13.0.2
- react: 18.2.0
- typescript : 4.8.4
近ければ、特に問題はないと思います。
フォルダ構成
フォルダの構成は、このようにしています。
React
を勉強して 2 か月で、フォルダ構成の仕方は、勉強中です。あくまで参考程度にしといてください。
src
├─components
│ main.tsx
├─hooks
│ reducer.ts
├─pages
| index.tsx
└─types
action.ts
state.ts
コードの記述
まずは、雛形の準備です。
環境ができたら、以下の二つをコピペしてください。
export const Main = () => {
return (
<div
style={{
margin: "30px",
padding: "20px",
border: "solid",
width: "600px",
}}
>
<h1>カウント:</h1>
<div>
変更値:
<input
type="number"
></input>
</div>
<div
style={{
display: "flex",
}}
>
<button>+</button>
<button>-</button>
</div>
</div>
);
};
export default Main;
import type { NextPage } from "next";
import Main from "../components/main";
const Home: NextPage = () => {
return (
<>
<Main />
</>
);
};
export default Home;
コピーが終わったら、以下のコマンドで一度サーバーを立ち上げてみましょう。
npm run dev
立ち上げたら、おそらく以下の画面が表示されると思います。
これで雛形が完成しました。
ここから、変更値にユーザーが数字を入力して、「+」を押せばカウントアップして、「-」を押すとカウントダウンする処理を作成します。
処理を作成する前に、どのような状態変数が必要か考えてみましょう。
ここでは、「結果の値を管理するもの」と「変更値を管理するもの」があれば良さそうですね。ということで、上記の 2 つを管理するための状態変数の型を作成しましょう。
export type TState = {
resultValue: number;
changeValue: number;
};
これで、管理したい状態変数の型を作成しました。
では、次に更新を行うための関数を作成します。まずは、どのようなアクションを起こしたいか考えてみてください。「カウントアップ」、「カウントダウン」、「任意の値への変更」、これら 3 つがあればできそうですね。ということで、更新関数を作成するための準備をします。いきなり関数を作成せず、型を定義します。ここでは、「関数名」と「関数に渡す引数」を宣言してあげるという感じです。
export type Action =
| {
type: "CountUp"; //関数名
}
| {
type: "CountDown"; //関数名
}
| {
type: "ChangeValue"; //関数名
payload: number; //引数
};
型定義ができたら、reducer
を宣言して、その中に関数を作成します。reducer
には、引数として先ほど作成した TState と Action の型で定義した変数を渡します。
import { Action } from "../types/action";
import { TState } from "../types/state";
export const reducer = (state:TState, action:Action):TState=> {
}
これで、やっと処理の中身を書けます。ただし、最後の戻り値として返すTState
で今エラーが出てると思います.
これはまだState
型の戻り値(状態変数)を返してないためエラーが出ています。今は、気にしなくて大丈夫です。では、switch 構文で先ほど宣言した 3 つの関数に対して処理内容を記述していきます。処理を記述すると以下のようになります。ここは、必ず記述していただきたいです!
(型を宣言したことによる、ちょっとした恩恵が得られます。)
import { Action } from "../types/action";
import { TState } from "../types/state";
export const reducer = (state: TState, action: Action): TState => {
switch (action.type) {
case "CountUp":
return {
resultValue: state.resultValue + state.changeValue,
changeValue: state.changeValue,
};
case "CountDown":
return {
resultValue: state.resultValue - state.changeValue,
changeValue: state.changeValue,
};
case "ChangeValue":
return { resultValue: state.resultValue, changeValue: action.payload };
}
};
これで、エラーが出ていた部分が解消されたと思います。先ほどは、TState
型を戻り値として返していない為、エラーが出ていましたが、今はreturn
値に TState の型に相当するものを返している為、エラーがでてません。もしかすると、TState
型を返すのに、辞書型で変数を返していて、よくわからない人がいるかもしれません。そんなときは、戻り値で宣言したTState
にカーソールを合わせてみてください。以下のように、表示されると思います。
よく見ると、return
で返しているものと、TState
の中身が一緒ですね!
「返し方が、わからなくなった!」って人は、戻り値をホバーすると一発で分かります。
ちなみに、スプレッド構文で書く方法もありますが、今回は省略します。気になる方は、調べてみましょう!
ここまで作ったものを、useReducer
にまとめていきます。やっと、出てきたかって感じですね。
src/components/main.tsx
に移ります。全体を記述すると以下のようになります。
import { useReducer } from "react";
import { reducer } from "../hooks/reducer";
import { TState } from "../types/state";
//状態の初期値
const initial: TState = {
resultValue: 0,
changeValue: 0,
};
export const Main = () => {
const [rstate, dispatch] = useReducer(reducer, initial);
const CountUp = () => {
dispatch({ type: "CountUp" });
};
const CountDown = () => {
dispatch({ type: "CountDown" });
};
return (
<div
style={{
margin: "30px",
padding: "20px",
border: "solid",
width: "600px",
}}
>
<h1>カウント:{rstate.resultValue}</h1>
<div>
変更値:
<input
type="number"
value={rstate.changeValue}
onChange={(e) => {
dispatch({ type: "ChangeValue", payload: +e.target.value });
}}
></input>
</div>
<div
style={{
display: "flex",
}}
>
<button
onClick={(e) => {
e.preventDefault();
CountUp();
}}
>
+
</button>
<button
onClick={(e) => {
e.preventDefault();
CountDown();
}}
>
-
</button>
</div>
</div>
);
};
export default Main;
コード上で、宣言した useReducer に着目してください。下に書いてるやつですね。
const [rstate, dispatch] = useReducer(reducer, initial);
useReduecer
の第一引数は更新用の処理を渡し、第二引数は状態変数の初期値を渡しています。
rstate
は現在の状態です。最初の状態は、initial
で設定した値になります。
dispatch
は、更新用関数で、reducer
内の関数を読み取ることで、rstate
の値を更新します。
上記のコードのように、ボタンやインプットエリアに更新関数であるdispatch
を呼び出せば、今回作成したいものができます。dispatch
の引数は最初に宣言したAction
の方に沿って書きます。なので、カウントアップだと
dispatch({ type: "CountUp" });
カウントダウンだと
dispatch({ type: "CountDown" });
値の変更だと
dispatch({ type: "ChangeValue", payload: +e.target.value });
このように、なります。呼出し方がわからなくなったら型を確認してみましょう!
最後に
いかがでしょうか。今回が初めての投稿で、至らない点が多々あると思います。まだまだ、始めたばかりで分からない点があります。何か質問やこうした方がもっと良いという意見があれば、よろしくお願いします。では、また次回の記事で!
Discussion