445
590

【完全版】これ1本でReactの基本がマスターできる!初心者チュートリアル!

Last updated at Posted at 2024-08-18

Qiitaサムネイル (2).png

はじめに

Reactの人気はどんどん加速しています。これからフレームワークを学ぶならReactを選ぶのは賢い選択でしょう

こんにちは、Watanabe Jin(@Sicut_study)です。
今回はReactをこれから学んでいこうと思っている方に向けて、初心者チュートリアルを作成しました。
このチュートリアルではReactを書く上で欠かせないことを紹介しています。

実際にアプリを作りながらReactの機能について学ぶことによってより深く理解することが可能です。
このチュートリアルを最後までやったら、次に自分で習ったことを活かして簡単なTODOアプリなどを作れば基本がしっかり身につくようになっています。

またこのチュートリアルではJavaScriptではなくTypeScriptを採用しています。
モダンな会社ではTypeScriptが採用されることが多いはずだなので、少しでも慣れていただけるように解説していきます。

TypeScript自体についての理解が浅いという方は最初にこちらのチュートリアルをやっていただくとよりReactの本質的な部分に集中できます。

この記事の対象

この記事を最後まで実施することで簡単なReactのアプリケーションを作ることができるようになります。

  • JavaScriptを経験したことがある人
  • Reactをやってみたいという人
  • 短い時間でしっかり学びたい人
  • 手を動かして勉強するのが好きな人
  • TypeScriptを使ってみたい人

動画で学びたい方

こちらのハンズオンは動画をご用意していますので、ご活用ください!

Reactとは?

Reactとは、Meta社(Facebook社)が開発したJavaScriptのフレームワークです。
ユーザーインターフェース(UI)を簡単に構築することができるようになります。

同じようなフレームワークとしてあげられるのが「Vue」や「Angular」ですが、npmのトレンドでは世界的にはReactがダントツで人気のフレームワークとなっています。

image.png

日本では「Vue」が人気の傾向がありましたが、最近「React」の人気がどんどん高まってきており、個人的な間隔としてはVueよりReactを選択するケースが増えてきているように感じます。

CAMELORS株式会社の調査では案件が多いフレームワーク1位にも輝きました

つまり今後学んでおくと役に立つ可能性の高いフレームワークだということがわかります。
私自信もプログラミングコーチングJISOUというサービスを運営していますが、ここでもReactを専門に指導をしています。それくらい将来性のあるフレームワークだと考えています。

Webサイトを作るとき、DOM(Document Object Model)をJavaScriptを使って操作することでユーザー操作で動きを変えていきます。

const element = document.querySelector();
element.classList.add();
element.addEventListener();

JavaScriptで操作をしているとコードはどんどん複雑になってしまいます。
そこでReactはコードが複雑になりにくい点や他のメリットが多くあることから利用されるようになりました。

1. 宣言的View

まずはJavaScriptとReactのコードを比較します。
以下はボタンをクリックするとアラートがでるような実装です。

JavaScript

        function updateUI() {
            const app = document.getElementById('app');
            app.innerHTML = '<h1>Hello, World!</h1>';
        }

        document.getElementById('updateButton').addEventListener('click', updateUI);

React

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function App() {
    const [message, setMessage] = useState('');

    const updateUI = () => {
        setMessage('Hello, World!');
    };

    return (
        <div>
            <div>{message}</div>
            <button onClick={updateUI}>Update</button>
        </div>
    );
}

JavaScriptはDOM要素を手動で操作し、UIを更新します。UIの状態が変更されるたびに、手動でDOMを変更する必要があります

宣言的なコード(React)では、 状態が変更されると、自動的にUIが再レンダリングされます。UIの状態と見た目を直接対応させることで、コードがシンプルで読みやすくなります。

        <div>
            <div>{message}</div>
            <button onClick={updateUI}>Update</button>
        </div>

操作の部分が入っていないことですごくわかりやすいコードになっています。
このわかりやすさがメンテナンスのしやすさにつながってきます。

2. コンポーネントベース

ページを作るときにコンポーネントという要素を組み合わせます。
たとえば以下のページであればこのようなコンポーネント構成になっています。

image.png

ヘッダーの部分、バナーの部分などそれぞれがパーツになっており、それらを組み合わせることによって1枚のページが作成されています。

このようにコンポーネントというブロックを作ることによって、部品化ができるため改修がしやすかったり、再利用がしやすくなります。

