FirebaseとReactでページネーション

はじめに

受注のあったアプリをFirebase+React+Typescriptで開発中です。
ページネーションと書きましたが、実際は次へと前へのボタン二つで実装です。
(※そして少々回りくどいかもしれません!!申し訳ありません!!)
Firebaseのクエリでは本物ページネーションが難しいため、開発中のアプリはAlgoliaを導入しました。
Firebase+Algoliaの開発記事はまた書こうと思います。

流れ

今回使うメインのデータの型

interface USER {
  uid: string
  name: string
  createdAt: Date
  image: string
}

今回用意するState
Stateの上が関連する説明として書いてます。

// fetchしたデータ、その時表示させる件数分しかsetしません
const  [ fetchData,  setFetchData ] = useState<USER[]>([]) // ①

// fetchDataで取得したデータの最初のtimestamp、ページを戻る場合に使う
// ちなみにtimestamp(createdAt)は、Firebaseだと下記のデータが入る
/*
createdAt: {
  nanoseconds: 34000000
  seconds: 1635892516
}
*/
const  [ firstTime,  setFirstTime ] = useState<Date>() // ②

// fetchDataで取得したデータの最後のtimestamp,ページを進む場合に使う
const  [ lastTime,  setLastTime ] = useState<Date>() // ③

// 全てのデータの最初のID
const  [ firstId, setFirstId ] = useState('') // ④

// 全てのデータの最後のID
const  [ lastId, setLastId ] = useState('') // ⑤

1.対象のデータをfetchして①のfirstTimeへsetする。
2.fetchしたデータの最初と最後のtimestamp(createdAt)をstateへset、最初の値を②のfirstTime、最後の値を③lastTimeへsetする。
3.fetchしたデータの最初と最後のID(uid)をstateへset、最初の値を④のfirstId、最後の値を⑤lastIdへsetする。
4.fetchしたデータに2でsetした最初と最後IDがあれば、ページを戻ったり進められないよう処理する。
※今回はフロント部分で矢印のボタンを制御するようにしました。
firstIdもlastIdも合致していない場合、どちらの矢印も表示させる f:id:zare926:20211129084607p:plain fetchDataの最初のデータのIDがfirstIdと合致している場合、戻る矢印は表示させない f:id:zare926:20211129084600p:plain fetchDataの最後のデータのIDがlastIdと合致している場合、進む矢印は表示させない f:id:zare926:20211129084631p:plain

tsxで書くとこんな感じ

{ fetchData[0].uid != firstId && 戻る矢印 }

{ fetchData[fetchData.length - 1].uid !=  lastId && 進む矢印 }

5.次に進む場合は、2でsetした③lastTimeの次のリストからデータをfetchする。
この時FirebaseのメソッドでstartAfterを使う。
6.前に戻る場合は、2でsetした②firstTimeの手前のリストからデータをfetchする。
この時fetchするデータをlimitで制限している場合、進むとは違って、limitToLastというメソッドとendBeforeを掛け合わせて使う。

という感じです。
下準備が多く、わかりにくく申し訳ありません…
これいらないんじゃないの?とか別の方法があれば教えてもらえたら幸いです!

コード

ざっくりとまとめるとこんな感じです。
今回fetchするデータはcollectionのusersを9件ずつ取得することにしてます。
フロント部分は最小限ですし、CSSも当たってないのでデザインしてください。

Pagination.tsx

import React, { useEffect, useState } from 'react'
import { db } from '../../../firebase' // ディレクトリ適当です、アプリごと変わります

interface USER {
  uid: string
  name: string
  createdAt: Date
  image: string
}

