ReactとMUIでWebアプリの作成

awsアプリケーション開発


当記事では、Reactで作成されたWebアプリをCSSフレームワークである「MUI」を利用して画面デザインをオシャレにする方法を紹介します。

Z-A-K-I
Z-A-K-I

アプリケーションの作り方はわかった。

次はデザインをいい感じにしていきたい。

と、思ってる方におすすめです。

この記事では、以下のことを紹介してます。

  • MaterialUI(MUI)とは
  • ReactのフロントエンドアプリにMUIを適用するコード

利用者の満足度を向上させるためには、アプリケーションの機能だけでなく、使いやすい画面デザインが不可欠です。

本記事では、Reactと相性の良いCSSフレームワークのひとつであるMaterialUIを使って具体的にデザインを変更する方法を手順を追って紹介します。

デザイン変更する対象のアプリケーションは、以下記事で作成したWebアプリです。

ReactのコードにMUIを適用するプログラム

作成するWebアプリ

データを検索・登録・更新を行う以下画面にMUIを適用していきます。

変更後は以下の画面になります。

変更後の画面仕様

変更内容を説明します。

元々の画面にあったデータ登録用のフォームは削除し、代わりに「ADD RECORD」リンクをクリックすると一覧の最下部に登録用の行が新規に追加されるように変更になります。

検索直後は表示モードですが、鉛筆アイコンをクリックすることで編集モードとなり、データを変更することができます。フロッピーアイコンをクリックすることで保存され、表示モードに戻ります。

将来的にレコードの削除もできるようなUIとしてます。ゴミ箱アイコンをクリックすることでレコード削除できるようにします。

MUIのDataGridを利用することで、ページ切り替え機能、各項目のフィルター、ソートが利用できるようになります。

MUIの取得方法

MUIとは

この記事を読んでいる方であればご存知と思いますが、MUIについて紹介します。

MUIは、Reactで作成された画面によく適用されるCSSフレームワークです(UIフレームワークとも言います)。これを適用することで簡単にリッチな画面を提供することができます。

MUIについては、以下記事でも紹介してますので、より詳しく知りたい方はこちらもご参照ください。

MUIの取得方法

MUIは、以下コマンドを実行することで対象のプロジェクトに導入できます。

npm install @mui/material @emotion/react @emotion/styled

MUIの公式サイトは以下です。

Material UI: React components that implement Material Design
Material UI is an open-source React component library that implements Google's Material Design. It's...

ReactコードにMUIのスタイルの適用

フロントエンドアプリの作成

前回の作成したソースコードにMUIを適用していきます。

変更前のコードは以下記事を参照ください。

適用後は、以下コードになります。

import React, { useState } from 'react';
import axios from 'axios';
import './App.css';
import Box from '@mui/material/Box';

import EditIcon from '@mui/icons-material/Edit';

import Button from '@mui/material/Button';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Close';
import {
  DataGrid,
  GridRowModes,
  GridToolbarContainer,
  GridActionsCellItem,
  GridRowEditStopReasons,
} from '@mui/x-data-grid';
import {
  randomId,
} from '@mui/x-data-grid-generator';

import TextField from '@mui/material/TextField';