3. 一度学習すれば、どこでも使える

Reactは一度使い方を覚えれば色々応用が効きます。モバイルアプリを作りたいなら「ReactNative」、ARなら「React360」など記述方法がほぼ同じで色々なものに技術が使えるのはMeta社のイチオシポイントのようです。

ここまででReactの特徴を紹介しましたが、Reactは仮想DOMを採用しているのも良いポイントです。
本来であればJavaScriptでDOMを変更するため操作をして、HTMLのDOMを新しく構築します。

仮想DOMを利用すると、DOMをコピーした仮想的なDOMでJavaScriptの変更をうけとって差分だけを変更して描画をしてくれます。つまりDOM全体を更新する必要がないのでパフォーマンスが上がります。

このように初心者WebエンジニアがReactを学ぶことは今後を考えても賢い選択だと思っています。
実際にこの記事ではReactのハンズオンをしながら抑えておきたい基本を紹介していきますので、学びながらReactの魅力を感じていきましょう

1. 環境構築

まずはReactを開発できる環境を用意しましょう。それにはJavaScriptを実行するためのNode.jsを動かせるようにする必要があります。

ダウンロードは以下のサイトから行ってください。

OSによってはインストールの方法も変わるため、調べれば簡単に導入することができます。

Reactの環境を作る方法はいくつかありますが、今回はViteを利用します。

Viteを使うことで環境構築が楽なだけでなくWebpackなどよりも早いアプリケーションになります。

$ npm -v
10.8.1

$ node -v
v22.4.0

$ npm create vite
✔ Project name: … react-beginner-tutorial
✔ Select a framework: › React
✔ Select a variant: › TypeScript

$ cd react-beginner-tutorial
$ npm i // ライブラリをインストール
$ npm run dev // サーバーを起動する

http://localhost:5173にアクセスして以下の画面が出ればReactの環境を作ることができたことがわかります。

image.png

ではこのディレクトリをVSCodeで開きましょう
ここからはReact開発で必須の「Prettier」という拡張機能を入れていきます。
Prettierを入れることによってコードのインデントなどを直してくれるようになります。

image.png

Prettierをインストールしたら、設定を開いて

image.png

「format on save」と検索して、チェックをいれましょう
image.png

もう1つ拡張機能をいれます。「ES7+React/Redux/React-Native snippets」と検索して入れてください。
image.png

2. ディレクトリ構成について

image.png

色々ファイルがありますが、ここでは初心者が知っておきたいものを説明していきます。

src/App.tsx はReactの心臓部分のようなファイルで、ここからUIの構築やルーティングなどを記述してアプリケーションを構築していきます。

src/main.tsxはReactアプリケーションの「スタート地点」です。このファイルは、App.tsxで定義されたアプリケーション全体をブラウザに表示するための設定を行います。

src/main.tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

index.htmlではmain.tsxを読み込んでいます。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

src/main.tsxでは以下のような記述があります