const Pagination = () => {
  const usersRef = db.collection('users')

  const [fetchData, setFetchData] = useState<USER[]>([])
  const [firstTime, setFirstTime] = useState<Date>()
  const [lastTime, setLastTime] = useState<Date>()
  const [firstId, setFirstId] = useState('')
  const [lastId, setLastId] = useState('')

  useEffect(() => {
    fetchUsersData()
  }, [])

  //  setStateをまとめてます
  const setMethod = (dataList: USER[]) => {
    setFirstTime(dataList[dataList.length - 1].createdAt)
    setLastTime(dataList[0].createdAt)
    setFirstId(dataList[dataList.length - 1].uid)
    setLastId(dataList[0].uid)
  }

  // 一番最初の9件を取りに行く
  const fetchUsersData = () => {
    let dataList: USER[] = []
    usersRef
      .limit(9)
      .orderBy('createdAt', 'desc')
      .get()
      .then((snapshot) => {
        snapshot.forEach((doc) => {
          const data = doc.data()
          const newData = {
            uid: data.uid,
            name: data.name,
            createdAt: data.createdAt,
            image: data.image,
          }
          dataList.push(newData)
        })
        setFetchData(dataList)
        setMethod(dataList)
      })
  }

  //  次のページ分を取りに行く
  // startAfterが指定したcreatedAt以降のデータを取ってきてくれる
  const fetchNextUsersData = () => {
    let dataList: USER[] = []
    usersRef
      .limit(9)
      .orderBy('createdAt', 'desc')
      .startAfter(lastTime)
      .get()
      .then((snapshot) => {
        snapshot.forEach((doc) => {
          const data = doc.data()
          const newData = {
            uid: data.uid,
            name: data.name,
            createdAt: data.createdAt,
            image: data.image,
          }
          dataList.push(newData)
        })
        setFetchData(dataList)
        setMethod(dataList)
      })
  }

  //  前のページ分を取りに行く
  // endBeforeとlimitToLastで指定したcreatedAtより手前のデータを取ってきてくれる
  const fetchPrevUsersData = () => {
    let dataList: USER[] = []
    usersRef
      .limitToLast(9)
      .orderBy('createdAt', 'desc')
      .endBefore(firstTime)
      .get()
      .then((snapshot) => {
        snapshot.forEach((doc) => {
          const data = doc.data()
          const newData = {
            uid: data.uid,
            name: data.name,
            createdAt: data.createdAt,
            image: data.image,
          }
          dataList.push(newData)
        })
        setFetchData(dataList)
        setMethod(dataList)
      })
  }
  return (
    <>
      {fetchData.map((fd: USER) => (
        <div>
          <img src={fd.image} />
        </div>
      ))}

      <div className='prevBtn'>
        {fetchData[0].uid != firstId && <img src={左矢印} onClick={() => fetchPrevUsersData()} />}
      </div>
      <div className='nextBtn'>
        {fetchData[fetchData.length - 1].uid != lastId && <img src={右矢印} onClick={() => fetchNextUsersData()} />}
      </div>
    </>
  )
}

export default Pagination


これでうまく機能すると思います!

【Rails】アクセス数をカウントしたかったので、impressionistを導入

初めに

ブログを作成していたので、記事にアクセスした回数を確認したく、探してみたところimpressionistというgemを発見。
少しつまづいたのと、まだ仮実装ですが、見返せるように投稿しておきます。

開発環境

バージョン
Ruby 2.5.1
Rails 5.2.3
impressionist 1.6.1

impressionistのバージョンはRails5なら1.6.1でないとエラーが出るようです。

やり方

ざっくりとですが、カラムを作ったり、モデルにコードを書いたりします。
カウントしたい対象のテーブルにimpressions_countというカラムをintegerで作ります。

下記はあくまで例です。

class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :name
      t.integer :impressions_count, default: 0
 
      t.timestamps
    end
  end
end

gemをインストールしましょう。
gem

gem 'impressionist'

$ bundle install してimpressionistをgenerateします。
ターミナル

$ rails g impressionist
Running via Spring preloader in process 134476
      invoke  active_record
      create    db/migrate/20180804231718_create_impressions_table.rb
      create  config/initializers/impression.rb

マイグレーションは下記が出来上がります。
migration.rb

