Open9

(フロントエンド経験ゼロの人が書いている)Tauri+React勉強メモ

Y.MarrY.Marr

プロジェクトの作成 ~ 開発サーバー起動まで

■ 環境

  • Widnows11
  • npm:10.7.0
  • node:20.12.0
  • rustc:1.78.0
  • pnpm:9.12.3
  • Tauri:2.0.3

■ プロジェクトの作成

  • コマンド
pnpm create tauri-app
  • 画面に従って入力。今回は以下のように設定
    • ? Project name (tauri-app)
      • → hello-tauri-react
    • ? Identifier (com.hello-tauri-react.app)
      • → 空白のままリターン。
    • ? Choose which language to use for your frontend
      • → TypeScript / JavaScript
    • ? Choose your package manager
      • → pnpm
    • ? Choose your UI template
      • → React
    • ? Choose your UI flavor
      • → TypeScript

■ 依存関係のインストール

cd hello-tauri-react
pnpm install
記事作成時の依存関係
Downloading @tauri-apps/cli-win32-x64-msvc@2.0.4: 7.52 MB/7.52 MB, done
node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild: Running postinstall script, done in 403ms

dependencies:
+ @tauri-apps/api 2.0.3
+ @tauri-apps/plugin-shell 2.0.1
+ react 18.3.1
+ react-dom 18.3.1

devDependencies:
+ @tauri-apps/cli 2.0.4
+ @types/react 18.3.12
+ @types/react-dom 18.3.1
+ @vitejs/plugin-react 4.3.3
+ typescript 5.6.3
+ vite 5.4.10

■ 開発サーバーの起動

pnpm tauri dev

画面が立ち上がれば成功

Y.MarrY.Marr

メイン画面用のコンポーネントを作成

メイン画面用のコンポーネントを MainView という名前で作成。

hello-tauri-react/src/MainView.tsx
import React from 'react'

const MainView: React.FC = () => {
    return (
        <div>
            Test
        </div>
    );
};

export default MainView;

main.tsx を書き換える

hello-tauri-react/src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import MainView from "./MainView";    // ★修正

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <MainView />    {// ★修正 }
  </React.StrictMode>,
);

画面表示は以下になる

Y.MarrY.Marr

タブビューを作成

■ コンポーネント構成

■ StudentInfoInputTab, StudentListTab コンポーネント作成

(StudentListTab もほぼ同様なので省略)

hello-tauri-react/src/compenents/StudentInfoInputTab.tsx
import React from 'react';

const StudentInfoInputTab: React.FC = () => {
    return (
        <div>
            StudentInfoInputTab
        </div>
    );
};

export default StudentInfoInputTab;

■ MainView の修正

hello-tauri-react/src/MainView.tsx
import React, {useState} from 'react'
import StudentInfoInputTab from './compenents/StudentInfoInputTab';
import StudentListTab from './compenents/StudentListTab';

type Tab = {
    label: string;
    content: React.ReactNode;
}

const tabViewList: Tab[] = [
    { label: 'Input', content: <StudentInfoInputTab />},
    { label: 'Student List', content: <StudentListTab />}
];

const MainView: React.FC = () => {
    const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
    
    const handleTabClick = (index: number) => {
        setActiveTabIndex(index);
    };

    return (
        <div>
            <div style={{ display: 'flex', borderBottom: '1px solid #ccc' }}>
                {
                    tabViewList.map((tabView, index) => (
                        <button
                            key={index}
                            onClick={() => handleTabClick(index)}
                            style={{
                                padding: '10px 20px',
                                cursor: 'pointer',
                                borderBottom: activeTabIndex === index ? '2 solid blue' : 'none',
                                backgroundColor: activeTabIndex === index ? '#CCB3DD' : 'transparent',
                                border: 'none',
                                outline: 'none'
                            }}>
                            { tabView.label }
                        </button>
                    ))
                }
            </div>
            <div style={{ padding: '10px' }}>
                { tabViewList[activeTabIndex].content }
            </div>
        </div>
    );
};

export default MainView;

ここまでの画面

Y.MarrY.Marr

MUI を使ってタブビューを更新

■ MUI のインストール

pnpm add @mui/material @emotion/react @emotion/styled

■ MainView の修正

hello-tauri-react/src/MainView.tsx
import React, {useState} from 'react'
import StudentInfoInputTab from './compenents/StudentInfoInputTab';
import StudentListTab from './compenents/StudentListTab';
import {
    Box,
    Tab,
    Tabs,
  } from '@mui/material';

const tabViewList: Tab[] = [
    { label: '入力', content: <StudentInfoInputTab />},
    { label: '一覧', content: <StudentListTab />}
];

const MainView: React.FC = () => {
    const [tabValue, setTabValue] = useState(0);

    const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
        setTabValue(newValue);
    };

    return (
        <Box padding={2} width="400px"> 
            <Tabs value={tabValue} onChange={handleTabChange}>
                {
                    tabViewList.map((tabView, index) => (
                        <Tab label={ tabView.label } />
                    ))
                }
            </Tabs>

            { tabViewList[tabValue].content }
        </Box>
    );
};

export default MainView;