ReactDOM.createRoot(document.getElementById('root')!).render(

ここでHTMLの

  <body>
    <div id="root"></div>

この部分でApp.tsxのレンダリングが行われています。

package.jsonは、Node.jsプロジェクト(Reactアプリを含む)における設定ファイルです。このファイルは、プロジェクトに関する基本的な情報や、依存しているライブラリ、スクリプトなどを管理します。

tsconfig.jsonはTypeScriptプロジェクトの設定ファイルです。このファイルは、TypeScriptコンパイラに対して、どのようにコードを解釈し、変換するかを指示します。
この設定を変更することでより厳密なコードを書くことも可能になります。

3. コンポーネントを作成する

今回は簡単な家計簿アプリを作りながらReactについて学んでいきます。
まずはじめはページを構成する部品であるコンポーネントをやっていきます。

srcの下にTitle.tsxを作成します。Reactのコンポーネントを作るにはtsxというファイル形式で作る必要があります。

$ touch src/Title.tsx

コンポーネントは以下のように作成します。

Title.tsx
export const Title = () => {
  return (
    <div>家計簿アプリ</div>
  )
}

このように書くことで、<Title />タグでタイトルを表示できるようになります。
作成したコンポーネントをApp.tsxでインポートして、コンポーネントを利用してみましょう

App.tsx
import './App.css'
import { Title } from './Title'

function App() {
  return (
    <div>
      <Title />
    </div>
  )
}

export default App

次に不要なスタイルを消しておきます
index.cssの中身をすべて消してください (左寄りに表示されてしまいます)

実際に画面をみると「家計簿アプリ」と表示されるようになりました

image.png

では、再利用してみましょう

App.tsx
import './App.css'
import { Title } from './Title'

function App() {
  return (
    <div>
      <Title />
      <Title />
      <Title />
    </div>
  )
}

export default App

image.png

再利用もすごく簡単にできました!

HTML部分にはテンプレートリテラルも利用できます。

Title.tsx
export const Title = () => {

  const name = "Yamada"
  return (
    <div>{name}の家計簿アプリ</div>
  )
}

image.png

4. ライブラリの導入

まずはフレームワークとライブラリの違いをはっきりさせましょう

フレームワークとは簡単に言うと、デフォルトでアプリケーション開発に必要なものが揃っているもの
ライブラリとは作業を簡単にするためのあらかじめ用意されたプログラムの集まり

ということができます。

今回はライブラリの中でもコンポーネントライブラリである「ChakraUI」を導入します。
ChakuraUIを利用するとリッチなUIを簡単な記述で使えるようになりますので、初心者でもいい感じのWebサイトを簡単に作ることができるようになります。

$ npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion

src/App.tsxにChakraUIを使う設定をします。

App.tsx
import { ChakraBaseProvider } from '@chakra-ui/react'
import './App.css'
import { Title } from './Title'

function App() {
  return (
    <ChakraBaseProvider>
      <div>
        <Title />
        <Title />
        <Title />
      </div>
    </ChakraBaseProvider>
  )
}

export default App

ChakraProviderをインポートして、HTMLの外側を覆うようにしました
このタグの内側にあるものはChakraUIを利用することができるようになります。

正しく導入できたかボタンを表示して確認してみましょう
<Buttoと打つとVSCodeが補完を出してくれるので、Tabキーを押してみましょう
自動でボタンコンポーネントのインポートまでしてくれます

image.png

App.tsx
import { Button, ChakraProvider } from '@chakra-ui/react'
import './App.css'
import { Title } from './Title'

function App() {
  return (
    <ChakraProvider>
      <div>
        <Title />
        <Title />
        <Title />
        <Button colorScheme="blue">ボタン</Button>
      </div>
    </ChakraProvider>
  )
}

export default App

image.png

リッチなボタンが表示できました。

<Button colorScheme="blue">ボタン</Button>

ボタンコンポーネントにはcolorSchemeというプロパティに値を渡すことで見た目を変更できます

ChakraUIを使うのであればドキュメントをみてどのようにカスタマイズできるのかをみるようにしましょう!
では実際にTextコンポーネントを使ってアプリ名を表示しましょう(私達の作ったコンポーネントとはお別れです)

App.tsx
import {ChakraProvider, Text } from '@chakra-ui/react'
import './App.css'

function App() {
  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
      </div>
    </ChakraProvider>
  )
}

export default App

サイズは2xlに設定しました

image.png

タイトルをつけることができたので、ここから支出を入力できるようなコンポーネントを作成していきます!

5. 入力フォームの作成

まずはじめに支出の記録をするための入力フォームと追加ボタンを作成していきます。

src/App.tsx
import { Button, ChakraProvider, Checkbox, Flex, Input, Text } from '@chakra-ui/react'
import './App.css'

function App() {
  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
        <div>
          <Input placeholder='支出を入力' mb="4px" />
          <Flex align="center" justifyContent="space-between">
            <Checkbox w="100px">入金</Checkbox>
            <Button colorScheme="teal">追加</Button>
          </Flex>
        </div>
      </div>
    </ChakraProvider>
  )
}

export default App

image.png

このアプリでは支出を記録しますが、もし給料などお金が手元に入ったときには入金をチェックすることでプラスで計算できるようんします。

入力フォームは以下のコンポーネントを利用しました

入金のチェックボックスはこちらを利用しています

入金のチェックボックスと追加ボタンを横並びにしたいのでdisploy: flexをする必要があります。
ChakaUIには簡単にflexができるコンポーネントがあるので使いました

<Flex align="center" justifyContent="space-between">

align="center"で縦に中央揃え、justifyContent="space-between"で左右に均等に配置しました
これらのプロパティは<Flex>に用意されているものでドキュメントをみると何が使えるのかを確認できます

 <Input placeholder='支出を入力' mb="4px" />

このmb="4px"もマージンを下に4pxあけるプロパティで、こちらも用意されているものを使っています。

