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

## 【要件】

#### 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を使ったログイン機能の実装②】](https://qiita.com/kanfutrooper/items/92e59e282c5a69015a3a)

#### 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](https://redux-toolkit.js.org/api/createslice)

##### 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](https://reffect.co.jp/react/react-redux-todo)
[Building a Todo List App with Redux 😍 | ( React-Redux ) Tutorial For Beginners](https://www.youtube.com/watch?v=YhgSuUkWlK4)
[Firebase CloudFirestoreの使い方を初心者が解説してみた](https://qiita.com/misa_m/items/0afbd063ff13b49f2729)
[FirestoreのデータをReduxに反映するシンプルなライブラリの紹介](https://qiita.com/macinjoke/items/0720a4416b1b19ffc0a3)
[firestoreとreduxのバインディングライブラリ「react-redux-firebase」の使い方](https://tech.frenps.co.jp/archives/561)
[Chromeのデベロッパーツールで
JavaScriptをデバッグする方法（2022年版）](https://ics.media/entry/190517/)
[react-redux-firebase](http://react-redux-firebase.com/)