【要件】
1. ユーザ登録機能
- メールアドレスとパスワード、ユーザ名を入力して登録可能
2. ログイン機能
- メールアドレスとパスワードにてログイン可能
3. ダッシュボード
- ログイン後はダッシュボードへ遷移
- ダッシュボードでは、ログインしているユーザ名が表示される
- ダッシュボードでは、自身のウォレット(残高)が表示
4. ログアウト機能
- ダッシュボードにログアウトボタン配置
- ログアウトボタン押下で、ログアウトされログイン画面へ遷移
- ログアウトされた状態でダッシュボードへアクセスすると、ログイン画面へリダイレクト
5. ユーザ一覧表示
- ダッシュボードに、自分以外の登録ユーザが一覧表示
- ユーザを選択し、そのユーザーのウォレットを確認可能
6. 投げ銭機能
- ユーザを選択し、渡す通貨の量を入力後、投げ銭可能
- 投げ銭後は即時にウォレットへ反映される
学習した内容
- firebaseの環境構築
- メールアドレスとパスワードを使用したログイン機能の実装
(firebase Authentication) - firebase cloudfirestoreの使い方
(データの保存、ドキュメントの呼び出し、データの更新、トランザクション) - データを保存/取り出し
- ユーザー登録
- ログイン機能
環境・使用技術
- react-router-dom : ルーティング(ページ移動)モジュール
- firebase : Authentication機能を利用
- react-redux : 状態管理
- redux-thunk : 非同期処理
- material-ui : cssフレームワーク
環境の準備
①ターミナルでreactアプリケーションを作成する。
$ npx create-react-app <プロジェクト名>
% cd <プロジェクト名>
% npm start
② Material-UI を利用するため、事前にライブラリをインストールする。
$ npm install @mui/material @emotion/react @emotion/styled
$ git diff -p
③reduxjs/toolkit framer-motion のインストール
$ npm install --save @reduxjs/toolkit react-redux
$ npm install framer-motion
$ npm install react-router-dom
$ npm install --save react-modal
$ npm install react-redux-firebase
$ npm install --save react-redux-firebase
コンポーネント・ファイル構成
src
├── auth
├── AuthProvider.js
└── Login.js
└── PrivateRoute.js
└── SignUp.js
├── components
├── DisplayTodos.js
├── Header.js
├── TodoItem.js
├── Todos.js
├── redux
├── reducer.js
└── store.js
├── Firebase.js
├── App.js
├── index.js
① 都度ベースとなる画面構成を作成する
$ mkdir src/auth
$ mkdir src/components
$ mkdir src/redux
$ touch src/auth/AuthProvider.js
$ touch src/auth/Login.js
$ touch src/auth/PrivateRoute.js
$ touch src/auth/SignUp.js
$ touch src/components/DisplayTodos.js
$ touch src/components/Header.js
$ touch src/components/TodoItem.js
$ touch src/components/Todos.js
$ touch src/redux/reducer.js
$ touch src/redux/store.js
$ touch src/firebase.js
Firebase
Reactを基本からまとめてみた【20】【Firebase v9を使ったログイン機能の実装②】
1. ユーザ登録機能
- メールアドレスとパスワード、ユーザ名を入力して登録可能
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import SignUp from "./auth/SignUp";
import { AuthProvider } from "./auth/AuthProvider";
ReactDOM.render(
<React.StrictMode>
<Router>
<AuthProvider>
<Routes>
<Route path="/signup" element={<SignUp />} />
</Routes>
</AuthProvider>
</Router>
</React.StrictMode>,
document.getElementById("root")
);
src/auth/AuthProvider.js
import React, { useEffect, useState } from "react";
import { auth } from "../firebase";
import {
createUserWithEmailAndPassword,
onAuthStateChanged,
signInWithEmailAndPassword,
updateProfile,
} from "firebase/auth";
const AuthContext = React.createContext();
const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
//サインアップ後認証情報を更新
const signup = async (name, email, password, navigate) => {
try {
// メアドとパスワードからユーザを作成
const res = await createUserWithEmailAndPassword(auth, email, password);
// 作成したユーザにdisplaynameをセット
updateProfile(res?.user, {
displayName: name,
});
onAuthStateChanged(auth, (user) => {
setCurrentUser(user);
});
// メイン画面へ移動
navigate("/");
} catch (error) {
alert(error);
}
};
//ログインさせる
const login = async (email, password, navigate) => {
try {
await signInWithEmailAndPassword(auth, email, password);
onAuthStateChanged(auth, (user) => setCurrentUser(user));
navigate("/");
} catch (error) {
alert(error);
}
};
//初回アクセス時に認証済みかチェック
useEffect(() => {
onAuthStateChanged(auth, setCurrentUser);
}, []);
return (
<AuthContext.Provider value={{ signup, login, currentUser }}>
{children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthProvider };
src/auth/SignUp.js
import React, { useContext, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import "firebase/auth";
import { AuthContext } from "./AuthProvider";
import { Box, Button, styled, TextField } from "@mui/material";
//import { auth } from './firebase';
const SignUpButton = styled(Button)({
background: "#f16272",
fontSize: "1.0rem",
border: 0,
borderRadius: 3,
color: "white",
padding: "10px 40px",
marginTop: "30px",
"&:hover": {
backgroundColor: "#ee3e52",
},
});
const SignUp = ({ history }) => {
const { signup } = useContext(AuthContext);
//AuthContextからsignup関数を受け取る
const navigate = useNavigate();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
signup(name, email, password, navigate);
};
return (
<div className="wrapper">
<div className="auth-container">
<div style={{ textAlign: "center" }}>
<h1>新規登録</h1>
<form className="auth-form" onSubmit={handleSubmit}>
<div className="auth-form-item">
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
margin="normal"
required
fullWidth
id="Display name"
label="Display name"
name="display name"
autoComplete="display name"
autoFocus
variant="outlined"
value={name}
onChange={(e) => {
setName(e.currentTarget.value);
}}
/>
</Box>
</div>
<div className="auth-form-item">
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
variant="outlined"
value={email}
onChange={(e) => {
setEmail(e.currentTarget.value);
}}
/>
</Box>
</div>
<div className="auth-form-item">
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
variant="outlined"
value={password}
onChange={(e) => {
setPassword(e.currentTarget.value);
}}
/>
</Box>
</div>
<SignUpButton className="signUp-btn" type="submit">
新規登録する
</SignUpButton>
</form>
<Link to="/login" className="auth-bottom">
アカウントをお持ちの方はこちら
</Link>
</div>
</div>
</div>
);
};
export default SignUp;
2. ログイン機能
- メールアドレスとパスワードにてログイン可能
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Login from "./auth/Login";
import SignUp from "./auth/SignUp";
import { AuthProvider } from "./auth/AuthProvider";
ReactDOM.render(
<React.StrictMode>
<Router>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
</Routes>
</AuthProvider>
</Router>
</React.StrictMode>,
document.getElementById("root")
);
src/auth/PrivateRoute.js
import React, { useContext } from "react";
import { Route } from "react-router-dom";
import { AuthContext } from "./AuthProvider";
import Login from "./Login";
const PrivateRoute = ({ component, ...rest }) => {
const { currentUser } = useContext(AuthContext);
//AuthContextからcurrentUserを受け取る
const renderingComponent = currentUser ? component : Login;
//currentUserがtrueの場合component=Home、falseならLoginコンポーネントにroute
return <Route {...rest} component={renderingComponent} />;
};
export default PrivateRoute;
src/auth/Login.js
import React, { useContext, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
//import { Link } from "react-router-dom";
import { AuthContext } from "./AuthProvider";
import { Box, Button, styled, TextField } from "@material-ui/core";
const SignInButton = styled(Button)({
background: "#6fc4f9",
fontSize: "1.0rem",
border: 0,
borderRadius: 3,
color: "white",
padding: "10px 40px",
marginTop: "30px",
"&:hover": {
backgroundColor: "#57baf8",
},
});
const Login = ({ history }) => {
const { login } = useContext(AuthContext);
//AuthContextからlogin関数を受け取る
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
login(email, password, navigate);
};
return (
<div className="wrapper">
<div className="auth-container">
<div style={{ textAlign: "center" }}>
<h1>LogIn</h1>
<form className="auth-form" onSubmit={handleSubmit}>
<div className="auth-form-item">
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
variant="outlined"
value={email}
onChange={(e) => {
setEmail(e.currentTarget.value);
}}
/>
</Box>
</div>
<div className="auth-form-item">
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
variant="outlined"
value={password}
onChange={(e) => {
setPassword(e.currentTarget.value);
}}
/>
</Box>
</div>
<SignInButton type="submit">LOGINする</SignInButton>
</form>
<Link to="/signup" className="auth-bottom">
アカウントをお持ちでない方はこちら
</Link>
</div>
</div>
</div>
);
};
export default Login;
3. ダッシュボード
- ログイン後はダッシュボードへ遷移
- ダッシュボードでは、ログインしているユーザ名が表示される
- ダッシュボードでは、自身のウォレット(残高)が表示
4. ログアウト機能
- ダッシュボードにログアウトボタン配置
- ログアウトボタン押下で、ログアウトされログイン画面へ遷移
- ログアウトされた状態でダッシュボードへアクセスすると、ログイン画面へリダイレクト
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./auth/Login";
import SignUp from "./auth/SignUp";
import { AuthProvider } from "./auth/AuthProvider";
ReactDOM.render(
<React.StrictMode>
<Router>
<AuthProvider>
<Routes>
<Route path="/" element={<App />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
</Routes>
</AuthProvider>
</Router>
</React.StrictMode>,
document.getElementById("root")
);
App.js
import { useState } from "react";
import Header from "./components/Header";
import Todos from "./components/Todos";
import { motion } from "framer-motion";
function App() {
const [myCount, setMyCount] = useState(0);
return (
<div className="App">
<Header />
<div style={{ textAlign: "center" }}>
<motion.h1
initial={{ y: -200 }}
animate={{ y: 0 }}
transition={{ type: "spring", duration: 0.5 }}
whileHover={{ scale: 1.1 }}
>
Money Transfer App
</motion.h1>
<motion.div
initial={{ y: 1000 }}
animate={{ y: 0 }}
transition={{ type: "spring", duration: 1 }}
>
<Todos count={myCount} setCount={setMyCount} />
</motion.div>
</div>
</div>
);
}
export default App;
src/components/Header.js
import React from "react";
import {
AppBar,
Box,
Button,
Toolbar,
Typography,
IconButton,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
const Header = () => {
const navigate = useNavigate();
const handleClick = () => {
navigate("/login");
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
/>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
React課題③
</Typography>
<div style={{ flexGrow: 1 }}></div>
<Button variant="text" color="inherit" onClick={handleClick}>
LogOut
</Button>
</Toolbar>
</AppBar>
</Box>
);
};
export default Header;
src/components/Todo.js
import React, { useContext, useState } from "react";
import { Box, TextField, Button } from "@mui/material";
import { auth } from "../firebase";
import { onAuthStateChanged } from "firebase/auth";
import { AuthContext } from "../auth/AuthProvider";
const Todos = (props) => {
const { count, setCount } = props;
// Contextからログインユーザを取得
const { currentUser } = useContext(AuthContext);
const [num, setNum] = useState(100);
const onCountUp = () => {
setCount(count + num);
};
const onCountDown = () => {
setCount(count - num);
};
return (
<>
<div style={{ textAlign: "center" }}>
<div className="balance-list">
<h2>
{currentUser?.displayName ?? "未ログイン"}
さんの残高 : {count} 円{" "}
</h2>
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
label="入出金額"
variant="outlined"
value={num}
onChange={(e) => setNum(Number(e.target.value))}
/>
</Box>
<Button onClick={onCountUp} variant="outlined" color="primary">
Increment
</Button>
<Button onClick={onCountDown} variant="outlined" color="secondary">
Decrement
</Button>
</div>
</div>
</>
);
};
export default Todos;
5. ユーザ一覧表示
- ダッシュボードに、自分以外の登録ユーザが一覧表示
- ユーザを選択し、そのユーザーのウォレットを確認可能
6. 投げ銭機能
- ユーザを選択し、渡す通貨の量を入力後、投げ銭可能
- 投げ銭後は即時にウォレットへ反映される
FirestoreとReduxを使う際の考え方
1.Firebase初期化 (getFirestore)
firebase.js
import { getFirestore } from "firebase/firestore";
export const firestore = getFirestore(firebaseApp);
2.Firebaseに接続しデータ取得 (onSnapshot)
const unsub = onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
const data = doc.data();
console.log('data', data);
});
return unsub;
});
firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
};
export const firebaseApp = initializeApp(firebaseConfig);
export const auth = getAuth(firebaseApp);
export const db = getFirestore(firebaseApp);
export const firestore = getFirestore(firebaseApp);
3. Firebaseから取得したデータをReduxへ格納
格納するsliceを作成する。
参考サイトredux-toolkit
4. Reduxからデータを読み出し
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Login from "./auth/Login";
import SignUp from "./auth/SignUp";
import { AuthProvider } from "./auth/AuthProvider";
ReactDOM.render(
<React.StrictMode>
<Router>
<Provider store={store}>
<AuthProvider>
<Routes>
<Route path="/" element={<App />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
</Routes>
</AuthProvider>
</Provider>
</Router>
</React.StrictMode>,
document.getElementById("root")
);
App.js
import { useState } from "react";
import DisplayTodos from "./components/DisplayTodos";
import Header from "./components/Header";
import Todos from "./components/Todos";
import { motion } from "framer-motion";
function App() {
const [myCount, setMyCount] = useState(0);
return (
<div className="App">
<Header />
<div style={{ textAlign: "center" }}>
<motion.h1
initial={{ y: -200 }}
animate={{ y: 0 }}
transition={{ type: "spring", duration: 0.5 }}
whileHover={{ scale: 1.1 }}
>
Money Transfer App
</motion.h1>
<motion.div
initial={{ y: 1000 }}
animate={{ y: 0 }}
//style={{ backgroundColor: "red" }}
transition={{ type: "spring", duration: 1 }}
>
<Todos count={myCount} setCount={setMyCount} />
<DisplayTodos count={myCount} setCount={setMyCount} />
</motion.div>
</div>
</div>
);
}
export default App;
src/components/DisplayTodos.js
//addTodos 受取人
//comment 未完了の受取人
//count 送金者の残高
//num 送金者の入金及び出勤額
//balance 受取人の残高
import React, { useState } from "react";
import { connect } from "react-redux";
import {
addTodos,
removeTodos,
updateTodos,
} from "../redux/reducer";
import TodoItem from "./TodoItem";
const mapStateToProps = (state) => {
return {
todos: state?.todos,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (obj) => dispatch(addTodos(obj)),
removeTodo: (id) => dispatch(removeTodos(id)),
updateTodo: (obj) => dispatch(updateTodos(obj)),
};
};
const DisplayTodos = (props) => {
const { count, setCount } = props;
const [sort, setSort] = useState("active");
return (
<div className="displaytodos">
<ol style={{ display: "inline-block" }}>
{props.todos.length > 0 && sort === "active"
? props.todos.map((item) => {
return (
item.completed === false && (
<TodoItem
key={item.id}
item={item}
removeTodo={props.removeTodo}
count={count}
setCount={setCount}
updateTodo={props.updateTodo}
/>
)
);
})
: null}
{props.todos.length > 0 && sort === "completed"
? props.todos.map((item) => {
return (
item.completed === true && (
<TodoItem
key={item.id}
item={item}
removeTodo={props.removeTodo}
count={count}
setCount={setCount}
/>
)
);
})
: null}
{props.todos.length > 0 && sort === "all"
? props.todos.map((item) => {
return (
<TodoItem
key={item.id}
item={item}
removeTodo={props.removeTodo}
count={count}
setCount={setCount}
/>
);
})
: null}
</ol>
</div>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(DisplayTodos);
src/components/Header.js
import React from "react";
import {
AppBar,
Box,
Button,
Toolbar,
Typography,
IconButton,
} from "@material-ui/core";
import { useNavigate } from "react-router-dom";
const Header = () => {
const navigate = useNavigate();
const handleClick = () => {
navigate("/login");
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inheriDefa"
aria-label="menu"
sx={{ mr: 2 }}
></IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
React課題③
</Typography>
<div style={{ flexGrow: 1 }}></div>
<Button variant="text" color="inherit" onClick={handleClick}>
SignOut
</Button>
</Toolbar>
</AppBar>
</Box>
);
};
export default Header;
src/components/TodoItem.js
//addTodos 受取人
//count 送金者の残高
//num 送金者の入金及び出勤額
//balance 受取人の残高
import React, { useState, useRef, useContext } from "react";
import Modal from "react-modal";
import {
Box,
TextField,
Button,
ButtonGroup,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Typography,
} from "@material-ui/core";
import { AuthContext } from "../auth/AuthProvider";
const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
border: "2px solid #000",
boxShadow: 24,
p: 4,
};
// 対象の受取人のWalletダイアログ
const AddTodoWalletDialog = ({ isOpen, addTodo, handleClose }) => {
if (!addTodo) return null;
console.log(addTodo);
return (
<Modal
className="wallet"
isOpen={isOpen}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Typography id="modal-modal-title" variant="h6" component="h2">
{addTodo.item}
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
残高 : {addTodo.balance ?? 0} 円
</Typography>
<Button variant="outlined" color="secondary" onClick={handleClose}>
Close
</Button>
</Box>
</Modal>
);
};
// 対象の受取人への送金ダイアログ
const AddTodoTransferDialog = ({
count,
open,
addTodo,
num,
setNum,
handleClickClose,
handleClickTransferButton,
}) => {
const {currentUser} = useContext(AuthContext);
if (!addTodo) return null;
return (
<Dialog open={open} onClose={handleClickClose}>
<DialogTitle>{addTodo.addTodos}</DialogTitle>
<DialogContent>
<DialogContentText>{currentUser?.displayName ?? '未ログイン'}の残高: {count} 円</DialogContentText>
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<TextField
className="balance"
id="outlined-basic"
label="振込金額を入力してください"
variant="outlined"
value={num}
onChange={(e) => setNum(Number(e.target.value))}
/>
</Box>
</DialogContent>
<ButtonGroup
variant="outlined"
color="primary"
aria-label="outlined primary button group"
></ButtonGroup>
<DialogActions>
<Button variant="outlined" color="secondary" onClick={handleClickClose}>
Cancel
</Button>
<Button
onClick={handleClickTransferButton}
variant="outlined"
color="primary"
>
振込
</Button>
</DialogActions>
</Dialog>
);
};
const TodoItem = (props) => {
const { item, updateTodo, removeTodo, count, setCount } = props;
const [balance, setBalance] = useState(0);
const [num, setNum] = useState(100);
const [open, setOpen] = React.useState(false);
const [IsOpen, setIsOpen] = React.useState(false);
const [targetTodo, setTargetTodo] = React.useState(null);
const inputRef = useRef(true);
const changeFocus = () => {
setIsOpen(true);
setTargetTodo(item);
};
const onCountDown = () => {
setCount(count - num);
};
const onBalanceUp = () => {
setBalance(balance + num);
};
const transfer = () => {
const newTodo = {...item, balance: item?.balance + num};
updateTodo?.(newTodo);
}
const handleClickOpen = (todo) => {
setTargetTodo(todo);
setOpen(true);
};
const handleClickClose = () => {
setOpen(false);
};
const handleClose = () => {
setIsOpen(false);
};
const handleClickTransferButton = () => {
onCountDown();
onBalanceUp();
transfer();
};
return (
<div className="balance-list">
<li key={item.id} className="card">
<p>{item.item}</p>
<div className="btns">
<Button
className="wallet"
variant="outlined"
color="primary"
onClick={() => changeFocus()}
>
walletを見る
</Button>
<Button
className="sendmoney"
variant="outlined"
color="primary"
onClick={() => handleClickOpen(item.id)}
>
送金
</Button>
<Button
variant="outlined"
color="secondary"
onClick={() => removeTodo(item.id)}
>
削除
</Button>
{""}
</div>
{item.completed && <span className="completed">done</span>}
</li>
<AddTodoWalletDialog
isOpen={IsOpen}
addTodo={targetTodo}
handleClose={handleClose}
/>
<AddTodoTransferDialog
open={open}
addTodo={targetTodo}
num={num}
setNum={setNum}
count={count}
handleClickClose={handleClickClose}
handleClickTransferButton={handleClickTransferButton}
/>
</div>
);
};
export default TodoItem;
src/components/Todos.js
//addTodos 受取人
//comment 未完了の受取人
//count 送金者の残高
//num 送金者の入金及び出勤額
//balance 受取人の残高
import React, { useContext, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { addTodos, onCountDown, onCountUp } from '../redux/reducer';
import { Box, TextField, Button } from '@mui/material';
import { AuthContext } from '../auth/AuthProvider';
import { useFirestoreConnect, useFirestore } from 'react-redux-firebase';
import { db } from '../firebase';
import { onSnapshot, collection, query } from 'firebase/firestore';
const mapStateToProps = (state) => {
return {
todos: state?.todos,
counter: state?.counter,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (addingtodo) => dispatch(addTodos(addingtodo)),
onCountUp: (addCount) => dispatch(onCountUp(addCount)),
onCountDown: (subtractCount) => dispatch(onCountDown(subtractCount)),
};
};
const Todos = (props) => {
const { counter } = props;
const [todo, setTodo] = useState('');
// Contextからログインユーザを取得
const { currentUser } = useContext(AuthContext);
const handleChange = (e) => {
setTodo(e.target.value);
};
const [num, setNum] = useState(100);
const firestore = useFirestore();
useEffect(() => {
const q = query(collection(db, 'senders'));
const unsub = onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
const data = doc.data();
});
return unsub;
});
});
const addTodo = () => {
return firestore.add('addTodo', {
idCount: 1,
item: todo,
completed: false,
balance: 0,
});
};
useFirestoreConnect({
collection: 'addTodo',
where: [['todo', '==', '0']],
});
const add = () => {
if (todo === '') {
alert('Input is Empty');
} else {
props.addTodo({
id: Math.floor(Math.random() * 1000),
item: todo,
completed: false,
balance: 0,
});
setTodo('');
}
};
return (
<>
<div style={{ textAlign: 'center' }}>
<div className='balance-list'>
<h2>
{currentUser?.displayName ?? '未ログイン'}
さんの残高 : {counter.value} 円
</h2>
<Box
component='form'
sx={{
'& > :not(style)': { m: 1, width: '25ch' },
}}
noValidate
autoComplete='off'
>
<TextField
label='入出金額'
variant='outlined'
value={num}
onChange={(e) => setNum(Number(e.target.value))}
/>
</Box>
<Button
onClick={() => props.onCountUp(num)}
variant='outlined'
color='primary'
>
Increment
</Button>
<Button
onClick={() => props.onCountDown(num)}
variant='outlined'
color='secondary'
>
Decrement
</Button>
</div>
<div className='addTodos'>
<h2>受取人一覧</h2>
<Box
component='form'
sx={{
'& > :not(style)': { m: 1, width: '25ch' },
}}
noValidate
autoComplete='off'
>
<TextField
id='outlined-basic'
label='受取人を入力してください'
variant='outlined'
type='text'
onChange={(e) => handleChange(e)}
className='todo-input'
value={todo}
/>
</Box>
<Button
variant='outlined'
color='primary'
className='add-btn'
onClick={() => add()}
>
受取人追加
</Button>
<br />
<h2>受取人名</h2>
</div>
</div>
</>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(Todos);
/src/redux/reducer.js
import { createSlice } from "@reduxjs/toolkit";
export const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
onCountUp: (state, action) => {
state.value += action.payload;
},
onCountDown: (state, action) => {
state.value -= action.payload;
},
},
});
export const { onCountUp, onCountDown } = counterSlice.actions;
export const counterReducer = counterSlice.reducer;
const initialState = {
todos: [],
};
const addTodoReducer = createSlice({
name: "todos",
initialState: [],
reducers: {
//todoを追加
addTodos: (state, action) => {
return [...state, action.payload];
},
//todoを削除
removeTodos: (state, action) => {
return state?.filter((item) => item.id !== action.payload);
},
//todoを更新
updateTodos: (state, action) => {
return state?.map((todo) => {
if (todo.id === action.payload.id) {
return {
...todo,
balance: action.payload.balance,
};
}
return todo;
});
},
},
});
export const { addTodos, removeTodos, updateTodos } = addTodoReducer.actions;
export const todoReducer = addTodoReducer.reducer;
src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import { todoReducer, counterReducer } from "./reducer";
const store = configureStore({
reducer: {
todos: todoReducer,
counter: counterReducer,
},
});
export default store;
参考サイト
【React Redux初心者向け】Todoリスト作成を通してしっかり学ぶRedux
Building a Todo List App with Redux 😍 | ( React-Redux ) Tutorial For Beginners
Firebase CloudFirestoreの使い方を初心者が解説してみた
FirestoreのデータをReduxに反映するシンプルなライブラリの紹介
firestoreとreduxのバインディングライブラリ「react-redux-firebase」の使い方
Chromeのデベロッパーツールで
JavaScriptをデバッグする方法(2022年版)
react-redux-firebase