function App() {

  const [name, setName] = useState('');

  const handleSearch = async () => {
    // 処理結果をクリア

    try {
      const response = await axios({
        method: 'post',
        url: 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test/get-user-data',
        data: {
          queryStringParameters: {
            name: name
          }
        }
      });
      const usersData = JSON.parse(response.data.body);
      const editableUsersData = usersData.map(user => ({
        ...user,
        editId: user.id,
        editName: user.name,
        editAge: user.age,
        editEmail: user.email
      }));
      setRows(editableUsersData);
    } catch (error) {
      console.error('検索中にエラーが発生しました:', error);
    }
  };

  // 更新ボタンの処理
  const handleUpdate = async (updUser) => {
    try {
      const response = await axios({
        method: 'post',
        url: 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test/update-user-data', // 実際のエンドポイントURLに置き換えてください
        data: {
          queryStringParameters: {
            id: updUser.id === undefined ? "" : updUser.id,
            name: updUser.name === undefined ? "" : updUser.name,
            age: updUser.age === undefined ? "" : updUser.age,
            email: updUser.email === undefined ? "" : updUser.email
          }
        }
      });
      // 処理結果を状態にセット
      // setUpdateStatus({ ...updateStatus, [index]: '処理が正常終了しました' });
    } catch (error) {
      // エラー処理
      // setUpdateStatus({ ...updateStatus, [index]: 'エラーが発生しました' });
    }
  };

  // 新しいデータを登録する関数
  const handleRegister = async (newUser) => {
    try {
      const response = await axios({
        method: 'post',
        url: 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/test/update-user-data', // 実際のエンドポイントURLに置き換えてください
        data: {
          queryStringParameters: {
            id: newUser.id === undefined ? "" : newUser.id,
            name: newUser.name === undefined ? "" : newUser.name,
            age: newUser.age === undefined ? "" : newUser.age,
            email: newUser.email === undefined ? "" : newUser.email
          }
        }
      });
      // 登録成功時の処理
      console.log('データが正常に登録されました。', response);
      
    } catch (error) {
      // 登録失敗時のエラー処理
      console.error('データ登録中にエラーが発生しました。', error);
    }
  };

  // ツールバーコンポーネント
  function EditToolbar(props) {
    const { setRows, setRowModesModel } = props;
  
    const handleClick = () => {
      const id = randomId();
      setRows((oldRows) => [...oldRows, { id, name: '', age: '', isNew: true }]);
      setRowModesModel((oldModel) => ({
        ...oldModel,
        [id]: { mode: GridRowModes.Edit, fieldToFocus: 'name' },
      }));
    };
  
    return (
      <GridToolbarContainer>
        <Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
          Add record
        </Button>
      </GridToolbarContainer>
    );
  }

  const [rows, setRows] = React.useState([]);
  const [rowModesModel, setRowModesModel] = React.useState({});

  const handleRowEditStop = (params, event) => {
    if (params.reason === GridRowEditStopReasons.rowFocusOut) {
      event.defaultMuiPrevented = true;
    }
  };
  //  編集ボタンクリック
  const handleEditClick = (id) => () => {
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
  };
  // 編集ボタンクリック
  const handleSaveClick = (id) => () => {
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
  };
  // 削除ボタンクリック
  const handleDeleteClick = (id) => () => {
    setRows(rows.filter((row) => row.id !== id));
  };
  // キャンセルボタンクリック
  const handleCancelClick = (id) => () => {
    setRowModesModel({
      ...rowModesModel,
      [id]: { mode: GridRowModes.View, ignoreModifications: true },
    });
    const editedRow = rows.find((row) => row.id === id);
    if (editedRow !== undefined && editedRow.isNew) {
      setRows(rows.filter((row) => row.id !== id));
    }
  };

    // 更新処理
  const processRowUpdate = (newRow) => {
    const updatedRow = { ...newRow, isNew: false };
    if (newRow.isNew){
      handleRegister(newRow);
    } else {
      handleUpdate(newRow);
    }
    setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));

    return updatedRow;
  };

  const handleRowModesModelChange = (newRowModesModel) => {
    setRowModesModel(newRowModesModel);
  };

  // グリッドの項目定義
  const columns = [
    { field: 'id', headerName: 'ID', width: 100, editable: true },
    { field: 'name', headerName: '名前', width: 180, editable: true },
    {
      field: 'age',
      headerName: '年齢',
      width: 80,
      align: 'left',
      headerAlign: 'left',
      editable: true,
    },
    {
      field: 'email',
      headerName: 'メール',
      width: 220,
      editable: true,
     },
    {
      field: 'actions',
      type: 'actions',
      headerName: 'Actions',
      width: 100,
      cellClassName: 'actions',
      getActions: ({ id }) => {
        const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;

        if (isInEditMode) {
          return [
            <GridActionsCellItem
              icon={<SaveIcon />}
              label="Save"
              sx={{
                color: 'primary.main',
              }}
              onClick={handleSaveClick(id)}
            />,
            <GridActionsCellItem
              icon={<CancelIcon />}
              label="Cancel"
              className="textPrimary"
              onClick={handleCancelClick(id)}
              color="inherit"
            />,
          ];
        }

        return [
          <GridActionsCellItem
            icon={<EditIcon />}
            label="Edit"
            className="textPrimary"
            onClick={handleEditClick(id)}
            color="inherit"
          />,
          <GridActionsCellItem
            icon={<DeleteIcon />}
            label="Delete"
            onClick={handleDeleteClick(id)}
            color="inherit"
          />,
        ];
      },
    },
  ];

  return (
    <Box m={2}>
      <div>
        <h3>検索条件</h3>
        <TextField id="standard-basic" label="名前" variant="standard"
        onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
        value={name}
            onChange={(e) => setName(e.target.value)} />
      </div>
      <div>
        <h3>検索結果</h3>
        {/* グリッド部分 */}
        <Box
          sx={{
            height: 500,
            width: '100%',
            '& .actions': {
              color: 'text.secondary',
            },
            '& .textPrimary': {
              color: 'text.primary',
            },
          }}
        >
        <DataGrid
          rows={rows}
          columns={columns}
          editMode="row"
          rowModesModel={rowModesModel}
          onRowModesModelChange={handleRowModesModelChange}
          onRowEditStop={handleRowEditStop}
          processRowUpdate={processRowUpdate}
          slots={{
            toolbar: EditToolbar,
          }}
          slotProps={{
            toolbar: { setRows, setRowModesModel },
          }}
        />
      </Box>
    </div>
  </Box>
  );
}

export default App;

ポイントの解説

ポイントについて解説していきます。

前回からの変更点としては、一覧を自前のHTMLから、MUIのDataGridに置き換えてます。

この変更により不要となったコードを削除し、DataGridのイベント用の関数を新規作成しています。

新規作成したメソッドのほとんどが、stateで管理している処理対象行の値を更新しているものとなります。

ProcessRowUpdateでは、処理対象行が新規データか更新データかを確認し、登録・更新処理を分岐しています。登録・更新処理後、Stateの値を更新し画面の表示も更新しています。

Webアプリのテスト

いつもの通り、ローカルPC上でReactを起動します。VSCodeのターミナル上で以下コマンドを実行します。

npm start

画面が起動し、MUIを適用したUIで検索、更新、登録ができれば成功です。

まとめ

ReactにMUIを適用したWebアプリ作成のまとめ

本記事では、MUIを適用してUIをオシャレにする方法を紹介しました。

前回と合わせて、サーバーレスサービスのみ利用して、画面UIにもこだわったWebアプリを構築することができした。簡易的ですがCRUDのWebアプリが作れましたので、今後はこのアプリをベースにグラフなどを利用した実用的なサービスの構築方法をご紹介していきます。

皆様の参考になりましたら幸いです。

コメント

タイトルとURLをコピーしました