6. リストの表示

では次にテストデータを用意して支出を表示ていきましょう

src/App.tsx
import { Box, Button, ChakraProvider, Checkbox, Flex, Input, Text } from '@chakra-ui/react'
import './App.css'

function App() {
 const testData = [
   // 支出と入金のデータを作成
   {
     id: 1,
     title: 'お金を払う',
     isIncome: false,
     amount: 1000,
   },
   {
     id: 2,
     title: 'お金をもらう',
     isIncome: true,
     amount: 1000,
   },
 ]
 return (
   <ChakraProvider>
     <div>
       <Text fontSize="2xl">家計簿アプリ</Text>
       <Box mb="8px">
         <Input placeholder='支出を入力' mb="4px" />
         <Flex align="center" justifyContent="space-between">
           <Checkbox w="100px">入金</Checkbox>
           <Button colorScheme="teal">追加</Button>
         </Flex>
       </Box>
       <div>
         {testData.map((data) => (
           <div key={data.id}>
             <Flex align="center" justifyContent="space-between">
               <Text>{data.title}</Text>
               <Text>{data.isIncome ? "+" : "-"}{data.amount}</Text>
             </Flex>
           </div>
         ))}
       </div>
     </div>
   </ChakraProvider>
 )
}

export default App

image.png

まずはテストデータを用意しました
支出と入金があるため、どちらかわかるようにisIncomeという項目を用意しました。
trueなら入金、falseなら支出を表現しています

 let testData = [
   // 支出と入金のデータを作成
   {
     id: 1,
     title: 'お金を払う',
     isIncome: false,
     amount: 1000,
   },
   {
     id: 2,
     title: 'お金をもらう',
     isIncome: true,
     amount: 1000,
   },
 ]

入力フォームとリストの表示の間にマージンをあけたかったので、divからBoxにタグを変えました
意味合いはあまり変わりませんが、BoxはChakraUIが用意しているコンポーネントなのでmbのようなプロパティが使えるのでCSSを書かないでスタイリングができます

       <Box mb="8px">
         <Input placeholder='支出を入力' mb="4px" />
         <Flex align="center" justifyContent="space-between">
           <Checkbox w="100px">入金</Checkbox>
           <Button colorScheme="teal">追加</Button>
         </Flex>
       </Box>

リストの表示は以下のように書きました

       <div>
         {testData.map((data) => (
           <div key={data.id}>
             <Flex align="center" justifyContent="space-between">
               <Text>{data.title}</Text>
               <Text>{data.isIncome ? "+" : "-"}{data.amount}</Text>
             </Flex>
           </div>
         ))}
       </div>

すこしわかりづらいですがmapを使っているため展開すると以下のようになります

<div>
  <div key={testData[0].id}>
    <Flex align="center" justifyContent="space-between">
      <Text>{testData[0].title}</Text>
      <Text>{testData[0].isIncome ? "+" : "-"}{testData[0].amount}</Text>
    </Flex>
  </div>
  <div key={testData[1].id}>
    <Flex align="center" justifyContent="space-between">
      <Text>{testData[1].title}</Text>
      <Text>{testData[1].isIncome ? "+" : "-"}{testData[1].amount}</Text>
    </Flex>
  </div>
</div>

支出か入金かを表すために3項演算子を使って、isIncomeなら+、それ以外なら-を表示しました

<Text>{testData[1].isIncome ? "+" : "-"}{testData[1].amount}</Text>

ここで疑問に思うのはkeyの存在です

<div key={data.id}>

リストの表示をするときにはkeyという項目に一意の値を入れる必要があります。
これはReact がどのアイテムが変更、追加、または削除されたかを識別するのに役立ちます。
もしkeyがない場合、このリストに変更が加わったときにどの要素が変更されたのかがわからず、支出の記録の一部を再描画するのではなく、全体を再描画することに繋がりパフォーマンスが落ちてしまいます
また、変な挙動を引き起こす原因にもなってしまいます。

簡単な例を表示します。
このアプリケーションはkeyを指定しないで作成しました。

Peek 2024-08-16 12-49.gif

使用したコード
import { useState } from 'react'

type Item = { value: string }[]

const App = () => {
  const [inputValue, setInputValue] = useState('')
  const [list, setList] = useState<Item>([])

  const addToList = () => {
    setList((prevList) => {
      return [{ value: inputValue }, ...prevList]
    })
    setInputValue('')
  }

  return (
    <>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={addToList}>追加</button>
      <ul>
        {list.map((item) => {
          return <li>{item.value}</li>
        })}
      </ul>
    </>
  )
}