class CreateImpressionsTable < ActiveRecord::Migration[5.1]
  def self.up
    create_table :impressions, :force => true do |t|
      t.string :impressionable_type
      t.integer :impressionable_id
      t.integer :user_id
      t.string :controller_name
      t.string :action_name
      t.string :view_name
      t.string :request_hash
      t.string :ip_address
      t.string :session_hash
      t.text :message
      t.text :referrer
      t.text :params
      t.timestamps
    end
    add_index :impressions, [:impressionable_type, :message, :impressionable_id], :name => "impressionable_type_message_index", :unique => false, :length => {:message => 255 }
    add_index :impressions, [:impressionable_type, :impressionable_id, :request_hash], :name => "poly_request_index", :unique => false
    add_index :impressions, [:impressionable_type, :impressionable_id, :ip_address], :name => "poly_ip_index", :unique => false
    add_index :impressions, [:impressionable_type, :impressionable_id, :session_hash], :name => "poly_session_index", :unique => false
    add_index :impressions, [:controller_name,:action_name,:request_hash], :name => "controlleraction_request_index", :unique => false
    add_index :impressions, [:controller_name,:action_name,:ip_address], :name => "controlleraction_ip_index", :unique => false
    add_index :impressions, [:controller_name,:action_name,:session_hash], :name => "controlleraction_session_index", :unique => false
    add_index :impressions, [:impressionable_type, :impressionable_id, :params], :name => "poly_params_request_index", :unique => false, :length => {:params => 255 }
    add_index :impressions, :user_id
  end
 
  def self.down
    drop_table :impressions
  end
end

migrationしましょう。
ターミナル

$ rails db:migrate

モデルに:counter_cacheを設定

class User < ApplicationRecord
    is_impressionable counter_cache: true
end

コントローラーに以下設定

  def show
    @user = User.find(params[:id])
    impressionist(@user, nil, :unique => [:session_hash])
  end

今回はuserとしてますが、記事とかであればarticleでもいいですし、動画ならvideoとかですかね?
また、:unique => [:session_hash]は自分の場合一旦消しました。
おそらくこれが無いと、同一のユーザーがアクセスした分カウントされてしまうのだと思われます。
ですが、TypeError - can't quote Rack::Session::SessionIdといったエラーが出てしまい、一旦カウントができる状態で仮完了としました。
エラーの参考記事はこちらです。

ruby on rails - TypeError - can't quote Rack::Session::SessionId - Stack Overflow
あとはViewで呼び出せばOKです。

<%= @user.impressions_count %>

このあとランキングを作ろうと思ってます。

参考

https://remonote.jp/rails-impressionist-ranking

https://github.com/charlotte-ruby/impressionist

【Vue.js】いいねボタン作ってみる(練習)

はじめに

Vue.jsの練習で作ったいいねボタンを、覚えるがてら書き残します。

完成形

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Component</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>

  <div id="app">
    <p>Total Likes: {{ total }}</p>
    <like-component message="Like" @increment="incrementTotal"></like-component>
    <like-component message="Awesome" @increment="incrementTotal"></like-component>
    <like-component message="Great" @increment="incrementTotal"></like-component>
    <like-component @increment="incrementTotal"></like-component>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  <script src="js/main.js"></script>
</body>
</html>

Vue.js

(function() {
  'use strict';

  var likeComponent = Vue.extend({
    props: {
      message: {
        type: String,
        default: 'Like'
      }
    },
    data: function() {
      return {
        count: 0
      }
    },
    template: `<button @click="countUp">{{ message }}{{ count }}</button>`,
    methods: {
      countUp: function() {
        this.count++;
        this.$emit('increment');
      }
    }
  });

  var app = new Vue({
    el: '#app',
    components: {
      'like-component': likeComponent
    },
    data: {
      total:0
    },
    methods: {
      incrementTotal: function() {
        this.total++;
      }
    }
  });
})();

自分なりに解説

まず基本的なHTMLは飛ばしますが、完成系の

中がVue.jsで制御されている部分です。
今回はVue.jsのComponentを使っていいねボタンを作成しますので、以下のように記述していきます。
HTML

<div id="app">
  <like></like>
</div>

はHTMLにないタグです。
これからVue.jsで制御していきます。
Vue.js

