1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React (ReduxToolkit + Firebase)の構成でアプリを作成しました【投げ銭アプリ】

Last updated at Posted at 2021-12-31

【要件】

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

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?