20200521のReactに関する記事は6件です。

Reactで再レンダリングを抑えたリストのドラッグアンドドロップを実装する

React, ReactDOM, nanoid 以外の依存ライブラリZEROでお送りします。
こういうのができる

Image from Gyazo

repo: https://github.com/konbu310/react-dnd-impl
demo: https://konbu310.github.io/react-dnd-impl/index.html

src/Dnd.tsxがメインです。
まずは必要なrefなどを定義していきます。

Dnd.tsx(42行目あたり~)
  // リストアイテムの親コンポーネント。inserBeforeするために使う
  const containerRef = useRef<HTMLDivElement | null>(null);

  // 各リストアイテム。idをkeyにするこの方法を思いついたときは歓喜した。
  const itemsRef = useRef<{ [id: string]: HTMLDivElement | null }>({});

  // ドラッグ中のリストアイテム。
  const draggingElmId = useRef<string | null>(null);

  // Stateの複製。余計な再レンダリングを防ぐためにuseRefで保持しておく。
  const listClone = useRef<State>(list);

  // propsから新しいStateが渡されてきた時にrefを更新するため。
  useEffect(() => {
    listClone.current = list;
  }, [list]);

  // touchmoveでスクロールしてしまうのを防ぐ。最適解がわからない。
  useEffect(() => {
    if (!itemsRef.current || !listClone.current) return;
    listClone.current.map(
      item => {
        itemsRef.current[item.id]?.addEventListener("touchmove", ev =>
          ev.preventDefault()
        );
      },
      { passive: false }
    );
  }, [itemsRef]);

refの指定とかはこんなかんじ。

Dnd.tsx(157行目あたり~)
  return (
    <div
      className="container"
      // containerRef
      ref={containerRef}
      onDrop={preventDefaultEvent}
      onDragOver={preventDefaultEvent}
      onDragEnter={preventDefaultEvent}
    >
      {list.map(item => (
        <div
          key={item.id}
          className="list-item"
          // TouchEventで使うためのカスタムデータ属性。これも最適解がわからない。
          data-id={item.id}
          // itemsRef
          ref={elm => (itemsRef.current[item.id] = elm)}
          draggable
          // DragEventとTouchEventのハンドラたち
          onDragStart={onDrag.start(item.id)}
          onDragEnter={onDrag.enter(item.id)}
          onDragEnd={onDrag.end(item.id)}
          onDragOver={preventDefaultEvent}
          // onDropも指定しておかないとドラッグ終了時にドラッグ中の画像が初期位置に戻るようにアニメーションしちゃう
          // ちょっと言葉で説明するの無理ある
          onDrop={preventDefaultEvent}
          onTouchStart={onTouch.start(item.id)}
          onTouchMove={onTouch.move(item.id)}
          onTouchEnd={onTouch.end(item.id)}
        >
          {item.value}
        </div>
      ))}
    </div>
  );

DragEventのハンドラはこんなかんじ
TypeScript且つStrict Modeなのでif文がめちゃんこ多い

Dnd.tsx(63行目あたり~)
  // まとめてみたけどまとめないほうがいいかも
  const onDrag: DragHandlers = {

    // `onDragStart` ドラッグを開始した要素で発火
    start: id => ev => {
      // ドラッグ中の要素のidを保持しておく
      draggingElmId.current = id;
      // これは多分やっておいたほうがいい
      // 画像を差し替えたいときは`ev.dataTranser.setDragImage()`
      ev.dataTransfer.effectAllowed = "move";
      // 1frずらしてやると本来は指定できない`opacity: 0`とかも指定できる
      requestAnimationFrame(() => {
        itemsRef.current[id]?.classList.add("list-item-dragging");
      });
    },

    // `onDragEnter` draggable要素をドラッグ中のマウスカーソルが進入してきた時に進入された要素で発火
    enter: id => ev => {
      // 親要素
      const containerElm = containerRef.current;
      if (!draggingElmId.current || !containerElm) return;

      // ドラッグ中の要素
      const draggingElm = itemsRef.current[draggingElmId.current];
      // ドラッグ先の要素(マウスカーソルの下にある要素)
      const underElm = itemsRef.current[id];
      if (!underElm || !draggingElm || underElm === draggingElm) return;

      // ドラッグ中の要素のインデックス
      const draggingElmIndex = listClone.current.findIndex(
        item => item.id === draggingElmId.current
      );
      // 移動先の要素のインデックス
      const underElmIndex = listClone.current.findIndex(item => item.id === id);

      // インデックスの大小に応じてinsertBeforeを使用して並び替え
      if (draggingElmIndex === underElmIndex) {
        return;
      } else if (draggingElmIndex < underElmIndex) {
        containerElm.insertBefore(underElm, draggingElm);
      } else if (draggingElmIndex > underElmIndex) {
        containerElm.insertBefore(draggingElm, underElm);
      }

      // 配列のデータも移動させておく。`arrayMove`は`src/util.ts`で定義してるインデックスで配列の要素を移動させるための関数。
      listClone.current = arrayMove(
        listClone.current,
        draggingElmIndex,
        underElmIndex
      );
    },

    // `onDragEnd` ドラッグ終了時に発火
    end: id => ev => {
      // 必要に応じてclassNameを削除したりremoveAttributeしたり
      itemsRef.current[id]?.classList.remove("list-item-dragging");
      // 変更をstateに適用する
      setList(listClone.current);
    }
  };

