TOP BLOG React|

【React】1週間企画でいいねボタン(Bellボタン)を作った

2020/03/19◉ 64 views

皆さんこんばんは!ちょめこです。
今回僕はtwitterの1週間チャレンジという企画に参加しました。
詳細はこちら
Webサービスを1週間で作るイベントを開催したい概要
僕は勉強途中であるReactを使ったいいねボタンを勉強がてら作ってみました。

作品がこちらになります。
Bell

githubはこちら

BELL


1週間企画のお題がHomeだった為いいねボタンを無理やりHomeに寄せて、ベルボタンというチャイムを押すボタンとしました!

何の変哲もないただのいいねボタンですw
機能ってほどでもないですが一応説明しとこうと思います。

ボタンを押すことでチャイムを鳴らします(音は出ません)

ピンポーン!

Bellを押す


そうするとカウントが1進み、1人目の呼び出しとなります。
いいねが1つく感じですね!
ボタンの方は押すことで凹むようになっています。

もう一度ボタンを押してみます。

ポチっ

Bell再度押す


そうすると1進んだカウントが再び0に戻る仕組みです!
いいねを解除的なやつですね!

そしてこれを1人1回だけ押せるようにしました。

その結果(あとで説明しますがバグで全然カウント増えなかった)

人数

今の所16人の方が押してくれました!
嬉しいね!!
本当はもっとカウント進んでたのですが、バグのせいでカウントが0になったり、−1になったりで初期化しました....とほほ

解説

簡単ではありますが解説をしていこうと思います。
もう一度言いますが、まだReactを勉強して1ヶ月ほどのくそ初心者なので、codeがおかしいかもしれません。
でもこれが今僕にできる知識なので、馬鹿にせず温かい目で見てください。

  • 構成
  • React
  • Redux
  • react-persist
  • firebase

構成はこれで作りました。
reduxは練習の為に使用
persistは誰でも1回だけカウントできるようにする為使用
firebaseはカウントの値をdatabaseに保存する為に使用
こんな感じで作っていきました。

まずアプリの作成
npx create-react-app my-app

次にLayoutの作成
metatag設定にhelmetを使いました
インストール
npm install --save react-helmet

components/Layout.js
import React, { Component } from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
import { Helmet } from 'react-helmet';

class Layout extends Component {
  render() {
    return (
      <div className='container'>
        <Helmet>
          <title>{this.props.title}</title>
          <meta charSet='uft-8' />
          <meta name='viewport'
            content='initial-scale=1.0, width=device-width' />
        </Helmet>
        <Header
          title={this.props.title}
        />
        {this.props.children}
        <Footer footer="copyright CHOMEKO."/>
      </div>
    );
  }
}

export default Layout;
components/Header.js
import React, { Component } from 'react';

class Header extends Component {
  header = {
    fontSize:"2rem",
    fontWeight:"bold",
    height: "100%"
  }
  h1 = {
    display: "inline-block",
    padding: "0px 5px 0px 5px",
    fontSize:"3.2rem",
    fontWeight:"bold",
    color: "#ff8c00",
    borderRadius: "13px",
    background: "#ffe100",
    boxShadow: "5px 5px 5px #e3c800,-4px -4px 5px #fffa00"
    }
  render() {
    return (
      <header style={this.header}>
        <h1 style={this.h1}>{this.props.title}</h1>
      </header>
    );
  }
}

export default Header;
components/Footer.js
import React, { Component } from 'react';

class Footer extends Component {
  footer = {
    margin: 'auto',
    position: 'fixed',
    bottom: "1.0rem",
    maxWidth: '1080px',
    width:"100%",
    hight: "100%",
  }
  text = {
    letterSpacing: ".1rem",
    fontSize:'1.2rem',
    display: "inline-block",
    padding: "0px 10px 0px 10px",
    borderRadius: "13px",
    background: "#ffe100",
    boxShadow: "5px 5px 5px #e3c800,-4px -4px 5px #fffa00"
  }
  render() {
    return (
      <footer style={this.footer}>
        <div style={this.text}>{this.props.footer}</div>
      </footer>
    );
  }
}

export default Footer;

ちなみにapp.cssにグローバルなcssを書いて、コンポーネントごとにlocalなcss-in-jsを書いてます。

グローバルなCSSって何だって感じだけど、この辺りのことです。
html{
  font-size: 62.5%;
  box-sizing: border-box;
}
body {
  text-align: center;
  margin:10px;
  padding:5px;
  color:rgb(0, 0, 0);
  background: #ffe100;
}

よーするに全体的なものって感じですw
今回1週間チャンレンジなんでこの辺りは本当バラバラになってます。
remだったりpxだったり...焦ってたんで許して...

まーレイアウトは特に解説ないんでこんな感じで!

その後は色々インストールして最終的なcodeの解説
Reduxの導入
インストール
npm install --save redux
npm install --save react-redux
npm install --save redux-thunk

Redux-persistの導入
npm install --save redux-persist

firebaseのインストール
npm install --save firebase

index.jsの編集
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { PersistGate } from 'redux-persist/integration/react';
import './index.css';
import App from './App';
import FireStore, {fireReducer} from './fire/Store';