export default App

参考
https://qiita.com/parkon_hhs/items/0794a72648be86be0df4

リストに追加をするとulの中のすべてのliが更新されていることがわかります。
Keyを指定することで、追加した部分だけを更新することができるようになります。

最初は難しいと感じると思うのでmapを使ったならkeyを一意なもので指定しておくことを覚えておけばよいでしょう。

7. イベントハンドラ

リストが表示できたので次は実際にインプットフォームに入力をして追加できるようんしましょう
ReactにはイベントハンドラというDom要素を処理してくれる便利な機能があります。

例えば、追加ボタンをクリックしたらコンソールにHello Worldを出すようにしてみましょう

<Button colorScheme="teal" onClick={ () => console.log("Hello World")}>追加</Button>

onClickを利用することでイベントを設定した要素をクリックしたときに関数を実行することができます。

image.png

インプットフォームなど入力する要素に対してはonChangeが利用できます

<Input placeholder='支出を入力' mb="4px" onChange={(e) => console.log(e.target.value)} />

onChangeでは(e) => と書くことでイベントを受け取ることができます。
イベントの中から変更した値を受け取るにはe.target.valueとすることでインプットフォームの入力内容を表示することが可能です

image.png

1文字打つたびに変更されるのでonChnageが動いて値が表示されます。

ではこれらを用いて追加ボタンを押したら記録データのオブジェクトをコンソールに表示するようにしましょう
タイトルの入力フォームもついでに用意します

src/App.tsx
import { Box, Button, ChakraProvider, Checkbox, Flex, Input, Text } from '@chakra-ui/react'
import './App.css'

function App() {
  let testData = [
    // 支出と入金のデータを作成
    {
      id: 1,
      title: 'お金を払う',
      isIncome: false,
      amount: 1000,
    },
    {
      id: 2,
      title: 'お金をもらう',
      isIncome: true,
      amount: 1000,
    },
  ]

  let title: string = ''
  let amount: number = 0
  let isIncome: boolean = false

  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
        <Box mb="8px">
          <Input placeholder='タイトルを入力' mb="4px" onChange={(e) => title = e.target.value} />
          <Input placeholder='支出を入力' mb="4px" onChange={(e) => amount = Number(e.target.value)} />
          <Flex align="center" justifyContent="space-between">
            <Checkbox w="100px" onChange={() => isIncome = !isIncome}>入金</Checkbox>
            <Button colorScheme="teal" onClick={ () => console.log(
              title,
              amount,
              isIncome
            )}>追加</Button>
          </Flex>
        </Box>
        <div>
          {testData.map((data) => (
            <div key={data.id}>
              <Flex align="center" justifyContent="space-between">
                <Text>{data.title}</Text>
                <Text>{data.isIncome ? "+" : "-"}{data.amount}</Text>
              </Flex>
            </div>
          ))}
        </div>
      </div>
    </ChakraProvider>
  )
}

export default App

image.png

入力して追加ボタンを押すことでコンソールに表示ができました。

では、これをテストデータに追加するようにしてみましょう

 <Button colorScheme="teal" onClick={ () => testData.push({ id: 999, "title": title, "isIncome": isIncome, "amount": amount})}>追加</Button>

しかし、追加ボタンを押しても新しい支出は表示されません

image.png

DOM要素を変更して再描画したい場合には、ステートを利用する必要があります。
次はこちらを説明していきます。

8. ステート

画面に表示されているデータなどを変更する場合にはuseStateというフック(React の機能を接続するための関数)を使います。

実際に使うほうがイメージが付きやすいのでまずは利用したコードから載せます

src/App.tsx
import { Box, Button, ChakraProvider, Checkbox, Flex, Input, Text } from '@chakra-ui/react'
import './App.css'
import { useState } from 'react'

type Record = {
  id: number
  title: string
  isIncome: boolean
  amount: number
}

