20190226のReactに関する記事は8件です。

styled-componentとreact-transition-groupでお手軽にtransitionやanimationをかける

Reactでアニメーションをさせたい時、react-posereact-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
  );
`

結果

a.gif

例えばfadeIn / fadeOutするなら

この理屈で例えばアニメーションの前後で要素を消したいfadeInのようなものを作るならこんな感じだろう

export const Fade = styled.div`
  transition: 0.5s;
  opacity: ${({ state }) => (state === "entered" ? 1 : 0)};
  display: ${({ state }) => (state === "exited" ? "none" : "block")};
`

もしくは若干開始タイミングの制御が雑でもよければunmountOnExitmountOnEnterを利用して下記のような方法もある

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>
}

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

create-react-appしないでreactの環境構築してみる

環境

$ node -v
v11.10.0

$ npm -v
6.8.0

yarnを初期化

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-dom

webpackいれる

$yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin

babel入れる

$yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/register

eslintのルールに従って、prettierでフォーマットする

$yarn add -D eslint
$yarn add -D prettier eslint-plugin-prettier eslint-config-prettier

index.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.js
import 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.js
const 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"
    ]
}

アプリケーションがうごきました!

image.png

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

[2019版]create-react-appしないでReactの環境構築してみるメモ

環境

$node -v
v11.10.0

$npm -v
6.8.0

$yarn -v
1.13.0

yarnを初期化

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-dom

webpackいれる

$yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin

babel入れる

$yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/register

eslintのルールに従って、prettierでフォーマットする

$yarn add -D eslint
$yarn add -D prettier eslint-plugin-prettier eslint-config-prettier

index.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.js
import 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.js
const 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"
    ]
}

アプリケーションがうごきました!

image.png

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

[2019年版]create-react-appしないでReactの環境構築してみるメモ

環境

$node -v
v11.10.0

$npm -v
6.8.0

$yarn -v
1.13.0

yarnを初期化

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-dom

webpackいれる

$yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin

babel入れる

$yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader @babel/register

eslintのルールに従って、prettierでフォーマットする

$yarn add -D eslint
$yarn add -D prettier eslint-plugin-prettier eslint-config-prettier

index.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.jsx
import 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.js
const 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"
    ]
}

アプリケーションがうごきました!

image.png

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

【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.jsx
class Sample extends React.Component {
    public render() {
        return (
            <>
                {conditionA && (
                    <p>A</p>
                )}
                {!conditionA && (
                    <p>Aじゃない</p>
                )}
            </>
        )
    }
}

もしくはシンプルにしたいので三項演算子で

sample.jsx
class Sample extends React.Component {
    public render() {
        return (
            <>
                {conditionA ? (
                    <p>A</p>
                ) : (
                    <p>Aじゃない</p>
                )}                
            </>
        )
    }
}

よくあるif~else みたいな構文は直接かけません(後述)

記法

三項演算子入れ子パターン

こんな風に普通に入れ子にして書くのが多分一番綺麗なのかもしれません。
が、なぜかこんな風にできるのを最近まで知らなかった。

sample.jsx
class Sample extends React.Component {
    public render() {
        return (
            <>
                {conditionA ?
                    (!conditionB ?
                        (<p>A</p>) :
                        (<p>B</p>)
                    ) :
                    (<p>C</p>)
                }
            </>
        )
    }
}

ネストしないパターン

最初は結構こんな感じで書いてた。
ネストせずに普通に順番に判定していくパターン

sample.jsx
class Sample extends React.Component {
    public render() {
        return (
            <>
                {conditionA && !conditionB && (
                    <p>A</p>
                )}
                {conditionA && conditionB && (
                    <p>B</p>
                )}
                {!conditionA && (
                    <p>C</p>
                )}
            </>
        )
    }
}

関数にしちゃうパターン

無名関数生成して、関数内部で判定させちゃうパターン。
処理自体は馴染みやすいけど、括弧多くて気持ち悪い

sample.jsx
class 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>
                    })()
                }
            </>
        )
    }
}

雑感

雑だけど締めます。
色々書き方あるけど、混ぜると混乱するので、プロジェクト毎に統一しよう。

(ああ!ネストしないのが一番見やすいッ!)

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

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.tsx
import React from 'react'
export const App = () => <h1>Hello</h1>

でもいいんですが、今回は react-router と styled-components を使った SSR までやりたいので、ちょっとしたアプリケーションを構築します。

redux を使わない代わりに、 React Hooks の useReducer を使った簡単な Flux を実装します。

src/shared/App.tsx
import 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>
            |&nbsp;
          </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.tsx
import 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.ts
import { 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.tsx
import 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.js
module.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.ymlfiles フィールドで static ディレクトリを静的アセットとして fly cdn に認識させるスコープを書きます。

.fly.fml
app: 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.js
module.exports = {
  ...require("../../webpack.shared.config"),
  entry: "./src/edge-worker/index"
};

(これも ts-loader の設定を使いまわしています)

起動スクリプト

package.jsonnpm-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 をキャッシュして返却すると速い、というのがフロントエンドの真理の一つです。みなさんやっていきましょう。

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

Reactで作ったチャットのメッセージをFirebaseRealtimeDatabaseに追加、削除する

はじめに

ReactFirebaseを使用したチャットシステムを作成していました。
それまではFirebaseはログイン状態を管理するために使用していましたが、RealtimeDatabaseの操作を学びたかったため、メッセージの追加、削除も管理するようにしました。
その時に学んだことを記録します。

この記事のコードでできること


削除ボタンを押した際、更新してくださいというポップアップが表示されます。

現在の状態

実装

ファイル構造

今回のファイル構造はこのようになっています。

src ー App.js
   |ー components
   |      |ー ChatMEssage.js
   |      |ー ChatForm.js
   |
   |ー firebase
       |ー firebase.js

FirebaseRealtimeDatabase

まずはFirebaseRealtimeDatabaseに接続します。
アプリの名前の設定などができていない場合は、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.js
import 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つ目

このようなファイルを作成することでfirebaseRealtimeDatabaseを使用できるようになります。

データの追加

続いて、データ、メッセージをRealtimeDatabaseに追加できるようにします。
では、解説していきます。

ChatForm.js

ここではユーザー名及びメッセージを入力するフォームを作成しています。
RealtimeDatabaeと直接の関わりはありません。
ここで入力された値がRealtimeDatabaseに渡されるため、src/App.jsで定義されたイベントと照らし合わせて確認してください。

src/components/ChatForm.js
import 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 ChatForm

ChatMessage.js

ここでは今までのメッセージが表示されます。
メッセージのデータはsrc/App.jsからpropsを使用して渡されます。
RealtimeDatabaseのデータはここに表示されるとだけ覚えておいてください。

ChatMessage.js
import React,{Component} from 'react'

class ChatMessage  extends Component {
  render(){
    return(
      <div>
        <p>{this.props.message.text}</p>
        <p>by&nbsp;{this.props.message.user_name}</p>
      </div>
    )
  }
}

export default ChatMessage

App.js

このファイルがRealtimeDatabaseと直接やり取りをします。
まずはコードをご覧ください。

src/App.js
import 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.js
  componentWillMount() {
  //このファイルのコードが読み込まれる前に行う
      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.jsApp.jsです

App.js

App.jsで変更するのはcomponentWillMount()の中の一箇所だけです。

src/App.js
  componentWillMount() {
      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.js
import 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&nbsp;{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

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

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

感想
最初覚えられない!!!って思ったんだけど
手を動かすことで勝手になんとなく覚えてきました。
習うより慣れろですね。
でも、仕組みとか引数とか型とか
一応なんとなくでもどういうものかって
理解しないとこわいから

理解する->覚える->手を動かして慣れる->復習する

っていうのが一番効率いいかなって思いました。

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