Open9
(フロントエンド経験ゼロの人が書いている)Tauri+React勉強メモ
プロジェクトの作成 ~ 開発サーバー起動まで
■ 環境
- 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
- ? Project name (tauri-app)
■ 依存関係のインストール
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
エントリポイントから新規プロジェクトの中身を見てみる
index.html
main.tsx
App.tsx
TypeScript → Rust 連携
作ってみる画面
メイン画面用のコンポーネントを作成
メイン画面用のコンポーネントを 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>,
);
画面表示は以下になる
タブビューを作成
■ コンポーネント構成
■ 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;
ここまでの画面
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;
画面
入力タブ(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;
画面
入力タブ(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;
一覧タブ(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} />}
];
画面