function App() {
  const [records, setRecords] = useState<Record[]>([])
  const [title, setTitle] = useState<string>('')
  const [isIncome, setIsIncome] = useState<boolean>(false)
  const [amount, setAmount] = useState<number>(0)

  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
        <Box mb="8px">
          <Input placeholder='タイトルを入力' mb="4px" onChange={(e) => setTitle(e.target.value)} />
          <Input placeholder='支出を入力' mb="4px" onChange={(e) => setAmount(Number(e.target.value))} />
          <Flex align="center" justifyContent="space-between">
            <Checkbox w="100px" onChange={() => setIsIncome(!isIncome)}>入金</Checkbox>
            <Button colorScheme="teal" onClick={() => setRecords([...records, {id: records.length + 1, "title": title, "isIncome": isIncome, "amount": amount}])}>追加</Button>
          </Flex>
        </Box>
        <div>
          {records.map((data) => (
            <div key={data.id}>
              <Flex align="center" justifyContent="space-between">
                <Text>{data.title}</Text>
                <Text>{data.isIncome ? "+" : "-"}{data.amount}</Text>
              </Flex>
            </div>
          ))}
        </div>
      </div>
    </ChakraProvider>
  )
}

export default App

これで実際に追加ができるようになりました

Peek 2024-08-16 20-27.gif

では詳しく説明していきます。
まずはuseStateの部分です

  const [records, setRecords] = useState<Record[]>([])
  const [title, setTitle] = useState<string>('')
  const [isIncome, setIsIncome] = useState<boolean>(false)
  const [amount, setAmount] = useState<number>(0)

useStateはこのように書くことができてrecordsがステート、setRecordsがステートの値を更新する関数になります。ステートが更新された場合もし画面に表示されているのであれば描画が行われます。
タイトルや金額もステートを使って管理するようにしました

例えばタイトルが更新されたら以下のようにステートを更新します

<Input placeholder='タイトルを入力' mb="4px" onChange={(e) => setTitle(e.target.value)} />

チェックボックスはtrue/falseなので、デフォルト値をfalseにして、チェックされたら逆を現在のisIncomeの逆の値で更新するようにしています

const [isIncome, setIsIncome] = useState<boolean>(false) // 初期値 false

<Checkbox w="100px" onChange={() => setIsIncome(!isIncome)}>入金</Checkbox>

次にrecordsの更新ですが、

<Button colorScheme="teal" onClick={() => setRecords([...records, {id: records.length + 1, "title": title, "isIncome": isIncome, "amount": amount}])}>追加</Button>

すでにあるrecordsを展開してその後ろに新しいrecordを追加して新しい配列を作成しています。
ここで疑問に思った人もいるかと思います。

<Button colorScheme="teal" onClick={() => setRecords.push({id: records.length + 1, "title": title, "isIncome": isIncome, "amount": amount})}>追加</Button>

このようにもとの配列に対して最後にpushすればよいように思えます。
しかしこれではステートの更新が行われません。ステートの更新には新しい変数を与える必要があります。
もしpushにしてしまうと配列の中身まではuseStateでは気にしないので(変数のアドレスは同じ)、配列自体が更新されてないと勘違いされてしまうのです。

配列の更新をする場合は展開をするということを覚えておきましょう

最後にいちいち追加するたびにタイトルと金額を消さないといけないのが大変なので直していきましょう

src/App.tsx
import {
  Button,
  ChakraProvider,
  Checkbox,
  Flex,
  Input,
  Text,
} from "@chakra-ui/react";
import { useState } from "react";

type Record = {
  id: number;
  title: string;
  amount: number;
  isIncome: boolean;
};

function App() {
  const [records, setRecords] = useState<Record[]>([]);
  const [title, setTitle] = useState<string>("");
  const [amount, setAmount] = useState<number>(0);
  const [isIncome, setIsIncome] = useState<boolean>(false);

  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
        <div>
          <Input
            placeholder="タイトルを入力"
            mb="4px"
            onChange={(e) => setTitle(e.target.value)}
            value={title}
          />
          <Input
            placeholder="金額を入力"
            mb="4px"
            type="number"
            onChange={(e) => setAmount(Number(e.target.value))}
            value={amount}
          />
          <Flex align="center" justifyContent="space-between">
            <Checkbox
              onChange={() => setIsIncome(!isIncome)}
              isChecked={isIncome}
            >
              入金
            </Checkbox>
            <Button
              colorScheme="blue"
              onClick={() => {
                const newRecord: Record = {
                  id: records.length + 1,
                  title: title,
                  amount: amount || 0,
                  isIncome: isIncome,
                };
                setRecords([...records, newRecord]);
                setTitle("");
                setAmount(0);
                setIsIncome(false);
              }}
            >
              追加
            </Button>
          </Flex>
        </div>
        <div>
          {records.map((record) => (
            <div key={record.id}>
              <Flex align="center" justifyContent="space-between">
                <Text>{record.title}</Text>
                <Text>
                  {record.isIncome ? "+" : "-"}
                  {record.amount}
                </Text>
              </Flex>
            </div>
          ))}
        </div>
      </div>
    </ChakraProvider>
  );
}

