- 投稿日:2019-02-26T21:20:18+09:00
styled-componentとreact-transition-groupでお手軽にtransitionやanimationをかける
Reactでアニメーションをさせたい時、react-poseやreact-springなどのライブラリがある。これらライブラリはすごくリッチなのだがちょちょっとCSSのtransitionを使うぐらいの時には若干オーバーになる。
一方で過去公式だった(今はコミュニティ運用) react-transition-groupもある。こちらは非常にシンプルだ。
styled-componets
でさくっとアニメーションさせたい時、react-transition-group
の<Transition>
を使うと割とさくっとできることに気付いたのでまとめたい。例示
まずTransitionを利用する側を書く。
この例ではReact Hooksを使っているが別にそこは本筋ではないのでclass componentsでも問題無い。import { Transition } from "react-transition-group" import { Animation } from "./Animation" export const AnimateItem = () => { // animationの状態を持つのにstateが必要。ここだけは面倒だけどしょうがない const [animate, setAnimate] = useState(false) // ボタンを押したらtrueになって3000ms後にfalseになるように仕組む const doAnimate = useCallback(() => { setAnimate(true) setTimeout(() => { setAnimate(false) }, 3000) }, []) return ( <div> {/* Transition は `in` に渡した値で変化する */} <Transition in={animate} timeout={500}> {(state) => ( // stateは下記の順序で変遷する // exited -> entering -> entered -> exiting -> exited <Animation state={state}>Hello</Animation> )} </Transition> <button onClick={doAnimate}>Animate</button> </div> ) }次にanimationを設定するstyled-components側を作る。
propsのcallbackを受け取れるのでこれに応じて値を変えるだけだ。// Animation.js import styled from "styled-components" export const Animation = styled.div` transition: 0.5s; width: 300px; height: 200px; /* 要素を動かす */ transform: translateX( ${({ state }) => (state === "entering" || state === "entered" ? 400 : 0)}px ); /* 色を変える */ background: ${({ state }) => { switch (state) { case "entering": return "red" case "entered": return "blue" case "exiting": return "green" case "exited": return "yellow" } }}; `Animationの部分がちょっと分厚くなったり汚くなりがちなので、例えば基礎となる要素とanimation部分を分けて書くのもよいだろう
const BaseItem = styled.div` width: 300px; height: 200px; ` export const Animation = styled(BaseItem)` transition: 0.5s; transform: translateX( ${({ state }) => (state === "entering" || state === "entered" ? 400 : 0)}px ); `結果
例えばfadeIn / fadeOutするなら
この理屈で例えばアニメーションの前後で要素を消したいfadeInのようなものを作るならこんな感じだろう
export const Fade = styled.div` transition: 0.5s; opacity: ${({ state }) => (state === "entered" ? 1 : 0)}; display: ${({ state }) => (state === "exited" ? "none" : "block")}; `もしくは若干開始タイミングの制御が雑でもよければ
unmountOnExit
、mountOnEnter
を利用して下記のような方法もあるexport const Fade2 = styled.div` transition: 5s; opacity: ${({ state }) => (state === "entered" ? 1 : 0)}; ` const Item = () => { // ... return <Transition in={animate} timeout={500} unmountOnExit mountOnEnter> {(state) => <Fade2 state={state}>Fade In</Fade2>} </Transition> }
- 投稿日:2019-02-26T17:28:03+09:00
create-react-appしないでreactの環境構築してみる
環境
$ node -v v11.10.0 $ npm -v 6.8.0yarnを初期化
yarnはbrewでいれてます
$yarn initとりあえず全部Enter
question name (learn-js): question version (1.0.0): question description: question entry point (index.js): question repository url: question author: question license (MIT): question private:reactいれる
$yarn add react react-domwebpackいれる
$yarn add -D webpack webpack-cli webpack-dev-server html-webpack-pluginbabel入れる
$yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/registereslintのルールに従って、prettierでフォーマットする
$yarn add -D eslint $yarn add -D prettier eslint-plugin-prettier eslint-config-prettierindex.htmlつくる
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Learn JS</title> </head> <body> <div id="root"></div> </body> </html>src/index.jsをかく
src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; const title = 'Learn js'; ReactDOM.render( <div>{title}</div>, document.getElementById('root') );server立ち上げるコマンド書く
package.json"scripts": { "start": "webpack-dev-server --mode=development --open" }webpack.config.jsかく
webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin') const path = require('path') const src = path.resolve(__dirname, 'src') const dist = path.resolve(__dirname, 'dist') module.exports = { mode: 'development', entry: src + '/index.js', output: { path: dist, filename: 'bundle.js' }, module: { rules: [ { test: /\.jsx$/, exclude: /node_modules/, loader: 'babel-loader' } ] }, resolve: { extensions: ['.js', '.jsx'] }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') }) ] }.babelrcかく
{ "presets": [ "@babel/preset-env", "@babel/preset-react" ] }アプリケーションがうごきました!
- 投稿日:2019-02-26T17:28:03+09:00
[2019版]create-react-appしないでReactの環境構築してみるメモ
環境
$node -v v11.10.0 $npm -v 6.8.0 $yarn -v 1.13.0yarnを初期化
yarnはbrewでいれてます
$yarn initとりあえず全部Enter
question name (learn-js): question version (1.0.0): question description: question entry point (index.js): question repository url: question author: question license (MIT): question private:reactいれる
$yarn add react react-domwebpackいれる
$yarn add -D webpack webpack-cli webpack-dev-server html-webpack-pluginbabel入れる
$yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/registereslintのルールに従って、prettierでフォーマットする
$yarn add -D eslint $yarn add -D prettier eslint-plugin-prettier eslint-config-prettierindex.htmlつくる
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Learn JS</title> </head> <body> <div id="root"></div> </body> </html>src/index.jsをかく
src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; const title = 'Learn js'; ReactDOM.render( <div>{title}</div>, document.getElementById('root') );server立ち上げるコマンド書く
package.json"scripts": { "start": "webpack-dev-server --mode=development --open" }webpack.config.jsかく
webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin') const path = require('path') const src = path.resolve(__dirname, 'src') const dist = path.resolve(__dirname, 'dist') module.exports = { mode: 'development', entry: src + '/index.js', output: { path: dist, filename: 'bundle.js' }, module: { rules: [ { test: /\.jsx$/, exclude: /node_modules/, loader: 'babel-loader' } ] }, resolve: { extensions: ['.js', '.jsx'] }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') }) ] }.babelrcかく
{ "presets": [ "@babel/preset-env", "@babel/preset-react" ] }アプリケーションがうごきました!
- 投稿日:2019-02-26T17:28:03+09:00
[2019年版]create-react-appしないでReactの環境構築してみるメモ
環境
$node -v v11.10.0 $npm -v 6.8.0 $yarn -v 1.13.0yarnを初期化
yarnはbrewでいれてます
$yarn initとりあえず全部Enter
question name (learn-js): question version (1.0.0): question description: question entry point (index.js): question repository url: question author: question license (MIT): question private:reactいれる
$yarn add react react-domwebpackいれる
$yarn add -D webpack webpack-cli webpack-dev-server html-webpack-pluginbabel入れる
$yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/registereslintのルールに従って、prettierでフォーマットする
$yarn add -D eslint $yarn add -D prettier eslint-plugin-prettier eslint-config-prettierindex.htmlつくる
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Learn JS</title> </head> <body> <div id="root"></div> </body> </html>src/index.jsxをかく
src/index.jsximport React from 'react'; import ReactDOM from 'react-dom'; const title = 'Learn js'; ReactDOM.render( <div>{title}</div>, document.getElementById('root') );server立ち上げるコマンド書く
package.json"scripts": { "start": "webpack-dev-server --mode=development --open" }webpack.config.jsかく
webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin') const path = require('path') const src = path.resolve(__dirname, 'src') const dist = path.resolve(__dirname, 'dist') module.exports = { mode: 'development', entry: src + '/index.jsx', output: { path: dist, filename: 'bundle.js' }, module: { rules: [ { test: /\.jsx$/, exclude: /node_modules/, loader: 'babel-loader' } ] }, resolve: { extensions: ['.js', '.jsx'] }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') }) ] }.babelrcかく
{ "presets": [ "@babel/preset-env", "@babel/preset-react" ] }アプリケーションがうごきました!
- 投稿日:2019-02-26T13:27:33+09:00
【React知見】条件分岐(IF文)とそのネスト
背景
Reactを最近触り始めたのだが、
React(jsx)で条件分岐(if文)をネストする方法がよく分かってなかったのでまとめたifのネスト
便宜上if文で説明します。
ifの中にifがあることですね。if (A) { if (b) { } }正確には「if文にネストされたif文」とかかもしれんけど、
いざこうやって言語化すると難しい。要件
条件A、条件BといったBoolがあるとする
- Aかつ!BならばAと(ブラウザ上に)表示
- AかつBならばBと表示
- !AならばCと表示
この条件分岐を実現するにはJSXでどのような記法が使えるのか。
前提
JSXの条件分岐はこんな感じで表現できる
sample.jsxclass Sample extends React.Component { public render() { return ( <> {conditionA && ( <p>A</p> )} {!conditionA && ( <p>Aじゃない</p> )} </> ) } }もしくはシンプルにしたいので三項演算子で
sample.jsxclass Sample extends React.Component { public render() { return ( <> {conditionA ? ( <p>A</p> ) : ( <p>Aじゃない</p> )} </> ) } }よくある
if~else
みたいな構文は直接かけません(後述)記法
三項演算子入れ子パターン
こんな風に普通に入れ子にして書くのが多分一番綺麗なのかもしれません。
が、なぜかこんな風にできるのを最近まで知らなかった。sample.jsxclass Sample extends React.Component { public render() { return ( <> {conditionA ? (!conditionB ? (<p>A</p>) : (<p>B</p>) ) : (<p>C</p>) } </> ) } }ネストしないパターン
最初は結構こんな感じで書いてた。
ネストせずに普通に順番に判定していくパターンsample.jsxclass Sample extends React.Component { public render() { return ( <> {conditionA && !conditionB && ( <p>A</p> )} {conditionA && conditionB && ( <p>B</p> )} {!conditionA && ( <p>C</p> )} </> ) } }関数にしちゃうパターン
無名関数生成して、関数内部で判定させちゃうパターン。
処理自体は馴染みやすいけど、括弧多くて気持ち悪いsample.jsxclass Sample extends React.Component { public render() { return ( <> { (() => { if (conditionA) if (!conditionB) return <p>A</p> else return <p>B</p> else return <p>C</p> })() } </> ) } }雑感
雑だけど締めます。
色々書き方あるけど、混ぜると混乱するので、プロジェクト毎に統一しよう。(ああ!ネストしないのが一番見やすいッ!)
- 投稿日:2019-02-26T04:49:39+09:00
fly.io で React アプリケーションの SSR を行う
最近 fly.io という Edge Worker で遊んでいます。
Edge Worker, 地理的に分散された CDN の Edge Location でSSRを実行するので、レスポンスが高速です。
また、 ほぼフルスペックの node が動くので、 単にCDNのキャッシュ機能付きの node の PaaS として使うにも便利です。fly.io がどういうものか、どういう利点があるかについては、こちらの記事を参考にしてください
Edge Worker PaaS の fly.io が面白い - mizchi's blog
で、簡単な typescript + react + react-router + styled-components の SSR を行うアプリを書いてみたんですが、このコードの解説をしようと思います。
src/ 以下だけなら 284 行で、比較的コンパクトです。https://github.com/mizchi/flyio-playground/tree/master/examples/simple-ssr
自分の容量削減のために他のリポジトリと同居した monorepo になってますが、ルートディレクトリの package.json と examples/simple-ssr/package.json の deps/devDeps を合成すれば動くと思います。
そのまま動かすなら、次のような感じ
git clone https://github.com/mizchi/flyio-playground cd flyio-playground/examples/simple-ssr yarn install yarn dev # localhost:3000 で起動これで動くと思います。動かなかったら twitter の @mizchi まで
SSR用の React Application を書く
単に SSRが出来るかどうか確認するなら、すごく単純な
src/shared/App.tsximport React from 'react' export const App = () => <h1>Hello</h1>でもいいんですが、今回は react-router と styled-components を使った SSR までやりたいので、ちょっとしたアプリケーションを構築します。
redux を使わない代わりに、 React Hooks の
useReducer
を使った簡単な Flux を実装します。src/shared/App.tsximport React, { Dispatch, useContext, useReducer, useState, useCallback } from "react"; import { Route, Switch, Link } from "react-router-dom"; import styled, { createGlobalStyle } from "styled-components"; import { Action, reducer, RootState } from "./reducer"; import * as actions from "./reducer"; // RootState context export const RootContext = React.createContext<RootState>(null as any); export const DispatchContext = React.createContext<Dispatch<Action>>( null as any ); export function useRootState(): RootState { return useContext(RootContext); } export function useDispatch() { return useContext(DispatchContext); } export function App(props: RootState) { const [rootState, dispatch] = useReducer(reducer, props); return ( // ssr warning <RootContext.Provider value={rootState}> <DispatchContext.Provider value={dispatch}> <RootContainer suppressHydrationWarning={true}> <RootContent suppressHydrationWarning={true}> <GlobalStyle /> <Header {...props} /> <Switch> <Route exact path="/counter" component={Counter} /> <Route exact path="/" component={Index} /> </Switch> </RootContent> </RootContainer> </DispatchContext.Provider> </RootContext.Provider> ); } const GlobalStyle = createGlobalStyle` html, body, .root { padding: 0; margin: 0; } body { background-color: #eee; } * { box-sizing: border-box; } `; const RootContainer = styled.div` width: 100%; `; const RootContent = styled.div` padding: 10px; `; // Header export const HEADER_LINKS: { name: string; path: string }[] = [ { name: "Index", path: "/" }, { name: "Counter", path: "/counter" } ]; function Header(props: RootState) { return ( <header suppressHydrationWarning={true}> <h1>SSR on fly.io playground</h1> <div>generated {props.timestamp}</div> {HEADER_LINKS.map(link => { return ( <span key={link.path}> <Link to={link.path}>{link.name}</Link> | </span> ); })} </header> ); } // Index function Index() { return ( <div> <h1>Index</h1> </div> ); } // Counter function Counter() { const rootState = useRootState(); const dispatch = useDispatch(); const onClickIncrement = useCallback(() => dispatch(actions.increment()), []); const onClickDecrement = useCallback(() => dispatch(actions.decrement()), []); return ( <div> <h2>Counter</h2> <button onClick={onClickIncrement}>+1</button> <button onClick={onClickDecrement}>-1</button> <div>value: {rootState.page.counter.value}</div> </div> ); }大したことはしていません。普通のSSRです。何かあるとしたら、 react-router のBrowserRouter をこの時点ではまだ使っていません。
本記事では reducer のコードを省略しますが、 https://github.com/mizchi/flyio-playground/blob/master/examples/simple-ssr/src/shared/reducer.ts といった普通のCounterです。
Appをマウントするクライアントの実装
SSR なので、
ReactDOM.render
ではなくReactDOM.hydrate
で App を注入します。
初期状態はwindow.__initialState
で渡すとしましょう。src/client/index.tsximport React from "react"; import ReactDOM from "react-dom"; import { App } from "../shared/App"; import { BrowserRouter } from "react-router-dom"; //@ts-ignore const initialState = window.__initialState; const root = document.querySelector(".root") as HTMLElement; ReactDOM.hydrate( <BrowserRouter> <App {...initialState} /> </BrowserRouter>, root );ここまで React としては特に特殊なことをしていません。
fly.io 上でのSSRの実装
ここからが本番
src/edge-worker.tsimport { mount } from "@fly/fetch/mount"; import staticServer from "@fly/static"; import { ssr } from "./ssr"; const mounts = mount({ "/static/": staticServer({ root: "/" }), "/": ssr }); fly.http.respondWith(mounts);
@fly/fetch/mount
は指定したルート要素を指定したモジュールでマウントします。今回は/static
で静的なアセットの配信として@fly/static
でマウントし、/
以下で SSR を行います。
@fly/*
のモジュールは fly.io によって注入されるので、 npm/yarn install する必要はありません。この ssr の実装は、次のようになっています。
src/edge-worker/ssr.tsximport url from "url"; import { getInitialState } from "../shared/reducer"; import { ServerStyleSheet } from "styled-components"; import { RootState } from "../shared/reducer"; import React from "react"; import { StaticRouter } from "react-router"; import { App } from "../shared/App"; import ReactDOMServer from "react-dom/server"; export const ssr = async (req: Request, _init: any) => { const pathname = url.parse(req.url).pathname as string; const initialState = getInitialState(pathname, Date.now()); const html = renderApp(initialState); return new Response(html); }; function renderApp(state: RootState) { const sheet = new ServerStyleSheet(); const element = sheet.collectStyles( <StaticRouter location={state.url}> <App {...state} /> </StaticRouter> ); const html = ReactDOMServer.renderToString(element); const styleTags = sheet.getStyleTags(); const serializedState = JSON.stringify(state).replace(/</g, "\\u003c"); return `<!DOCTYPE html> <html lang="en-US"> <head> <title>flyio-example</title> <meta charset="utf-8"/> ${styleTags} </head> <body> <div class="root">${html}</div> <!-- SSR --> <script>window.__initialState = ${serializedState};</script> <script src="/static/bundle.js"></script> </body> </html> `; };
renderApp
関数が React のSSRの実装です。
- ReactRouter の StaticRouter を pathname 付きで実行
- reducer の初期状態を構築元に、
App
コンポーネントを初期化- styled-components でラップして
ReactDOMServer.renderToString()
を実行して、 SSR で注入する初期CSSを収集- クライアントに状態を引き継ぐため、
window.__initialState = ...
に初期状態を注入この関数は、単に html の文字列を組み立てて終わりです。
fly が実行する ssr 関数は、この文字列を HTML のレスポンスとして返却します。
return new Response(html);
ですね。静的アセットの配信
↑のHTMLの
<script src="/static/bundle.js"></script>
と edge-worker/index の"/static/": staticServer({ root: "/" }),
から、/static/bundel.js
というファイルを生成することを期待しているのがわかると思いますが、このままではまだアセットの配信ができていません。先に用意した
src/client/index.tsx
を/static/bundle.js
にビルドします。webpack.config.client.jsmodule.exports = { ...require("../../webpack.shared.config"), entry: "./src/client/index", output: { path: __dirname + "/static", filename: "bundle.js" } };
webpack.shared.config
は単に ts-loader の設定が書いてあるだけです。使い回すので、別に切り出しています。
yarn webpack --config webpack.config.client.js
と叩くと/static/bundle.js
を生成するのがわかると思います。.fly.yml
つぎに、
.fly.yml
のfiles
フィールドでstatic
ディレクトリを静的アセットとして fly cdn に認識させるスコープを書きます。.fly.fmlapp: aquatic-dream-897 files: - static/**これで、
@fly/static
の がこれを認識して"/static/": staticServer({ root: "/" })
が static/bundle.js を読み取れるようになるはずです。今こうなってるはずです。
$ tree . -d 2 . ├── package.json ├── src │ ├── client │ ├── edge-worker │ └── shared └── static └── bundle.js.fly.yml のapp の部分は自分が fly.io で登録したアプリ名です。このアプリ名は自分が登録してるので、他の人には使えません。各自自分で取得してください。
fly アプリケーションは
webpack.cofig.fly.js
で entry に指定したファイルで起動します。webpack.cofig.fly.jsmodule.exports = { ...require("../../webpack.shared.config"), entry: "./src/edge-worker/index" };(これも ts-loader の設定を使いまわしています)
起動スクリプト
package.json
にnpm-run-all
のヘルパを使って、次のようなコマンドを書きます。"scripts": { "dev": "run-p dev:*", "dev:edge-server": "fly server", "dev:watch-client": "webpack --config webpack.client.config.js -w --mode development", "build": "run-s build:*", "build:client": "webpack --config webpack.client.config.js --mode production", "test": "jest", "deploy": "yarn build && fly deploy" },
yarn dev
を叩くと、flyの開発サーバーとwebpackのビルドサーバーが並列に起動します。fly コマンドがインストールされていれば、fly server
で開発用のサーバーが立ち上がってるわけですね。あとは適当に動作確認して、
yarn deploy
で本番環境にリリース。おわり。おわり
fly.io というより、実際にはSSRの統括 + HTML を fly.io で返す、という作例でした。
なぜこれらのSSRを fly.io でやるかというと、 Edge Worker でこの作業にやることに価値があるからです。今回はやっていませんが、 SSR したHTML を fly のストレージでキャッシュして、キャッシュがあればそれを返却、ということを、CDNのネットワーク上で高速に行えます。
Edge ロケーションで SSR した HTML をキャッシュして返却すると速い、というのがフロントエンドの真理の一つです。みなさんやっていきましょう。
- 投稿日:2019-02-26T03:42:09+09:00
Reactで作ったチャットのメッセージをFirebaseRealtimeDatabaseに追加、削除する
はじめに
React
とFirebase
を使用したチャットシステムを作成していました。
それまではFirebase
はログイン状態を管理するために使用していましたが、RealtimeDatabase
の操作を学びたかったため、メッセージの追加、削除も管理するようにしました。
その時に学んだことを記録します。この記事のコードでできること
削除ボタンを押した際、更新してください
というポップアップが表示されます。現在の状態
- URL: Reactチャット
- Github: kaibara/React-chat
実装
ファイル構造
今回のファイル構造はこのようになっています。
src ー App.js |ー components | |ー ChatMEssage.js | |ー ChatForm.js | |ー firebase |ー firebase.jsFirebaseRealtimeDatabase
まずは
Firebase
のRealtimeDatabase
に接続します。
アプリの名前の設定などができていない場合は、ReactにFirebaseでGoogleアカウントの情報を反映させるを参考にしてください。Firebase console
Firebase console
でアプリの名前が設定できたら、開発 -> Database
を選択してください。
最初にCloud Firestore
が表示されますが、それではなく、その下にあるRealtime Database
を選びましょう。
次に、
データベースを作成
を選択し、テストモード
を選んでください。
firebase init
ここまでできたら端末の操作に移ります。
$ firebase init // Database,Hostingを選択 // 基本的に全部Enterで可能、カスタマイズしたい場合は各自で変更
上のコマンドの処理が終わったら、作成された
database.rules.json
を確認してください。database.rules.json{ "rules": { ".read": "true", ".write": "true" } }このようになっていますか?
なっていなかったら、上のように変更してくださいコードから接続
最後に、ファイル、コードから
FirebaseRealtimeDatabae
に接続できるようにします。
行うことはfirebase
の設定が記載されたファイルにDatabase
に関連するコードを追加するだけです。
config
で定義しているコードはReactにFirebaseを使ったログイン機能を実装する
を参考に持ってきてください。firebase/firebase.jsimport firebase from 'firebase/app' import 'firebase/auth' import 'firebase/database' //追加するコード1つ目 const config = { apiKey: "***", authDomain: "***", databaseURL: "***", projectId: "***", storageBucket: "***", messagingSenderId: "***" } export const firebaseApp = firebase.initializeApp(config) export const firebaseDB = firebase.database() //追加するコード2つ目このようなファイルを作成することで
firebase
とRealtimeDatabase
を使用できるようになります。データの追加
続いて、データ、メッセージを
RealtimeDatabase
に追加できるようにします。
では、解説していきます。ChatForm.js
ここではユーザー名及びメッセージを入力するフォームを作成しています。
RealtimeDatabae
と直接の関わりはありません。
ここで入力された値がRealtimeDatabase
に渡されるため、src/App.js
で定義されたイベントと照らし合わせて確認してください。src/components/ChatForm.jsimport React,{Component} from 'react' import firebase from 'firebase/app' import {firebaseApp} from '../firebase/firebase' class ChatForm extends Component { render(){ return( <div id='Form'> <input name='user_name' onChange={this.props.onTextChange} placeholder='名前'/> <textarea name='text' onChange={this.props.onTextChange} placeholder='メッセージ'/> <button onClick={this.props.onButtonClick}>送信</button> </div> ) } } export default ChatFormChatMessage.js
ここでは今までのメッセージが表示されます。
メッセージのデータはsrc/App.js
からprops
を使用して渡されます。
RealtimeDatabase
のデータはここに表示されるとだけ覚えておいてください。ChatMessage.jsimport React,{Component} from 'react' class ChatMessage extends Component { render(){ return( <div> <p>{this.props.message.text}</p> <p>by {this.props.message.user_name}</p> </div> ) } } export default ChatMessageApp.js
このファイルが
RealtimeDatabase
と直接やり取りをします。
まずはコードをご覧ください。src/App.jsimport React, { Component } from 'react' import firebase from 'firebase/app' import { firebaseApp,firebaseDB } from './firebase/firebase' import ChatMessage from './components/ChatMessage' import ChatForm from './components/ChatForm' const messagesRef = firebaseDB.ref('messages') //RealtimeDatabaseの`messages`という要素にメッセージを保存する class App extends Component { constructor(props) { super(props) this.onTextChange = this.onTextChange.bind(this) this.onButtonClick = this.onButtonClick.bind(this) //このファイルで使用するイベントを明確化している this.state = { text : "", user_name: "", messages : [] } } componentWillMount() { messagesRef.on('child_added', (snapshot) => { const m = snapshot.val() let msgs = this.state.messages msgs.push({ 'text' : m.text, 'user_name' : m.user_name, }) this.setState({ messages : msgs }) }) } //下で解説 onTextChange(e) { // ChatForm.jsで入力された値がthis.state.user_nameとthis.state.textに格納される if(e.target.name == 'user_name') { this.setState({ "user_name": e.target.value }); } else if (e.target.name == 'text') { this.setState({ "text": e.target.value }); } } onButtonClick() { //ChatForm.jsの送信ボタンが押されたらthis.stateに格納された値をRealtimeDatabaseに送信する messagesRef.push({ "user_name" : this.state.user_name, "text" : this.state.text }) this.setState({"text": ""}) } render() { return ( <div> <div> <h2>メッセージログ</h2> {this.state.messages.map((m, i) => { return <ChatMessage key={i} message={m} /> })} //それぞれのメッセージ毎にiというキーを割り当てて、RealtimeDatabaseにある全てのメッセージを表示する </div> <ChatForm onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} /> //ChatForm.jsにここで定義した2つのイベントを渡す </div> ) } } export default App;eventなどReactの基本的な内容がわかっていればそこまで難しくはないでしょう。
注目していただきたいのはcomponentWillMount()
です。
ここで定義している内容によって、RealtimeDatabae
に送ったメッセージが即座に表示されるようになっています。では、コメントアウトで解説していきます。
App.jscomponentWillMount() { //このファイルのコードが読み込まれる前に行う messagesRef.on('child_added', (snapshot) => { //RealtimeDatabseにデータが追加されたら以下の操作を行う const m = snapshot.val() //追加されたデータをmとして扱う let msgs = this.state.messages //データが追加される前のthis.state.messegesをmsgsに入れる msgs.push({ 'text' : m.text, 'user_name' : m.user_name, }) //msgsに追加されたデータを追加する this.setState({ messages : msgs }) //最新のデータをこのコンポーネントで扱えるようmsgsをthis.state.messagesとして定義する }) }やっていることは
RealtimeDatabae
に追加されたメッセージをthis.state.messages
追加しているだけです。
わかりづらかったらconsole.log()
を使って、一つずつのコードでどのようなデータが扱われているのかを確認してみてください。データの削除
続いて、データを削除するコードを実装していきます。
ここで扱うのは、ChatMessage.js
とApp.js
ですApp.js
App.js
で変更するのはcomponentWillMount()
の中の一箇所だけです。src/App.jscomponentWillMount() { messagesRef.on('child_added', (snapshot) => { const m = snapshot.val() let msgs = this.state.messages console.log({msgs}) msgs.push({ 'text' : m.text, 'user_name' : m.user_name, 'key': snapshot.key }) this.setState({ messages : msgs }) console.log(this.state.messages) }) }データを削除する際に必要なのは、削除したいメッセージと
this.state.messages
で保存しているメッセージが一致するようにすることです。
RealtimeDatabase
にデータが追加される際、snapshot
によって一意のキーが割当られます。
そのキーをthis.state.messages
に保存することで、キーを呼び出せばそのデータを削除できるようになるのです。
実際に削除するのはChatMessage.js
で行います。ChatMessage.js
RealtimeDatabse
でデータの削除はremove()
だけで行なえます。
そのためには削除したいデータを保存している場所、キーを見つけ出すことが必要ですが、snapshot
のキーをthis.state.messages
に格納したことでメッセージ毎にデータを削除できるようにしました。src/componentsChatMessage.jsimport React,{Component} from 'react' import { firebaseDB } from '../firebase/firebase' const messagesRef = firebaseDB.ref('messages') class ChatMessage extends Component { constructor(props) { super(props) this.onRemoveClick = this.onRemoveClick.bind(this) } onRemoveClick(){ messagesRef.child(this.props.message.key).remove() //メッセージ毎にキーがあるためそれを取得して削除する alert('メッセージはページを更新した後に削除されます') } render(){ return( <div> <p>{this.props.message.text}</p> <p">by {this.props.message.user_name}</p> <button onClick={this.onRemoveClick}>削除</button> //削除ボタンの実装 </div> ) } } export default ChatMessageまとめ
今回はページから
RealtimeDatabase
へデータの追加、削除する方法をご紹介しました。
削除機能に関して、データを削除してもスグに表示が変更されないため、それが今後の課題となっています。
自身で発見すれば更新しますが、もし良い方法が見つかった際は、コメントなどで教えていただけると助かります。参考資料
React & Firebaseで簡単なChatアプリを作ってみた
ウェブでのデータの読み取りと書き込み - Firebase Documentation
ウェブ上でデータリストを操作する - Firebase Documentation
How to add remove data function of Firebase Realtime Database - stackoverflow
How to read props on event on React - stackoverflow
- 投稿日:2019-02-26T01:48:54+09:00
Redux 勉強会に参加してきた
first chapter -概要-
前回のReact勉強会に参加させて頂いた会の続きで、Reduxの会に参加させて頂いたので記憶が消えない今日のうちにメモ。φ(・・
chapter01 - Reduxとreact-redux
- Reduxとreact-reduxは別物、分けて考えたほうが整理できる
- Reduxは状態管理ツール。Reactと一緒に使用することも、他のビューライブラリと一緒に使用することもできる。
- react-reduxは、ReactでReduxを使うためのライブラリ。
chapter02 - なぜReactにReduxか -
- propsバケツリレーを解消できる
- どのコンポーネントからでもstateと更新するハンドラを読み込むことができる
chapter03 - Reduxの基礎 -
store
- 状態管理場所。Reduxではstoreで全ての状態管理を行う。
- reducerを引数にcreateStore()メソッドを実行しstoreを作る。
const store = redux.createStore(reducer);reducer
- storeを更新する際に使用する。storeは神聖なものなのでreducerという門番を通さずには更新できない。
- Reduxには変更は純粋な関数から行われるべきという原則がある。
- actionに応じて新しいstoreを返す関数
const reducer = (state=initialState, action) => { switch(action.type){ case: "COUNT_UP": return {...state, count: state.count + 1 }; default: // defaultを忘れずに書かないと、初期ロードでつまづきやすい return {...state, state }; } };action
- storeに対し、どのような変更をしてほしいか書かれたメッセージ。
- {type: string, payload?: mixed} 形式のただのオブジェクト
- typeは様々なアクションの識別子、payloadは更新してほしいデータ。
- store.dispach(action)でreducerを実行し、actionを元にstoreを更新できる。
store.dispach({type: "COUNT_UP"});chapter04 - react-reduxの基礎 -
provider(react-reduxが持つ機能)
- で囲ったコンポーネント配下は、reduxのstoreにアクセスできるようになる。
- ルートコンポーネントに記述すればおk
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );connect(react-reduxが持つ機能)
- コンポーネントとstoreを接続するための仕組み
connect(mapStateToProps, mapDispatchToProps)(Component);mapStateToProps(自作する)
- storeのどの値をコンポーネントで使用したいかを選択する機能。
// state => stateでもいいが、不必要な値は入れないほうがいい const mapStateToProps = state => ({ value: state.value })mapDispatchToProps(自作する)
- どのactionをdispatchさせるのか定義する
- 通常アクションの生成からdispatchの実行まで行う関数を定義し、propsに渡す
const mapDispatchToProps = dispatch => { dispatchAddValue: amount => dispatch(addValue(amount)) };actionCreator(自作する)
- actionを作る関数
- 呼ぶだけでactionを作るように自分で定義する
const checkUser = (userId) => { return { type: CHECK_USER, payload: id }; };Last chapter
感想
最初覚えられない!!!って思ったんだけど
手を動かすことで勝手になんとなく覚えてきました。
習うより慣れろですね。
でも、仕組みとか引数とか型とか
一応なんとなくでもどういうものかって
理解しないとこわいから理解する->覚える->手を動かして慣れる->復習する
っていうのが一番効率いいかなって思いました。