(function() {
  'use strict';

  var like = Vue.extend({
    template: `<button>いいね</button>`
  });

  var app = new Vue({
    el: "#app",
    components: {
      'like': like ←このlikeはメソッドは上の変数likeを呼び出してる。
    }
  });
})();

Vue.extendとして、続けてtemplate:〜〜と書くことで、HTMLに作ったlikeのタグにが表示される。
次にいいねが押された回数カウントするように、カウントのメソッドを作る。
Vue.js

(function() {
  'use strict';

  var like = Vue.extend({
    data: function(){  ←② countについて
      return{
        count:0
      }
    },
    template: `<button @click="countUp">いいね{{ count }}</button>`, ←① @clickについて
    methods: {
      countUp: function(){
        this.count++;
      }
    }
  });

  var app = new Vue({
    el: "#app",
    components: {
      'like': like
    }
  });
})();

①@clickはイベントで、v-on:clickとも書けます。
よくあるクリックでイベント発火ってやつですね。
この時countの初期値が欲しいので、dataを作ってcountの数値を渡しますが、Component内はdataを関数で返す必要があります。

②関数とするためにfunction()とし、returnでcount:0を返してます。
あとはmethodsのcountUpに基づき、数字がクリックするごとに増えるといった流れです。

Propsを持たせる

現時点でHTMLのを増やせばボタンが増えてくれます。
その上で、いいね以外のボタンもComponentを使って増やす場合に、表示したい文字が変わるケースもあると思います。
変更方法も書いていきます。
HTML

<div id="app">
  <like message="いいね"></like>
  <like message="よくない" class="hoge"></like> ←こういうclassも設定できます。
  <like></like>
</div>

message="いいね"のことを、カスタム属性といいます。
続いてVue.jsで操作する方法。
Vue.js

(function() {
  'use strict';

  var like = Vue.extend({
  ①props: {
      message: {
        type: String,
        default: 'いいね'
      }
    },
    data: function(){
      return{
        count:0
      }
    },
    template: `<button @click="countUp">{{ message }}{{ count }}</button>`, ←②
    methods: {
      countUp: function(){
        this.count++;
      }
    }
  });

  var app = new Vue({
    el: "#app",
    components: {
      'like': like
    }
  });
})();

①HTMLでlike要素毎messageに割り振られた文字列に変えるためにpropsを設定。
今回の書き方はに何も文字が割り振られてなかった場合、いいねと設定する書き方です。
props: ['message']でも表示されます。
あとはtemplate内の「いいね」と設定していた文字列を{{ message }}に変えて、HTMLで設定されたmessageの文字列を表示するといった仕組みです。

とりあえず、一旦以上!個人アプリに取り入れるために、勉強中です〜

【Vue.js】e.preventDefaultの書き方

Vue.jsで簡単にかけるってことがわかったので、備忘録です。

$("要素").on("イベント",function(e){
    e.preventDefault();
});

使いまくりですよね。
e.preventDefault();が不明な方は、簡単に言うとイベント発生を、意図的に阻止とか妨害したりするものです。
ここがわかりやすかったです!

JavaScriptのpreventDefault()って難しくない?preventDefault()を使うための前提知識 - Qiita

Vue.jsの基本的な形(と思ってるだけかもしれませんが笑)

var app = new Vue({
  el: '#app',
  data: {
  },
  methods: {
    hoge: function(){
// ここにe.preventDefaultを書くこともできる。
    }
  }
});

これはjsのファイルです。
今の所こんな感じでとインプットしてます。
hoge:function(e)〜といつも通りかけるんですが、もっと楽に書けました。
HTML

<form v-on:submit.prevent="メソッド">
// もしくは
<form @submit.prevent="メソッド">

となります。
.preventの部分ですね。
submitした時に、イベントを阻止するってことになります。
ちなみにv-on = @です。

【Vue.js】v-forディレクティブ

はじめに

v-forがなぜ機能するか、初見でよくわからなかった。
理由としては