const persistConfig = {
  key: 'root',
  storage,
  blacklist: []
};

const persisteReducer = persistReducer(persistConfig, fireReducer);

let store = createStore(persisteReducer);
let pstore = persistStore(store);

ReactDOM.render(
  <Provider store={store}>
    <PersistGate loading={null}
      persistor={pstore}>
      <App />
    </PersistGate>
  </Provider>,
  document.getElementById('root')
);

storeの作成
import { createStore } from "redux";
import firebase from "firebase";

//ステートの初期値
const initial = {
  count: 1,
  push: true,
  message: "ベルを押せい!",
};
var firebaseConfig = {
  apiKey: " ",
  authDomain: " ",
  databaseURL: " ",
  projectId: " ",
  storageBucket: " ",
  messagingSenderId: " ",
  appId: " ",
  measurementId: " "
};

firebase.initializeApp(firebaseConfig);

//レデューサー
//送信された値でストアの内容を更新するもの
export function fireReducer(state = initial, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: 1,
        push: true,
        message: "押して帰ろう"
      };
    case 'DECREMENT':
      return {
        count:  -1,
        push: false,
        message: "ピンポーン!"
      };
    default:
      return state;
  }
}

export default createStore(fireReducer);

初期値の説明
count: 1 最初に1をセットします。
push: true ボタンを押したか押してないか。
message: "ベルを押せい!" 最初のテキスト。

firebaseの初期化
これは各自firebaseからコピー

レデューサーの作成
INCREMENTの場合==ボタンを押した時
count: 1 1をセット
push: true ボタンを押した状態にする
message: "押して帰ろう" テキストを変更

DECREMENTの場合==ボタンを戻した時
count: 1 -1をセット
push: false ボタンを戻した状態にする
message: "ピンポーン!" テキストを変更

ちょっとややこしい感じになっちゃいました。

次に本体の作成
import React, { Component } from 'react';
import { connect } from "react-redux";
import firebase from "firebase";
import "firebase/storage";

class Bell extends Component{
  p = {
    fontSize:"2.0rem"
  }
  span = {
    fontSize: "3.6rem",
    fontWeight:"bold"
  }
  text = {
    fontSize: "5.0rem",
    fontWeight:"bold"
  }
  btn = {
    borderRadius: "14.8rem",
    background: "linear-gradient(145deg, #e6cb00, #fff100)",
    boxShadow:  "5px 5px 10px #c7b000,inset 5px 5px 10px #ffff00,inset -5px -5px 10px #c7b000",
    border: "none",
    width: "23rem",
    height: "23rem",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontWeight: "bold",
    fontSize:"2.0rem",
    margin: "auto",
  }

  btnPush = {
    borderRadius: "14.8rem",
    background: "linear-gradient(145deg, #e6cb00, #fff100)",
    boxShadow: "inset 8px 5px 10px #d9bf00,inset -7px -5px 60px #ffff00",
    border: "none",
    width: "23rem",
    height: "23rem",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontWeight: "bold",
    fontSize:"2.0rem",
    margin: "auto",
  }

  constructor(props) {
    super(props);
    this.state = {
      dbvalue: 0,
    }
    this.doAction = this.doAction.bind(this);
  }

  componentDidMount() {
    //dbから取得
    let db = firebase.database();
    let ref = db.ref('count');
    let self = this;
    ref.orderByKey()
      .equalTo("value")
      .on("value", (snapshot) => {
        let data = snapshot.val();
        console.log("data=", data);
        self.setState({
          dbvalue: data.value
        });
      });
  }


  doAction() {
    //クリックaction
    if (this.props.push === false) {
      this.props.dispatch({ type: 'INCREMENT' });
      console.log(this.props.count);
      console.log(this.props.push);
      console.log(this.props.message);
    } else {
      this.props.dispatch({ type: 'DECREMENT' });
      console.log(this.props.count);
      console.log(this.props.push);
      console.log(this.props.message);
    }
    //dbに追加
    let db = firebase.database();
    let ref = db.ref('count');
    ref.set({ value: this.state.dbvalue + this.props.count });
  }

  render() {
    return (
      <section className="top">
        <p style={this.p}><span style={this.span}>{this.state.dbvalue}</span>人目の呼び出し</p>
        <p style={this.text}>{this.props.message}</p>
        { this.props.push ?
          <button style={this.btn}
            onClick={this.doAction}
            disabled={this.state.dbvalue === 0}
          >
            呼び出す
          </button>
          :
          <button style={this.btnPush}
            onClick={this.doAction}
            disabled={this.state.dbvalue === 0}
          >
            呼び出す
          </button>
        }
      </section>
    );
  }
}


export default connect((state) => state)(Bell);

DBの値を表示するためのステートを設定
初期値は0とします。

this.state = {
  dbvalue: 0,
}
DBの値を取得してdbvalueにsetState
componentDidMount()
renderメソッドによるDOM挿入直後に呼ばれる。
let db = firebase.database();
firebaseのdatabaseを使う
let ref = db.ref('count');
databaseのcount/を参照する
ref.orderByKey()
  .equalTo("value")
