- 投稿日:2020-05-21T23:24:33+09:00
Reactで再レンダリングを抑えたリストのドラッグアンドドロップを実装する
React
,ReactDOM
,nanoid
以外の依存ライブラリZEROでお送りします。
こういうのができる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); } };もっとスマートな方法ありそう。
次は仮想スクロールと合体させたい。
- 投稿日:2020-05-21T23:00:58+09:00
ページのURLごとにタブアイコンの色を変える(React)
ページのURLを取得する
props.history.location.pathname;Reactのhistoryにはページ遷移の履歴が入っている
上記のようにReactRouterのpropsからhistoryを取ってくることができる。startsWith / includesWith
ある文字列が含まれている時はtrueを返す。
①const pagePath = this.props.history.location.pathname; ②const isActive = pagePath.startsWith(***)①今いるページのURLをpagePathに渡す。
②pagePathに***が含まれていればtrueを返すタブアイコンの色が変わるようにする
index.jsclass 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.jsimport FooterTabItem from './index' <FooterTabItem icon={なんかのアイコン} link={routes.home()} />style.module.scss.activeTab { color: red; } .tab { color: green; }
- 投稿日:2020-05-21T22:07:07+09:00
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> )
- 投稿日:2020-05-21T16:16:48+09:00
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の設置方法について。不毛なドツボ作業ではあったがとても良い勉強になったかもしれない。
- 投稿日:2020-05-21T13:26:01+09:00
ReactのkeyにはMath.random()が使いやすい
オブジェクトのID等をkeyとして渡していた時に、意図せず同じオブジェクトをレンダリングして重複したIDが渡ると、コンポーネントの更新等に不具合が起きるので、Math.random()使っておけばいいという結論に達しました。
- 投稿日:2020-05-21T00:11:33+09:00
【Gatsby.js】ポートフォリオ作成_環境準備
Node.js をインストール
// nodeをインストール brew install nodeインストール後、バージョンチェック
// nodeのバージョンチェック node -v v12.16.3Gatsby.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