<ul id="example-1">
  <li v-for="item in items" :key="item.message">
    # ↑のclassかのように書かれてるのが個人的に癖がありすぎた。
    {{ item.message }}
  </li>
</ul>

勉強不足だとは思うが、" "に囲われる文でマッチさせるという発想がなかったです。

噛み砕いて書くと

HTML

<ul id="example-1">
  <li v-for="①item in ②items" :key="item.message">
    {{ ①item.message }}
  </li>
</ul>

Vue.js

var example1 = new Vue({
  el: '#example-1',
  data: {
    ②items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }

結果

・Foo
・Bar

①に②を入れるよ ってことでした。
messageは配列に入ったkeyと同じなので、割愛。

【Vue.js】始めます。

初めに

つい最近、Vue.jsを勉強するよう勧められたので、覚えたものをアウトプットするという名目でブログに投稿して行きます。
結構噛み砕いて書きます。笑

Vue.jsとは

ご存知かと思いますが、JavaScriptフレームワークです。
ページ遷移をしない、SPA(Single Page Application)を作成するのに適している。
仮想DOMを使う

使い方

通常のHTMLを書きます。
Vue.jsはappというidを利用する決まり事がある為、appというidを作る。
{{ message }}ここはVue.jsで表示したいものを書くと、それが表示される場所です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue.js lesson</title>
</head>
<body>
  <div id="app">
    {{ message }}
  </div>
</body>
</html>

次にscriptタグを作って、Vue.jsのサイトからVue.jsをインストールする。

インストール — Vue.js 今回は、上記URLから少ししたにスクロールしたCDNという項目のURLを利用します。
今こんな感じです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue.js lesson</title>
</head>
<body>
  <div id="app">
    {{ message }}
  </div>
// 追加したところ
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
  </script>
</body>
</html>

ここからVue.js書いていますが、今回は深く追求せず、形だけ残します。

<script>
    let app = new Vue({
        el: "#app",
        data: {
            message: "Hello World"
        }
    })
</script>

HTMLの{{ message }}とVue.js内で書いているdataの中の文字はリンクするので、同じ文字列であれば機能するようですね。
簡単ですが、ここまで。
時間がある時にドットインストールで触れてみます。
ちなみに今はYoutubeでお世話になってます。

参考

https://www.youtube.com/watch?v=cL3Al628mLE

CarrierWave+MiniMagickでリサイズ

開発環境

バージョン
Ruby 2.5.1
Rails 5.2.3
MiniMagick 4.10.1
CarrierWave 2.1.0

主なメソッド

・resize_to_fit
・resize_to_limit
・resize_to_fill

resize_to_fit

画像の縦と横を維持して比率をリサイズしてくれます。

image_uploader.rb(タイトルとして表示されないんだよなぁ…、以下コードは全てimage_uploaderです)

process resize_to_fit: [300, 200]
# process resize_to_fit: [width, height]


resize_to_limit

resize_to_fitと変わりませんが、対象の画像が指定されているサイズよりも小さい場合はリサイズされない。

process resize_to_limit: [300, 200]
# process resize_to_limit: [width, height]

さらに、余白ができた場合指定した色で塗りつぶしも可能。
らしいのですが、バージョンのせいなのか、うまくいきませんでした。
後日検証します。
どの記事を参照しても記述に間違いはなさそうなので、念のためやり方だけ載せておきます。

process resize_to_limit: [300, 200, "#ffffff", "Center"]
# process resize_to_limit: [width, height,余白の色,余白が出た場合の画像の位置]

resize_to_fill

上二つとは性質が違い、指定したサイズで画像を切り抜く
第三引数で場所を選択して、切り抜きます。

process resize_to_fill: [100, 100, "Center"]
# process resize_to_fill: [width, height, 切り抜きを行う位置]

投稿画像する前のプレビューと画像の比率を合わせるなど、奥が深いです。

参考にした記事

Rails gem MiniMagick を利用して画像ファイルをリサイズする - Qiita

CarrierWave+MiniMagickで使う、画像リサイズのメソッド - Qiita