count/valueからデータを取り出す
.on("value", (snapshot) => {
snapshotに取り出したデータをオブジェクトとしてまとめる
  let data = snapshot.val();
今回はDBにはvalue:値しかデータがないので変数に代入する
正直この辺あってるかわからないけど、このようにしてみました。
   self.setState({
     dbvalue: data.value
   });
 });
そしてそのデータをdbvalueにセットステートします
これでDBの値を使うことができるようになりました。

クリックした時のイベント
this.doAction = this.doAction.bind(this);
doAction()
if (this.props.push === false) {
もしボタンが押されていなかったら
      this.props.dispatch({ type: 'INCREMENT' });
レデューサーのINCREMENTをdispatchする
押した時のpropsはこちらになります
count: 1,
push: true,
message: "押して帰ろう"
    } else {
      this.props.dispatch({ type: 'DECREMENT' });
 }
それ以外はレデューサーのDECREMENTをdispatchする
押した時のpropsはこちらになります
count:  -1,
push: false,
message: "ピンポーン!"

//dbに追加
let db = firebase.database();
let ref = db.ref('count');
ref.set({ value: this.state.dbvalue + this.props.count });
databasecount/valueに値を保存する
this.state.dbvalue にはcomponentDidMount()でデーターベースから値を取り出したのが入ってます。現在のDBの値ですね。それにdispatchから受け取ったprops.countを足します。

ボタンを押す dbvalue(DBの値) + 1
ボタンを再度押 dbvalue(DBの値) ー 1

これでクリックすることでDBの値をカウントして、合計値をデーターベースに保存してます。

最後にrender return表示の部分では

<p>{this.state.dbvalue}人目の呼び出し</p>
これで現在のDBの値を表示させています。
<p>{this.props.message}</p>
ここではdispatchから受け取ったprops.messageを表示

次にボタンを押した時の見た目と、ボタンを戻した時の見た目を真偽値で切り替えるようにします。

{ this.props.push ?
props.pushがtrueならば
          <button style={this.btn}>
            呼び出す
          </button>
          :
props.pushがfalseならば
          <button style={this.btnPush}>
            呼び出す
          </button>
        }
これでボタンの見た目を切り替えています。

onClick={this.doAction}はクリックイベントの事です。
disabled={this.state.dbvalue === 0}はこれから説明します。

ハマった所

クリックしても1か-1しか入らないバグ
最初doAction内でステートのdbvalueを更新するcodeでやっていて、それだとベルを押すまで初期値のままになり、画面更新の度に0に戻ることで、dbvalueには1か-1しか入らなかった。

Twitterで声をかけてくれた方がいて、ご相談すると色々教えてくれてcomponentDidMount()の存在を知り、doActionから切り離してdbvalueの更新をできるようにしてくれました。
名前は出していいのかわからないので、出しませんが本当に助かりました。
ありがとうございました!!!

サイト読み込み直後にボタンのクリックでカウントがバグる
これはdbvalueがサイト読み込み直後に初期値の0が入ってる瞬間があります。
その瞬間にボタンをクリックすることで、カウントが0になったり−1になったりする。
よーするに初期値の0の状態でクリックするからdispatchからのprops.countが1か-1なので

ボタンを押す dbvalue(初期値0) + 1
ボタンを再度押 dbvalue(初期値0) ー 1

このようになり、結果データーベースの値が1か−1になって保存される

これのせいでカウントが最初全然増えなくて萎えました。
色々考えて試してみたのですが解決策がわからなくて、どうしても直すことができなかったので、今回一時凌ぎとしてこのような処置を取ってみました。
disabled={this.state.dbvalue === 0}
もう最悪ですよね...w
0の時はクリックできないようにしましたw
サイト読み込み時の初期値0の瞬間はボタン押させません!!!
これしかわからなかった...でもこれだとまだ誰もクリックしてない状態だとしたら0のままなので、永遠にクリックできません。
なので裏でこっそりDBに1を入れました。
あー最低だ!!!!!

誰か対処法教えてくださいお願いします。
そもそもこのソース自体やばいのかもしれない...凹

まとめ

1週間チャレンジでしたが、実際はプライベートが忙しくあまり時間が取れない中の挑戦でした。
わからない事だらけでまだReactも全然理解力が足りなくて、自分の知識不足に萎えました。
それでも1週間という期間内に作り終えて投稿することができ、助けてもらったりしながらも最後までやり通せたことに意味があるのかなと思います。
codeはまだまだくそですが、すごい勉強にもなって、本当にやってよかったなと思います。
次はReactでポートフォリオを作ります。
今回のいいねボタンもそこで使う予定なので、これが活かせそうです!
そしてもっともっと勉強していきます!

長くなりましたがこの辺で終わります!
全然まとまんないし何この記事って感じですがw

それでわ!

このブログサイトは初学者の僕が独学で
Ruby on Railsで作ったブログサイトです。 間違っている所もあるかもしれません。 あくまで参考程度にしていただけたらなと思います。 何かありましたらお問い合わせか、Twitter@ちょめこよりご連絡下さい。