画面

Y.MarrY.Marr

入力タブ(StudentInfoInputTab)の作成

hello-tauri-react/src/compenents/StudentInfoInputTab.tsx
import React, { useState } from 'react';
import {
  Box,
  TextField,
  FormControl,
  FormLabel,
  RadioGroup,
  FormControlLabel,
  Radio,
  Checkbox,
  FormGroup,
  Select,
  MenuItem,
  Button,
} from '@mui/material';

const StudentInfoInputTab: React.FC = () => {
    const [gender, setGender] = useState('');
    const [subjects, setSubjects] = useState<string[]>([]);
    const [committee, setCommittee] = useState('');

    const handleGenderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setGender(event.target.value);
    };

    const handleSubjectChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const value = event.target.name;
        setSubjects((prev) =>
        prev.includes(value) ? prev.filter((s) => s !== value) : [...prev, value]
        );
    };

    const handleCommitteeChange = (event: React.ChangeEvent<{ value: unknown }>) => {
        setCommittee(event.target.value as string);
    };

    const handleAdd = () => {
        // データを保存するなどの処理を追加
        console.log({ gender, subjects, committee });
    };
    
    return (
        <Box mt={2}>
            <TextField fullWidth label="名前" variant="outlined" />

            <FormControl component="fieldset" margin="normal">
                <FormLabel component="legend">性別</FormLabel>
                <RadioGroup row value={gender} onChange={handleGenderChange}>
                    <FormControlLabel value="男性" control={<Radio />} label="男性" />
                    <FormControlLabel value="女性" control={<Radio />} label="女性" />
                </RadioGroup>
            </FormControl>

            <FormControl component="fieldset" margin="normal">
                <FormLabel component="legend">得意科目</FormLabel>
                <FormGroup row>
                {
                    ['国語', '数学', '社会', '理科', '英語'].map((subject) => (
                        <FormControlLabel
                            key={subject}
                            control={
                                <Checkbox
                                    checked={subjects.includes(subject)}
                                    onChange={handleSubjectChange}
                                    name={subject}
                                />
                            }
                            label={subject}
                        />
                    ))
                }
                </FormGroup>
            </FormControl>

            <FormControl fullWidth margin="normal">
                <FormLabel component="legend">委員</FormLabel>
                <Select value={committee} onChange={handleCommitteeChange} displayEmpty>
                    <MenuItem value="">
                        <em>選択してください</em>
                    </MenuItem>
                    {
                        ['図書', '保健', '体育', '風紀', '文化'].map((option) => (
                            <MenuItem key={option} value={option}>
                                {option}
                            </MenuItem>
                        ))
                    }
                </Select>
            </FormControl>

            <Box mt={2} textAlign="right">
                <Button variant="contained" color="primary" onClick={handleAdd}>
                追加
                </Button>
            </Box>
        </Box>
    );
};

export default StudentInfoInputTab;

画面

Y.MarrY.Marr

入力タブ(StudentInfoInputTab)の追加ボタン押下で入力内容を内部状態値のリストに追加

hello-tauri-react/src/compenents/StudentInfoInputFormData.tsx
interface StudentInfoInputFormData {
    id: number;
    name: string;
    gender: string;
    subjects: string[];
    committee: string;
}

export default StudentInfoInputFormData;
hello-tauri-react/src/MainView.tsx
import React, {useState} from 'react'
import StudentInfoInputFormData from './compenents/StudentInfoInputFormData';
import StudentInfoInputTab from './compenents/StudentInfoInputTab';
import StudentListTab from './compenents/StudentListTab';
import {
    Box,
    Tab,
    Tabs,
  } from '@mui/material';
  
type TabDefine = {
    label: string;
    content: React.ReactNode;
}

const MainView: React.FC = () => {
    const [tabValue, setTabValue] = useState(0);
    const [data, setData] = useState<StudentInfoInputFormData[]>([]);
    
    const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
        setTabValue(newValue);
    };

    const handleAddData = (newData: Omit<StudentInfoInputFormData, 'id'>) => {
        console.log(newData);
        setData([...data, { ...newData, id: data.length + 1 }]);
        console.log(data);
    };
    
    const tabDefineList: TabDefine[] = [
        { label: '入力', content: <StudentInfoInputTab onAddData={handleAddData} />},
        { label: '一覧', content: <StudentListTab />}
    ];

    return (
        <Box padding={2} width="400px"> 
            <Tabs value={tabValue} onChange={handleTabChange}>
                {
                    tabDefineList.map((tabDefine) => (
                        <Tab label={ tabDefine.label } />
                    ))
                }
            </Tabs>

            { tabDefineList[tabValue].content }
        </Box>
    );
};

export default MainView;
hello-tauri-react/src/compenents/StudentInfoInputTab.tsx
import React, { useState } from 'react';
import StudentInfoInputFormData from './StudentInfoInputFormData';
import {
  Box,
  TextField,
  FormControl,
  FormLabel,
  RadioGroup,
  FormControlLabel,
  Radio,
  Checkbox,
  FormGroup,
  Select,
  MenuItem,
  Button,
} from '@mui/material';

