[react]第5週開発ログ


スパティンエンコーディングクラブ「フロントの花、反応」の5週間の講座を聞いた後に書いた文章です.

ReduxからFireStoreデータを交換する


まず、非同期通信に必要なミドルウェアをインストールします.
yarn add redux-thunk
  • redux-thunk:オブジェクトではなくアクション作成関数を作成できます.
  • configStore.js
  • import { createStore, combineReducers, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import bucket from "./modules/bucket";
    import { createBrowserHistory } from "history";
    
    export const history = createBrowserHistory();
    
    const middlewares = [thunk];
    
    const enhancer = applyMiddleware(...middlewares);
    const rootReducer = combineReducers({ bucket });
    const store = createStore(rootReducer, enhancer);
    
    export default store;
  • Firestoreアプリケーション順序
    1)ロード時にデータをインポートする(Firebaseと通信する関数を作成し減速機を修正する)
    2)createにFirebaseを適用する(Firebaseと通信する関数を作成し、減速機を修正→読み込みと書き込み)
    3)prestoreをupdateに適用する
    4)delete
  • にFirestoreを適用する
  • bucket.js
  • import { firestore } from "../../firebase";
    
    const bucket_db = firestore.collection("bucket");
    
    // Actions
    const LOAD = "bucket/LOAD";
    const CREATE = "bucket/CREATE";
    const DELETE = "bucket/DELETE";
    const UPDATE = "bucket/UPDATE";
    
    const LOADED = "bucket/LOADED";
    
    const initialState = {
      list: [
        { text: "영화관 가기", completed: false },
        { text: "매일 책읽기", completed: false },
        { text: "기타 배우기", completed: false },
      ],
    
      is_loaded: false,
    };
    
    // Action Creators
    export const loadBucket = (bucket) => {
      return { type: LOAD, bucket };
    };
    
    export const createBucket = (bucket) => {
      // bucket = text
      return { type: CREATE, bucket };
    };
    
    export const deleteBucket = (bucket) => {
      return { type: DELETE, bucket };
    };
    
    export const updateBucket = (bucket) => {
      return { type: UPDATE, bucket };
    };
    
    export const isLoaded = (loaded) => {
      return { type: LOADED, loaded };
    };
    
    export const loadBucketFB = () => {
      return function (dispatch) {
        bucket_db.get().then((docs) => {
          let bucket_data = [];
    
          docs.forEach((doc) => {
            if (doc.exists) {
              bucket_data = [...bucket_data, { id: doc.id, ...doc.data() }];
            }
          });
    
          console.log(bucket_data);
    
          dispatch(loadBucket(bucket_data));
        });
      };
    };
    
    export const addBucketFB = (bucket) => {
      return function (dispatch) {
        let bucket_data = { text: bucket, completed: false };
    
        dispatch(isLoaded(false));
    
        bucket_db.add(bucket_data).then((docRef) => {
          bucket_data = { ...bucket_data, id: docRef.id };
          dispatch(createBucket(bucket_data));
          dispatch(isLoaded(true));
        });
      };
    };
    
    export const updateBucketFB = (bucket) => {
      return function (dispatch, getState) {
        const _bucket_data = getState().bucket.list[bucket];
    
        let bucket_data = { ..._bucket_data, completed: true };
    
        if (!bucket_data.id) {
          return;
        }
    
        bucket_db
          .doc(bucket_data.id)
          .update(bucket_data)
          .then((docRef) => {
            dispatch(updateBucket(bucket));
          })
          .catch((error) => {
            console.log(error);
          });
      };
    };
    
    export const deleteBucketFB = (bucket) => {
      return function (dispatch, getState) {
        const _bucket_data = getState().bucket.list[bucket];
    
        if (!_bucket_data.id) {
          return;
        }
    
        bucket_db
          .doc(_bucket_data.id)
          .delete()
          .then((docRef) => {
            dispatch(deleteBucket(bucket));
          })
          .catch((error) => {
            console.log(error);
          });
      };
    };
    
    // Reducer
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        case "bucket/LOAD": {
          if (action.bucket.length > 0) {
            return { list: action.bucket, is_loaded: true };
          }
          return state;
        }
        case "bucket/CREATE": {
          const new_bucket_list = [...state.list, action.bucket];
          return { list: new_bucket_list };
        }
    
        case "bucket/DELETE": {
          const bucket_list = state.list.filter((l, idx) => {
            if (idx !== action.bucket) {
              return l;
            }
          });
          return { list: bucket_list };
        }
    
        case "bucket/UPDATE": {
          const bucket_list = state.list.map((l, idx) => {
            if (idx === action.bucket) {
              return { ...l, completed: true };
            } else {
              return l;
            }
          });
          return { list: bucket_list };
        }
    
        case "bucket/LOADED": {
          return { ...state, is_loaded: action.loaded };
        }
    
        default:
          return state;
      }
    }

    material-uiの使用


    インストール
  • :糸add@material-ui/core@material-ui/アイコン
  • material-ui Detailを使用jsページボタン
  • を変更
  • Detail.js
  • import React from "react";
    import Button from "@material-ui/core/Button";
    import ButtonGroup from "@material-ui/core/ButtonGroup";
    
    import { useSelector, useDispatch } from "react-redux";
    import { updateBucketFB, deleteBucketFB } from "./redux/modules/bucket";
    
    const Detail = (props) => {
      const dispatch = useDispatch();
    
      const bucket_list = useSelector((state) => state.bucket.list);
      console.log(bucket_list, props);
      const bucket_index = parseInt(props.match.params.index);
    
      return (
        <div>
          <h1>{bucket_list[bucket_index].text}</h1>
          <ButtonGroup>
            <Button
              color="secondary"
              onClick={() => {
                dispatch(deleteBucketFB(bucket_index));
                props.history.goBack();
              }}
            >
              삭제하기
            </Button>
            <Button
              color="primary"
              onClick={() => {
                dispatch(updateBucketFB(bucket_index));
                props.history.goBack();
              }}
            >
              완료하기
            </Button>
          </ButtonGroup>
        </div>
      );
    };
    
    export default Detail;

    Spinnerオフセット

  • でリフレッシュすると、まずredoxに格納された偽データが表示されます.
    →Firestoreのデータだけを正しく表示したい場合は、データを読み込む前にページを隠します.
  • Spinner.js
  • import React from "react";
    import styled from "styled-components";
    import { Eco } from "@material-ui/icons";
    
    const Spinner = (props) => {
      return (
        <Outter>
          <Eco style={{ color: "#673ab7", fontSize: "150px" }} />
        </Outter>
      );
    };
    
    const Outter = styled.div`
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: #ede2ff;
    `;
    
    export default Spinner;
  • App.js
  • import React from "react";
    import styled from "styled-components";
    
    import { withRouter } from "react-router";
    import { Route, Switch } from "react-router-dom";
    
    import BucketList from "./BucketList";
    import Detail from "./Detail";
    import NotFound from "./NotFound";
    
    import { connect } from "react-redux";
    import { loadBucketFB, addBucketFB } from "./redux/modules/bucket";
    import Progress from "./Progress";
    import Spinner from "./Spinner";
    
    import { firestore } from "./firebase";
    
    const mapStateToProps = (state) => ({
      bucket_list: state.bucket.list,
      is_loaded: state.bucket.is_loaded,
    });
    
    const mapDispatchToProps = (dispatch) => {
      return {
        load: () => {
          dispatch(loadBucketFB());
        },
    
        create: (new_item) => {
          dispatch(addBucketFB(new_item));
        },
      };
    };
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {};
    
        this.text = React.createRef();
      }
    
      componentDidMount() {
        this.props.load();
    
        // const bucket = firestore.collection("bucket2");
    
        // bucket.doc("bucket_item1").set({ text: "수영 배우기", completed: false });
    
        // bucket
        //   .doc("bucket_item")
        //   .get()
        //   .then((doc) => {
        //     if (doc.exists) {
        //       console.log(doc);
        //       console.log(doc.data());
        //       console.log(doc.id);
        //     }
        //     console.log(doc.exists);
        //   });
    
        // bucket.get().then((docs) => {
        //   let bucket_data = [];
    
        //   docs.forEach((doc) => {
        //     if (doc.exists) {
        //       bucket_data = [...bucket_data, { id: doc.id, ...doc.data() }];
        //     }
        //   });
    
        //   console.log(bucket_data);
        // });
    
        // // bucket.add({ text: "드럼 배우기", completed: false }).then((docRef) => {
        // //   console.log(docRef);
        // //   console.log(docRef.id);
        // // });
    
        // // bucket.doc("bucket_item").update({ text: "기타 배우기2" });
    
        // bucket
        //   .doc("bucket_item2")
        //   .delete()
        //   .then((docRef) => {
        //     console.log("지웠어요!");
        //   });
      }
    
      addBucketList = () => {
        const new_item = this.text.current.value;
        this.props.create(new_item);
      };
    
      render() {
        return (
          <div className="App">
            {!this.props.is_loaded ? (
              <Spinner />
            ) : (
              <React.Fragment>
                <Container>
                  <Title>My BucketList</Title>
                  <Progress />
                  <Line />
                  <Switch>
                    <Route path="/" exact component={BucketList} />
                    <Route path="/detail/:index" component={Detail} />
                    <Route component={NotFound} />
                  </Switch>
                </Container>
                <Input>
                  <input type="text" ref={this.text} />
                  <button onClick={this.addBucketList}>추가하기</button>
                </Input>
                <button
                  onClick={() => {
                    window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
                  }}
                >
                  위로가기
                </button>
              </React.Fragment>
            )}
          </div>
        );
      }
    }
    
    const Input = styled.div`
      max-width: 350px;
      min-height: 10vh;
      background-color: #fff;
      padding: 16px;
      margin: 20px auto;
      border-radius: 5px;
      border: 1px solid #ddd;
      display: flex;
      align-items: center;
      justify-content: center;
      & > * {
        padding: 10px;
      }
    
      & input {
        border-radius: 5px;
        margin-right: 10px;
        border: 1px solid #5d5d5d;
        width: 60%;
        &:focus {
          border: 1px solid #a673ff;
        }
      }
    
      & button {
        width: 25%;
        color: #fff;
        border-radius: 5px;
        border: 1px solid #6799ff;
        background-color: #6799ff;
      }
    `;
    
    const Container = styled.div`
      max-width: 350px;
      min-height: 70vh;
      background-color: #fff;
      padding: 16px;
      margin: 20px auto;
      border-radius: 5px;
      border: 1px solid #ddd;
    `;
    
    const Title = styled.h1`
      color: #5587ed;
      text-align: center;
    `;
    
    const Line = styled.hr`
      margin: 16px 0px;
      border: 1px dotted #ddd;
    `;
    
    export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));

    1.AWS 3パケットへの配備

  • 43(簡易ストレージサービス)スイート:イメージまたはファイルを格納するストレージサービス.
  • 43のパケットを作成する設定した後、yarn build
  • を構築した後、構築フォルダ内のすべてのファイルをbuild bucketにアップロードします.
  • ドメイン接続

  • ルーティング53に管理領域を作成した後、名前サーバをガビアに登録する(4つの順序/最後.登録を減算)
    →レコード作成→マイドメインアドレス確認

  • 2.Firebaseへの配備


    1)Firebase管理
    // cli 설치
    yarn add global firebase-tools
    // firebase에 로그인
    yarn firebase login
    // init 실행
    yarn firebase init
    2)ホスト管理を選択した後、
    // 빌드한 결과물 올리기
    yarn firebase deploy
    3)Firebaseダッシュボード-管理中にドメインに入り、画面を確認します.

    ドメインを移動した後の認証結果画面



    修正する箇所:Spinner画面では自動スキップはできませんが、リフレッシュして次の画面に移動するには理由が分かりません.

    [HW] FriendTest Project

  • Spinner.js
  • import React from "react";
    import styled from "styled-components";
    
    import img from "./spinner.png";
    const Spinner = (props) => {
      return (
        <Outter>
          <img src={img} />
        </Outter>
      );
    };
    
    const Outter = styled.div`
      background-color: #df402c88;
      width: 100vw;
      height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      & img {
        width: 400px;
      }
    `;
    
    export default Spinner;
  • redux/modules/rank.js
  • import { firestore } from "../../firebase";
    
    const rank_db = firestore.collection("rank");
    
    // Actions
    const ADD_USER_NAME = "rank/ADD_USER_NAME";
    const ADD_USER_MESSAGE = "rank/ADD_USER_MESSAGE";
    const ADD_RANK = "rank/ADD_RANK";
    const GET_RANK = "rank/GET_RANK";
    
    const IS_LOADED = "rank/IS_LOADED";
    
    const initialState = {
      user_name: "",
      user_message: "",
      user_score: "",
      score_text: {
        60: "우린 친구! 앞으로도 더 친하게 지내요!",
        80: "우와! 우리는 엄청 가까운 사이!",
        100: "우린 둘도 없는 단짝! :)",
      },
    
      ranking: [
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
        { score: 40, name: "최수빈", message: "안녕 포뇨!" },
      ],
    
      is_loadad: false,
    };
    
    export const addUserName = (user_name) => {
      return { type: ADD_USER_NAME, user_name };
    };
    
    export const addUserMessage = (user_message) => {
      return { type: ADD_USER_MESSAGE, user_message };
    };
    
    export const addRank = (rank_info) => {
      return { type: ADD_RANK, rank_info };
    };
    
    export const getRank = (rank_list) => {
      return { type: GET_RANK, rank_list };
    };
    
    export const isLoaded = (loaded) => {
      return { type: IS_LOADED, loaded };
    };
    
    export const addRankFB = (rank_info) => {
      return function (dispatch) {
        dispatch(isLoaded(false));
    
        let rank_data = {
          message: rank_info.message,
          name: rank_info.name,
          score: rank_info.score,
        };
        rank_db.add(rank_data).then((doc) => {
          console.log(doc.id);
          rank_data = { ...rank_data, id: doc.id, current: true };
          dispatch(addRank(rank_data));
        });
      };
    };
    
    export const getRankFB = () => {
      return function (dispatch) {
        dispatch(isLoaded(false));
    
        rank_db.get().then((docs) => {
          let rank_data = [];
    
          docs.forEach((doc) => {
            console.log(doc.data());
    
            rank_data = [...rank_data, { id: doc.id, ...doc.data() }];
          });
    
          dispatch(getRank(rank_data));
          dispatch(isLoaded(true));
        });
      };
    };
    
    //Reducer
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        case "rank/ADD_USER_NAME": {
          return { ...state, user_name: action.user_name };
        }
    
        case "rank/ADD_USER_MESSAGE": {
          return { ...state, user_message: action.user_message };
        }
    
        case "rank/ADD_RANK": {
          return { ...state, ranking: [...state.ranking, action.rank_info] };
        }
    
        case "rank/GET_RANK": {
          let ranking_data = [...state.ranking];
    
          const rank_ids = state.ranking.map((r, idx) => {
            return r.id;
          });
    
          console.log(rank_ids);
    
          const rank_data_fb = action.rank_list.filter((r, idx) => {
            if (rank_ids.indexOf(r.id) === -1) {
              ranking_data = [...ranking_data, r];
            }
          });
    
          console.log(ranking_data);
    
          return { ...state, ranking: ranking_data };
        }
    
        case "rank/IS_LOADED": {
          return { ...state, is_loaded: action.loaded };
        }
    
        default:
          return state;
      }
    }
  • Ranking.js
  • import React from "react";
    import styled from "styled-components";
    
    import { useSelector, useDispatch } from "react-redux";
    import { resetAnswer } from "./redux/modules/quiz";
    import { getRankFB } from "./redux/modules/rank";
    import Spinner from "./Spinner";
    
    const Ranking = (props) => {
      const dispatch = useDispatch();
      const _ranking = useSelector((state) => state.rank.ranking);
      const is_loaded = useSelector((state) => state.rank.is_loaded);
    
      const user_rank = React.useRef(null);
    
      React.useEffect(() => {
        dispatch(getRankFB());
        if (!user_rank.current) {
          return;
        }
    
        window.scrollTo({
          top: user_rank.current.offsetTop,
          left: 0,
          behavior: "smooth",
        });
      }, []);
      const ranking = _ranking.sort((a, b) => {
        return b.score - a.score;
      });
    
      if (!is_loaded) {
        return <Spinner />;
      }
    
      return (
        <RankContainer>
          <Topbar>
            <p>
              <span>{ranking.length}</span>의 사람들 중 당신은?
            </p>
          </Topbar>
    
          <RankWrap>
            {ranking.map((r, idx) => {
              if (r.current) {
                return (
                  <RankItem key={idx} highlight={true} ref={user_rank}>
                    <RankNum>{idx + 1}</RankNum>
                    <RankUser>
                      <p>
                        <b>{r.name}</b>
                      </p>
                      <p>{r.message}</p>
                    </RankUser>
                  </RankItem>
                );
              }
    
              return (
                <RankItem key={idx}>
                  <RankNum>{idx + 1}</RankNum>
                  <RankUser>
                    <p>
                      <b>{r.name}</b>
                    </p>
                    <p>{r.message}</p>
                  </RankUser>
                </RankItem>
              );
            })}
          </RankWrap>
    
          <Button
            onClick={() => {
              dispatch(resetAnswer());
              window.location.href = "/";
            }}
          >
            다시 하기
          </Button>
        </RankContainer>
      );
    };
    
    const RankContainer = styled.div`
      width: 100%;
      padding-bottom: 100px;
    `;
    
    const Topbar = styled.div`
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      min-height: 50px;
      border-bottom: 1px solid #ddd;
      background-color: #fff;
      & > p {
        text-align: center;
      }
    
      & > p > span {
        border-radius: 30px;
        background-color: #fef5d4;
        font-weight: 600;
        padding: 4px 8px;
      }
    `;
    
    const RankWrap = styled.div`
      display: flex;
      flex-direction: column;
      width: 100%;
      margin-top: 58px;
    `;
    
    const RankItem = styled.div`
      width: 80vw;
      margin: 8px auto;
      display: flex;
      border-radius: 5px;
      border: 1px solid #ddd;
      padding: 8px 16px;
      align-items: center;
      background-color: ${(props) => (props.highlight ? "#ffd6aa" : "#ffffff")};
    `;
    
    const RankNum = styled.div`
      text-align: center;
      font-size: 2em;
      font-weight: 600;
      padding: 0px 16px 0px 0px;
      border-right: 1px solid #ddd;
    `;
    
    const RankUser = styled.div`
      padding: 8px 16px;
      text-align: left;
      & > p {
        &:first-child > b {
          border-bottom: 2px solid #212121;
        }
        margin: 0px 0px 8px 0px;
      }
    `;
    
    const Button = styled.button`
      position: fixed;
      bottom: 5vh;
      left: 0;
      padding: 8px 24px;
      background-color: ${(props) => (props.outlined ? "#ffffff" : "#dadafc")};
      border-radius: 30px;
      margin: 0px 10vw;
      border: 1px solid #dadafc;
      width: 80vw;
    `;
    
    export default Ranking;
  • モバイル結果画面

  • ❗修正するポイント:
    1)問題解決swifeにおける1回のswife挙動に2つの問題があるエラー
    2)rankデータ値をFirebaseに正しく関連付ける