TouchEventのハンドラも似たようなかんじ
touchmoveだけ少しトリッキー
あとスクロールを無理やり封じてしまったのでviewport外の要素にいけない(つらい)

Dnd.tsx(109行目あたり~)
  const onTouch: TouchHandlers = {
    // `ontouchstart` タッチを開始した要素で発火
    start: id => ev => {
      draggingElmId.current = id;
      requestAnimationFrame(() => {
        itemsRef.current[id]?.classList.add("list-item-dragging");
      });
    },

    // `ontouchmove` タッチした状態で指を動かした時に発火
    // ここがDragEventとの違い。`ontouchenter`ほしい...
    move: id => ev => {
      const containerElm = containerRef.current;
      const draggingElm = itemsRef.current[id];
      if (!containerElm || !draggingElm) return;

      // タッチしてるとこの座標から無理くりドラッグ先の要素を取得する
      const { clientX, clientY } = ev.targetTouches[0];
      const underElm = document.elementFromPoint(clientX, clientY);
      if (!underElm || draggingElm === underElm) return;

      // カスタムデータ属性からidを取得する
      // これ何かいい方法ないですかねぇ...
      const underElmId = underElm.getAttribute("data-id");
      if (!underElmId) return;

      const draggingElmIndex = listClone.current.findIndex(
        item => item.id === draggingElmId.current
      );
      const underElmIndex = listClone.current.findIndex(
        item => item.id === underElmId
      );
      if (draggingElmIndex === underElmIndex) {
        return;
      } else if (draggingElmIndex < underElmIndex) {
        containerElm?.insertBefore(underElm, draggingElm);
      } else if (draggingElmIndex > underElmIndex) {
        containerElm?.insertBefore(draggingElm, underElm);
      }

      listClone.current = arrayMove(
        listClone.current,
        draggingElmIndex,
        underElmIndex
      );
    },

    end: id => ev => {
      itemsRef.current[id]?.classList.remove("list-item-dragging");
      setList(listClone.current);
    }
  };

もっとスマートな方法ありそう。
次は仮想スクロールと合体させたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ページのURLごとにタブアイコンの色を変える(React)

ページのURLを取得する

props.history.location.pathname;

:point_up_tone1:Reactのhistoryにはページ遷移の履歴が入っている
上記のようにReactRouterのpropsからhistoryを取ってくることができる。

startsWith / includesWith

ある文字列が含まれている時はtrueを返す。

①const pagePath = this.props.history.location.pathname;
②const isActive = pagePath.startsWith(***)

①今いるページのURLをpagePathに渡す。
②pagePathに***が含まれていればtrueを返す

タブアイコンの色が変わるようにする

index.js
class FooterTabItem extends React.Component {
  render() {
    const pagePath = this.props.history.location.pathname;
    const isActive = pagePath.startsWith(this.props.link)
    //今いるページのURLにthis.pros.link(Footer.jsの変数linkで定義されてるもの)
  //が含まれているか?
    const Icon = this.props.icon
  //Footer.jsのiconから取得
    return (
      <Link to={this.props.link}>
        <Icon className={isActive ? styles.activeTab : styles.tab} />
        //isActiveがtrueだったらstyles.activeTab、falseだったらstyles.tabになる
     //→style.module.scssの記載している色になる
      </Link>
    )
  }
}
export default withRouter(FooterTabItem); 
Footer.js
import FooterTabItem from './index'