interface InputTabProps {
    onAddData: (data: Omit<StudentInfoInputFormData, 'id'>) => void;
}

const StudentInfoInputTab: React.FC<InputTabProps> = ({ onAddData }) => {
    const [name, setName] = useState('');  
    const [gender, setGender] = useState('');
    const [subjects, setSubjects] = useState<string[]>([]);
    const [committee, setCommittee] = useState('');

    const handleGenderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setGender(event.target.value);
    };

    const handleSubjectChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const value = event.target.name;
        setSubjects((prev) =>
        prev.includes(value) ? prev.filter((s) => s !== value) : [...prev, value]
        );
    };

    const handleCommitteeChange = (event: React.ChangeEvent<{ value: unknown }>) => {
        setCommittee(event.target.value as string);
    };

    const handleAdd = () => {
        onAddData({ name, gender, subjects, committee });
        setName('');
        setGender('');
        setSubjects([]);
        setCommittee('');
    };
    
    return (
        <Box mt={2}>
            <TextField fullWidth label="名前" variant="outlined" value={name} onChange={(e) => setName(e.target.value)} />

            <FormControl component="fieldset" margin="normal">
                <FormLabel component="legend">性別</FormLabel>
                <RadioGroup row value={gender} onChange={handleGenderChange}>
                    <FormControlLabel value="男性" control={<Radio />} label="男性" />
                    <FormControlLabel value="女性" control={<Radio />} label="女性" />
                </RadioGroup>
            </FormControl>

            <FormControl component="fieldset" margin="normal">
                <FormLabel component="legend">得意科目</FormLabel>
                <FormGroup row>
                {
                    ['国語', '数学', '社会', '理科', '英語'].map((subject) => (
                        <FormControlLabel
                            key={subject}
                            control={
                                <Checkbox
                                    checked={subjects.includes(subject)}
                                    onChange={handleSubjectChange}
                                    name={subject}
                                />
                            }
                            label={subject}
                        />
                    ))
                }
                </FormGroup>
            </FormControl>

            <FormControl fullWidth margin="normal">
                <FormLabel component="legend">委員</FormLabel>
                <Select value={committee} onChange={handleCommitteeChange} displayEmpty>
                    <MenuItem value="">
                        <em>選択してください</em>
                    </MenuItem>
                    {
                        ['図書', '保健', '体育', '風紀', '文化'].map((option) => (
                            <MenuItem key={option} value={option}>
                                {option}
                            </MenuItem>
                        ))
                    }
                </Select>
            </FormControl>

            <Box mt={2} textAlign="right">
                <Button variant="contained" color="primary" onClick={handleAdd}>
                追加
                </Button>
            </Box>
        </Box>
    );
};

export default StudentInfoInputTab;
Y.MarrY.Marr

一覧タブ(StudentListTab)に入力した内容をDataGridを表示する

pnpm add @mui/x-data-grid
hello-tauri-react/src/compenents/StudentListTab.tsx
import React, { useState } from 'react';
import { Box, Button } from '@mui/material';
import { DataGrid, GridColDef, GridRowSelectionModel, GridValueGetterParams } from '@mui/x-data-grid';
import StudentInfoInputFormData from './StudentInfoInputFormData';

interface StudentListTabProps {
  data: StudentInfoInputFormData[];
  onDeleteData: (idsToDelete: number[]) => void;
}

const StudentListTab: React.FC<StudentListTabProps> = ({ data, onDeleteData }) => {
    const [selectionModel, setSelectionModel] = useState<GridRowSelectionModel>([]);

    const columns: GridColDef[] = [
        { field: 'name', headerName: '名前', width: 150 },
        { field: 'gender', headerName: '性別', width: 100 },
        { 
            field: 'subjects', 
            headerName: '得意科目', 
            width: 200, 
            valueGetter: (params: GridValueGetterParams) => {
              return params.join(', ');
            },
        },
        { field: 'committee', headerName: '委員', width: 100 },
    ];

    const handleDelete = () => {
        onDeleteData(selectionModel as number[]);
    };

    return (
        <Box mt={2}>
          <DataGrid
            rows={data}
            columns={columns}
            checkboxSelection={true}
            disableRowSelectionOnClick={true}
            // memo:onSelectionModelChange={(newSelection) => { だとチェックボックス選択時に反応しなかったため
            //       onRowSelectionModelChange に変更
            onRowSelectionModelChange={(newSelection) => {
                setSelectionModel(newSelection as GridRowSelectionModel);
            }}
          />
          <Box mt={2} textAlign="right">
            <Button variant="contained" color="secondary" onClick={handleDelete}>
              削除
            </Button>
          </Box>
        </Box>
      );
};

export default StudentListTab;
hello-tauri-react/src/MainView.tsx
 (変更箇所のみ)
    const handleDeleteData = (idsToDelete: number[]) => {
        setData(data.filter((row) => !idsToDelete.includes(row.id)));
    };

    const tabDefineList: TabDefine[] = [
        { label: '入力', content: <StudentInfoInputTab onAddData={handleAddData} />},
        { label: '一覧', content: <StudentListTab data={data} onDeleteData={handleDeleteData} />}
    ];

画面