export default App;

インプットフォームにvalueを設定することで現在のステートをインプットの入力として入れてくれます

          <Input
            placeholder="金額を入力"
            mb="4px"
            type="number"
            onChange={(e) => setAmount(Number(e.target.value))}
            value={amount}
          />

そして追加ボタンを押したらtitle``amount``isIncomeを初期値に更新することでフォームから消せます

            <Button
              colorScheme="blue"
              onClick={() => {
                const newRecord: Record = {
                  id: records.length + 1,
                  title: title,
                  amount: amount || 0,
                  isIncome: isIncome,
                };
                setRecords([...records, newRecord]);
                setTitle("");
                setAmount(0);
                setIsIncome(false);
              }}
            >
              追加
            </Button>

追加ボタンのところが大きくなってきたので、追加の処理を関数に切り分けてあげましょう

src/App.tsx
import {
  Button,
  ChakraProvider,
  Checkbox,
  Flex,
  Input,
  Text,
} from "@chakra-ui/react";
import { useState } from "react";

type Record = {
  id: number;
  title: string;
  amount: number;
  isIncome: boolean;
};

function App() {
  const [records, setRecords] = useState<Record[]>([]);
  const [title, setTitle] = useState<string>("");
  const [amount, setAmount] = useState<number>(0);
  const [isIncome, setIsIncome] = useState<boolean>(false);

  const addRecord = () => {
    const newRecord: Record = {
      id: records.length + 1,
      title: title,
      amount: amount || 0,
      isIncome: isIncome,
    };
    setRecords([...records, newRecord]);
    setTitle("");
    setAmount(0);
    setIsIncome(false);
  };

  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
        <div>
          <Input
            placeholder="タイトルを入力"
            mb="4px"
            onChange={(e) => setTitle(e.target.value)}
            value={title}
          />
          <Input
            placeholder="金額を入力"
            mb="4px"
            type="number"
            onChange={(e) => setAmount(Number(e.target.value))}
            value={amount}
          />
          <Flex align="center" justifyContent="space-between">
            <Checkbox
              onChange={() => setIsIncome(!isIncome)}
              isChecked={isIncome}
            >
              入金
            </Checkbox>
            <Button colorScheme="blue" onClick={addRecord}>
              追加
            </Button>
          </Flex>
        </div>
        <div>
          {records.map((record) => (
            <div key={record.id}>
              <Flex align="center" justifyContent="space-between">
                <Text>{record.title}</Text>
                <Text>
                  {record.isIncome ? "+" : "-"}
                  {record.amount}
                </Text>
              </Flex>
            </div>
          ))}
        </div>
      </div>
    </ChakraProvider>
  );
}

export default App;

addRecordという関数に処理を移動しました

  const addRecord = () => {
    const newRecord: Record = {
      id: records.length + 1,
      title: title,
      amount: amount || 0,
      isIncome: isIncome,
    };
    setRecords([...records, newRecord]);
    setTitle("");
    setAmount(0);
    setIsIncome(false);
  };

そしてハンドラでは関数を呼び出すだけになってとても読みやすくなりました

<Button colorScheme="blue" onClick={addRecord}>
    追加
</Button>

9. useEffect

最後にReactで必ず利用するuseEffectを紹介します。
useEffectとはコンポーネントを外部システムと同期させるための React フックです。

画面を表示する前に外部のAPIなどを叩いて記録データを取得して、描画前に用意することで、画面が表示されたときには外部のDBにある支出データを事前に表示することができるようになります。

ここでは簡易的なモックサーバーをjson-serverを使って作成していきます。

$ npm i json-server
$ touch db.json

db.jsonに今回サーバーから返すjsonを定義します

db.json
{
  "records": [
    {
      "id": 1,
      "title": "お金を払う",
      "isIncome": false,
      "amount": 1000
    },
    {
      "id": 2,
      "title": "お金をもらう",
      "isIncome": true,
      "amount": 2000
    }
  ]
}

ではモックサーバーを起動しましょう
package.jsonというところにコマンドを追加します
この設定をすることでnpm run json-serverを動かすとjson-server --watch db.jsonを実行することができます