<FooterTabItem icon={なんかのアイコン} link={routes.home()} />
style.module.scss
.activeTab {
  color: red;
}

.tab {
  color: green;
} 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのルーティング メモ

Reactでページを追加する手順

1.routes/index.jsにルーティングの追加

export default {
  top: () => '/',
  signIn: () => '/sign_in',
  signUp: () => '/sign_up',
}

2.containers/以下にページようファイルを作成

3.routes/Router.jsにページをインポートしてルーティング作成

import React from 'react'
import { ConnectedRouter } from 'connected-react-router'
import { Route, Switch } from 'react-router-dom'
import routes from '../routes'
import requireAuth from '../components/requireAuth'
import { withGuestLayout, withMemberLayout } from '../components/Layouts/RouteWithLayout'
import TopPage from '../containers/TopPage'
import SignInPage from '../containers/SignInPage'
import SignUpPage from '../containers/SignUpPage'

export default ({ history }) => (
  <ConnectedRouter history={history}>
    <Switch>
      <Route exact path={routes.top()} render={withGuestLayout(TopPage)} />
      <Route exact path={routes.signIn()} render={withGuestLayout(SignInPage)} />
      <Route exact path={routes.signUp()} render={withGuestLayout(SignUpPage)} />
    </Switch>
  </ConnectedRouter>
)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsby + Firestoreでコメント欄機能を自作しようとしてドツボ…

Gatsby + Firestoreでコメント欄機能を自作しようとしてドツボ…

動的ページで動的なクエリをこさえてFirestoreからコメントデータを取得・書き込みできる機能を実装しようとした。ステートフックとEffectを使用するやり方に沿って →Gatsby+Firestoreでステートフックを利用したCRUD

なぜなのだ?

まあ以下はコメントデータをFirestoreからページごとに読み込むコードなのだが。

import React, { useState, useEffect } from "react"
import firebase from "../utils/firebase"
import { AuthProvider } from '../contexts/auth'
import  AddCommentForm from './addcommentform'// コメント書き込みフォームコンポーネント<AddCommentForm />
import { useLocation } from "@reach/router"

const useComments = (pathname) => { // pathnameは '/article/10'とかいったような
  /*
    useLocation()で記事URLパスから作ったpathnameをもらってくる
   'article/10' ← こんなような文字列
   そしてpathnameフィールドに格納する.
  */
  const { location } = useLocation() // /article/10
  const _location = location.replace(/\//g, '') // スラッシュを削除
  const pathname = _location // article10

  const [comments, setComments] = useState([]);

  useEffect(() => {
      const unsubscribe = firebase
      firebase
        .firestore()
        .collection("comments")
        .where("pathname", "==", pathname) // URLパスをドキュメントデータに登録してある
        .orderBy("createdAt")
        .onSnapshot(snapshot => {
          const data = []
          snapshot.forEach(d => data.push({ 
              id: d.id,
              ...d.data(),
            }));
            setComments(data)
        });
      return () => unsubscribe(); 
  }, []);
  return comments;
};

・・・以下略(renderするところ)・・・

表示ページのslug(例えば/articla/10というようなURLパス)をuseLocation()で変数pathnameに格納し、.where("pathname", "==", pathname)でフィルターしたデータを取ってくるようにしている。

どうもうまくいかない。

ページ内での移動だと問題ないが、ブラウザの戻るボタンで移動するとこんなエラー。
TypeError: unsubscribe is not a function

おそらくlifecycleとの関連だろうか(参考:useBackButton hook to handle back button behavior in React Native

コンテキストを使ってみたり、onSnapshopを使わずgetでとりにいくようにしてみたり、slugを親からpropsで渡してみたり…。いろいろやったが非常に困窮を極めた。Reactとは違うのだよReactとは!ということか…。
参考:GatsbyとWordPressを使用したウェブサイト構築の概要(高速で静的)より「ダイナミックコンテンツの組み込みがないGatsbyを使用すると、ダイナミックコンテンツの管理方法と配信方法を再考する必要があります。つまり、静的と動的のハイブリッドを作成しなければなりません」

プラグインを使ってみよう

Gatsbyはプラグインが豊富でFirestore用にもgatsby-source-firestoreというプラグインがある。
これでやってみよう。

インストールと設定方法は以下の通り。
参考:gatsby-source-firestore

そしてgatsby-config.jsは以下の通り。

// gatsby-config.js

・・・略・・・
{
        resolve: 'gatsby-source-firestore',
        options: {
          credential: require(`./credentials.json`),
          types: [
            {
              type: 'Comment',
              collection: 'comments',
              map: doc => ({
                name: doc.name,
                content: doc.content,
                createdAt: doc.createdAt,
                parentDid: doc.parentDid,
                path: doc.path,
                uid: doc.uid,              
              }),
            },
          ],
        },
      },
    ],
  }