package.json
{
  "name": "typescript-beginner-tutorial",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "json-server": "json-server --watch db.json" // 追加
  },
  "dependencies": {
    "@chakra-ui/react": "^2.8.2",
    "@emotion/react": "^11.13.0",
    "@emotion/styled": "^11.13.0",
    "framer-motion": "^11.3.24",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^7.15.0",
    "@typescript-eslint/parser": "^7.15.0",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^8.57.0",
    "eslint-plugin-react-hooks": "^4.6.2",
    "eslint-plugin-react-refresh": "^0.4.7",
    "typescript": "^5.2.2",
    "vite": "^5.3.4"
  }
}
$ npm run json-server

$ curl localhost:3000/records
[
  {
    "id": "1",
    "title": "お金を払う",
    "isIncome": false,
    "amount": 1000
  },
  {
    "id": "2",
    "title": "お金をもらう",
    "isIncome": true,
    "amount": 2000
  }
]j

http://localhost:3000/recordsにcurlするとjsonが返ってきました

このデータをアプリを開いたら取得して、recordsの初期値にしてきます。

src/App.tsx
import { Box, Button, ChakraProvider, Checkbox, Flex, Input, Text } from '@chakra-ui/react'
import './App.css'
import { useEffect, useState } from 'react'

type Record = {
  id: number
  title: string
  isIncome: boolean
  amount: number
}

function App() {
  const [records, setRecords] = useState<Record[]>([])
  const [title, setTitle] = useState<string>('')
  const [isIncome, setIsIncome] = useState<boolean>(false)
  const [amount, setAmount] = useState<number>(0)

  useEffect(() => {
    getRecords()

    async function getRecords() {
      const response = await fetch('http://localhost:3000/records')
      const data = await response.json()
      setRecords(data)
    }
  }, [])


  return (
    <ChakraProvider>
      <div>
        <Text fontSize="2xl">家計簿アプリ</Text>
        <Box mb="8px">
          <Input placeholder='タイトルを入力' mb="4px" onChange={(e) => setTitle(e.target.value)} />
          <Input placeholder='支出を入力' mb="4px" onChange={(e) => setAmount(Number(e.target.value))} />
          <Flex align="center" justifyContent="space-between">
            <Checkbox w="100px" onChange={() => setIsIncome(!isIncome)}>入金</Checkbox>
            <Button colorScheme="teal" onClick={() => setRecords([...records, {id: records.length + 1, "title": title, "isIncome": isIncome, "amount": amount}])}>追加</Button>
          </Flex>
        </Box>
        <div>
          {records.map((data) => (
            <div key={data.id}>
              <Flex align="center" justifyContent="space-between">
                <Text>{data.title}</Text>
                <Text>{data.isIncome ? "+" : "-"}{data.amount}</Text>
              </Flex>
            </div>
          ))}
        </div>
      </div>
    </ChakraProvider>
  )
}

export default App

image.png

アプリを開くとサーバーから取得した値を初期値にしています。
ではuseEffectの実装を見てみましょう

  useEffect(() => {
    getRecords()

    async function getRecords() {
      const response = await fetch('http://localhost:3000/records')
      const data = await response.json()
      setRecords(data)
    }
  }, [])

useEffectの中ではasync直接呼び出せないので、非同期でデータを取得する関数を定義して関数を呼び出すようにしています。
データを取得したらrecordsの初期値としてsetRecordsを使ってデータを入れています。

useEffect(() => {
 ここに画面描画前にやりたい処理
 }, []

この書き方を覚えておけば初心者の方は大丈夫です
[]も意味があるので、そこは気になる方は調べてみてください

これにて家計簿アプリは完成です🎉🎉

おわりに

今回はReactをやる中で絶対に抑えておきたい頻出のスキルを一通りハンズオンを通して学習しました
この内容があればTODOアプリなどを作れるスキルが身についているはずです。
ぜひともここで習ったことを活かしてアプリを作ってみてください。自分で実際につかうことでより理解を深めることが可能です。

今回の内容は以下の動画でも学べますのでより詳しく知りたい方はぜひご覧ください

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

普段はTwitterでエンジニアに関する情報を発信していますのでよければ友達になってください👇

また明日の記事でお会いしましょう!

JISOUのメンバー募集中!

プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?

興味のある方は、ぜひホームページからお気軽にご連絡ください!
▼▼▼

445
590
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
445
590