コメント表示コンポーネントは以下の通り。

// components/commentlist.jsx

import React, { useState, useEffect } from "react"
//import firebase from "../utils/firebase"
import { graphql, StaticQuery } from "gatsby"
import { AuthProvider } from './auth'
import { useLocation } from "@reach/router"
import AddForm from './addform'

const CommentList = () => {
  const { pathname } = useLocation()
  const _pathname = pathname.replace(/\//g, '') // スラッシュを削除

  return (
    <>
    <AddForm />
      <table className="tg">
          <tbody>
          <tr>
              <th>名前</th>
              <th>コメント</th>
              <th>uid</th>
              <th>ドキュメントId</th>
              <th>parentDid</th>
              <th>path</th>
              <th>日時</th>
          </tr>
          </tbody>

          <StaticQuery 
                query={graphql`
                    query {
                        comments: allComment {
                            edges {
                                node {
                                    id
                                    name
                                    content
                                    path
                                    parentDid
                                    createdAt
                                    uid
                                }
                            }
                        }
                    }
            `}
            render={(data) => {
            const comment = data.comments.edges.find(n => {
                return n.node.path == _pathname
                });
                console.log('■comment from GraphQL ',comment)
            if (!comment) { return null; }
              return (
                <tbody key>
                <tr>
                    <td>{comment.node.name}</td>
                    <td>{comment.node.content}</td>
                    <td>{comment.node.uid}</td>
                    <td>{comment.node.id}</td>
                    <td>{comment.node.parentId}</td>
                    <td>{comment.node.path}</td>
                    <td>{comment.node.createdAt}</td>
                    <td>
                        <button>返信</button>
                    </td>
                </tr>
                </tbody>
              )}
            }
           />
      </table>
    </>
  )
}
export default CommentList

これでうまくいった。イケそうだ!

しかし残念なことにビルドエラー・・・

The behavior for Date objects stored in Firestore is going to change
AND YOUR APP MAY BREAK.
To hide this warning and ensure your app does not break, you need to 
add the following code to your app before calling any other Cloud 
Firestore methods:

  const firestore = new Firestore();
  const settings = {/* your settings... */ timestampsInSnapshots: 
  true};
  firestore.settings(settings);

With this change, timestamps stored in Cloud Firestore will be read 
back as Firebase Timestamp objects instead of as system Date objects. 
So you will also need to update code expecting a Date to instead 
expect a Timestamp...

これはFirestoreのタイムスタンプフィールドの仕様が変わったことが原因。自分でコード書いてれば直せるがプラグインを使ってる以上、プラグインに対応してもらわないとどうしようもない。issueあがってるがご主人は放置状態のようだ。

結局

Disqusという外部APIサービスを知った。
はるかに簡単そうで多機能なのでこれを使うことにしようと思う。
次回、Disqusの設置方法について。

不毛なドツボ作業ではあったがとても良い勉強になったかもしれない。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのkeyにはMath.random()が使いやすい

オブジェクトのID等をkeyとして渡していた時に、意図せず同じオブジェクトをレンダリングして重複したIDが渡ると、コンポーネントの更新等に不具合が起きるので、Math.random()使っておけばいいという結論に達しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Gatsby.js】ポートフォリオ作成_環境準備

Node.js をインストール

// nodeをインストール
brew install node

インストール後、バージョンチェック

// nodeのバージョンチェック
node -v
v12.16.3

Gatsby.js をインストール

// gatsby をインストール
npm i -g gatsby-cli

// gatsby のバージョンチェック
gatsby -v
Gatsby CLI version: 2.12.29

// プロジェクトの雛形を生成(TypeScript)
gatsby new gatsby_sample01 https://github.com/haysclark/gatsby-starter-typescript

スタータのひな形は以下にある。
https://www.gatsbyjs.org/starters/?v=2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む