20200823のReactに関する記事は9件です。

【面倒臭がりさん必見】たった1つのコマンドでRails + React環境構築

突然ですが、環境構築は好きですか?
...私は苦手です。
端が丸まってくっついちゃったサランラップぐらい苦手です。

恐らく、あなたがこの記事を開いたということは
少なからず環境構築に苦手意識を持っているのではないでしょうか。

「もっと気軽に環境構築できたらいいのにな...」

というあなたの心の声を受け
今回は『コマンド1つでRailsとReactの環境構築ができるシェルスクリプト』をつくりました。

たった1つのコマンドを叩くだけ!
あとは『Netflix』で好きな動画でも見ていれば
勝手に環境構築が終わっているという"夢の世界"がここにあります!

さらに!

環境構築終了後、すぐに開発が始められるよう
Rails側のトップページとReact側のエントリーポイントも自動で生成される完全親切設計版です!
Reactのコンポーネントが初めからゴリゴリ書ける状態になっています。

以下、本文

前提必須条件

Dockerがインストール済みであること

主にインストールされるもの

インストールされるもの バージョン
Ruby 2.6.3
Rails 6.0.3.2
Node.js 10.x
PostgreSQL 12.3
foreman(Gem) 0.87.2

インストールされるReact周りのパッケージ群

  • redux
  • react-redux
  • react-router-dom
  • redux-devtools-extension
  • redux-form
  • redux-thunk
  • axios
  • babel-plugin-root-import
  • redux-toolkit
  • material-ui

使い方の手順(この記事のメイン)

  1. 好きな名前でフォルダを作成する
  2. 1で作成したフォルダの中に『docker_rails_react.sh』という空ファイルを作成する
  3. 2で作成したファイルに下記『シェルスクリプト本体』を全文コピーして保存する
  4. ターミナルを起動
  5. 1で作成したフォルダに移動し、コマンド『bash docker_rails_react.sh』を実行する
  6. Netflixを見て時間を潰す(数分〜数十分間)
  7. 『Compiled successfully.』がターミナルに表示されたらhttp://localhost:3000/にアクセス

※macOS動作検証済み

シェルスクリプト本体

docker_rails_react.sh

#!/bin/bash

ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn

APP_NAME=$(basename `pwd`)
echo APP_NAME: $APP_NAME
UPPER_APP_NAME=`echo $APP_NAME | tr "[:lower:]" "[:upper:]"`
echo UPPER_APP_NAME: $UPPER_APP_NAME

# make Procfile.dev
cat <<'EOF' > Procfile.dev
web: bundle exec rails s -p 3000 -b '0.0.0.0'
webpacker: bin/webpack-dev-server
EOF

# make Dockerfile
cat <<EOF > Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
# install yarn
RUN apt-get update && apt-get install -y curl apt-transport-https wget && \\
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \\
apt-get update && apt-get install -y yarn
# install Node.js
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \\
apt-get install -y nodejs
RUN mkdir /$APP_NAME
WORKDIR /$APP_NAME
COPY Gemfile /$APP_NAME/Gemfile
COPY Gemfile.lock /$APP_NAME/Gemfile.lock
RUN bundle install
RUN bundle exec rails webpacker:install
RUN bundle exec rails webpacker:install:react
COPY . /$APP_NAME

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
EOF

# make Gemfile
cat <<'EOF' > Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6.0', '>= 6.0.3.2'
EOF

# make Gemfile.lock
touch Gemfile.lock

# make docker-compose.yml
cat <<EOF > docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec foreman start -f Procfile.dev"
    volumes:
      - .:/$APP_NAME
    ports:
      - "3000:3000"
      - "3035:3035"
    environment:
      ${UPPER_APP_NAME}_DB_HOST: db
      ${UPPER_APP_NAME}_DB_USERNAME: postgres
      ${UPPER_APP_NAME}_DB_PASSWORD: password
      ${UPPER_APP_NAME}_DEVELOPMENT_DB: ${APP_NAME}_development
      ${UPPER_APP_NAME}_TEST_DB: ${APP_NAME}_test
    depends_on:
      - db
EOF

# make entrypoint.sh
cat <<EOF > entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /$APP_NAME/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "\$@"
EOF

echo "docker-compose run web rails new . --force --no-deps -–skip-turbolinks --webpack=react --database=postgresql"
docker-compose run web rails new . --force --no-deps -–skip-turbolinks --webpack=react --database=postgresql

# fix config/database.yml
echo "fix config/database.yml"
rm -f config/database.yml
cat <<EOF > config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: <%= ENV['${UPPER_APP_NAME}_DB_HOST'] %>
  username: <%= ENV['${UPPER_APP_NAME}_DB_USERNAME'] %>
  password: <%= ENV['${UPPER_APP_NAME}_DB_PASSWORD'] %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: <%= ENV['${UPPER_APP_NAME}_DEVELOPMENT_DB'] %>

test:
  <<: *default
  database: <%= ENV['${UPPER_APP_NAME}_TEST_DB'] %>

production:
  <<: *default
  database: <%= ENV['${UPPER_APP_NAME}_DB'] %>
  username: <%= ENV['${UPPER_APP_NAME}_DB_USERNAME'] %>
  password: <%= ENV['${UPPER_APP_NAME}_DB_PASSWORD'] %>
EOF

# fix config/webpacker.yml
cat config/webpacker.yml | sed "s/host: localhost/host: 0.0.0.0/g" > __tmpfile__
cat __tmpfile__ > config/webpacker.yml
rm __tmpfile__

# fix config/routes.rb
rm -f config/routes.rb
cat <<'EOF' > config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root "top#show"
end
EOF

# make .babelrc
.babelrc
cat <<'EOF' > .babelrc
{
  "plugins": [
    [
      "babel-plugin-root-import",
      {
        "paths": [
          {
            "rootPathSuffix": "./app/javascript/src",
            "rootPathPrefix": "~/"
          },
        ]
      }
    ]
  ]
}
EOF

# fix config/webpack/environment.js
rm -f config/webpack/environment.js
cat <<'EOF' > config/webpack/environment.js
const { environment } = require('@rails/webpacker')
environment.splitChunks();

module.exports = environment
EOF

# make top_controller.rb
cat <<'EOF' > app/controllers/top_controller.rb
class TopController < ApplicationController
  def show
  end
end
EOF

# fix application.html.erb
rm -f app/views/layouts/application.html.erb
cat <<EOF > app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>$APP_NAME</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= javascript_pack_tag 'application' %>
  </head>
  <body style="margin: 0;">
    <div id="root">
      <%= yield %>
    </div>
  </body>
</html>
EOF

# make top.html.erb
mkdir -p app/views/top
cat <<'EOF' > app/views/top/show.html.erb
<%= javascript_packs_with_chunks_tag 'index' %>
EOF

# make entrypoint
cat <<'EOF' > app/javascript/packs/index.jsx
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import {
  Router,
  Route,
  Switch,
  IndexRoute,
  useLocation
} from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { composeWithDevTools } from 'redux-devtools-extension';

// reducer
import rootReducer from '~/reducers/';

// Component
import Top from '~/components/tops/';

const middleWares = [thunk];
const enhancer = process.env.NODE_ENV === 'development' ?
  composeWithDevTools(applyMiddleware(...middleWares)) : applyMiddleware(...middleWares);
const store = createStore(rootReducer, enhancer);
const customHistory = createBrowserHistory();

render(
  <Provider store={store}>
    <Router history={customHistory}>
      <Route render={({ location }) => (
        <Switch location={location}>
          <Route exact path='/' component={Top} />
        </Switch>
      )}/>
    </Router>
  </Provider>,
  document.getElementById('root')
)
EOF

# add foreman-gem
echo "gem 'foreman', '~> 0.87.2'" >> Gemfile

docker-compose run web bundle install

docker-compose build

echo "install package"
echo "docker-compose run web yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import @reduxjs/toolkit @material-ui/core"
docker-compose run web yarn cache clean
docker-compose run web yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import @reduxjs/toolkit @material-ui/core

echo "docker-compose run web rake db:create"
docker-compose run web rake db:create

# make styles/images dir
mkdir -p app/javascript/src/styles
mkdir -p app/javascript/src/images

# make component
mkdir -p app/javascript/src/components/tops
cat <<'EOF' > app/javascript/src/components/tops/index.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';

import { topInitial } from '~/modules/tops/';

const useStyles = makeStyles(theme => ({
  successed: {
    color: '#036ab5',
  },
}));

const Top = () => {

  const classes = useStyles();
  const dispatch = useDispatch();
  const topState = useSelector(state => state.top);

  React.useEffect(() => {
    dispatch(topInitial({ initial: true }));
  }, []);

  return (
    <>
      <Typography variant="h1" align="center">Hello Rails on React</Typography>
      { topState.initial &&
        <Typography variant="h5" align="center" className={ classes.successed }>SUCCESSED</Typography>
      }
    </>
  )
}
export default Top;
EOF

# make module
mkdir -p app/javascript/src/modules/tops
cat <<'EOF' > app/javascript/src/modules/tops/index.jsx
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  initial: false,
}

const top = createSlice({
  name: 'top',
  initialState,
  reducers: {
    topInitial(state, action) {
      const { initial } = action.payload;
      return {
        ...state,
        initial: initial,
      }
    }
  }
})
export const { topInitial } = top.actions
export default top.reducer;
EOF

# make reducer
mkdir -p app/javascript/src/reducers
cat <<'EOF' > app/javascript/src/reducers/index.jsx
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

import top from '~/modules/tops/';

export default combineReducers({
  form: formReducer,
  top,
});
EOF

echo "docker-compose up"
docker-compose up

初期生成ファイルの仕様(気になる人向け)

  • 状態管理はReduxを使用
  • ファイル構成はDucksパターンを使用
  • Redux Toolkitを使用
  • Redux Formを使用
  • Material-UIを使用
  • React関連のソースパスを『.babelrc』ファイルで管理
  • webpackにsplitChunksを使用
  • foremanを使用し、railsサーバーとwebpack-dev-serverを同一コンテナで起動
  • top_controllerのshowアクションをRootに設定
  • database.ymlの環境変数名にはフォルダ名が適用される
  • ページタイトルにはフォルダ名が適用される
  • TypeScript未対応

ディレクトリ構成(気になる人向け)

任意のフォルダ
├── app
│   ├── controllers
│   │   └── top_controller.rb
│   ├── javascript
│   │   ├── packs
│   │   │   ├── application.js
│   │   │   └── index.jsx
│   │   └── src
│   │       ├── components
│   │       │   └── tops
│   │       │       └── index.jsx
│   │       ├── images
│   │       ├── modules
│   │       │   └── tops
│   │       │       └── index.jsx
│   │       ├── reducers
│   │       │   └── index.jsx
│   │       └── styles
│   └── views
│       ├── layouts
│       │   └── application.html.erb
│       └── top
│           └── show.html.erb
├── config
│   ├── database.yml
│   ├── routes.rb
│   ├── webpack
│   │   └── environment.js
│   └── webpacker.yml
├── docker_rails_react.sh
├── docker-compose.yml
├── Dockerfile
├── entrypoint.sh
├── Gemfile
├── Gemfile.lock
├── package.json
├── Procfile.dev
└── yarn.lock

※最適化されていない(無駄な処理がある)のはご容赦ください

では、楽しいRails on React生活を!

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

とりあえずrecoil触ってみるまで

特にコメントなし

import React from "react";
import "./styles.css";
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue
} from "recoil";

const countState = atom({
  key: "countState", // unique ID (with respect to other atoms/selectors)
  default: 0 // default value (aka initial value)
});

const charCountState = selector({
  key: "charCountState", // unique ID (with respect to other atoms/selectors)
  get: ({ get }) => {
    const text = get(countState);

    return text * 10;
  }
});

export default function App() {
  console.log("111111111");
  return (
    <RecoilRoot>
      <div className="App">
        <Test />
        <Test2 />
      </div>
    </RecoilRoot>
  );
}

const Test = () => {
  const [count, setCount] = useRecoilState(countState);
  console.log("fffffffff");
  return (
    <div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        test
      </button>
      {count}
    </div>
  );
};

const Test2 = () => {
  console.log("88888888888");
  const count = useRecoilValue(charCountState);
  return (
    <div>
      <button>test2</button>
      {count}
    </div>
  );
};

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

ReactとFirebaseを使ってログインフォームを実装する③

ログイン機能を確認するため、先にEメールとパスワードでのログインを完了させます。その次にダイアログ表示まで実装します。

FirebaseでEメールとパスワードでのログイン設定

FIrebaseのコンソール画面の左側にあるAuthenticationをクリックします。ログイン方法からメール/パスワードをクリックして有効にするをオンにして保存しましょう。

react-firebase-login3-1-800x349.png
これだけでEメールとパスワードでのログインできます。簡単です。

ログインフォームをダイアログ表示にする

material-uiのダイアログを使用します。ダイアログを管理するステートを用意し、trueの時はダイアログ表示、falseで閉じます。

下記のURLを参照してください。
https://material-ui.com/components/dialogs/

NavBarを編集しましょう。Reduxのstoreからログイン情報を取得して、ログインとログアウト処理を切り替えています。LoginFormにformTextを渡すことでログインとサインアップも切り替えてます。

firebase.auth().signOut()でログアウトできます。 コメントを参照してください。

src/components/NavBar.jsx
import React, { useState, Fragment } from 'react';
import { connect } from 'react-redux';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';

import firebase from '../config/firebase';
import LoginForm from './LoginForm';

const useStyles = makeStyles(theme => ({
  root: {
    flexGrow: 1,
  },
  title: {
    flexGrow: 1,
  },
}));

function NavBar(props) {
  const classes = useStyles();
  // ダイアログ表示のステート
  const [daialogOpen, setDaialogOpen] = useState(false);
  // ログインとサインアップを切り替えるステート
  const [formText, setFormText] = useState('ログイン');

  const handleClickSignUpOpen = () => {
    // ダイアログ表示してformTextを新規登録にする
    setDaialogOpen(true);
    setFormText('新規登録');
  };
  const handleClickLoginOpen = () => {
    // ダイアログ表示してformTextをログインにする
    setDaialogOpen(true);
    setFormText('ログイン');
  };

  const handleClose = () => {
    // ダイアログを閉じる
    setDaialogOpen(false);
  };

  const logout = () => {
    // firebaseからのログアウト処理
    firebase
      .auth()
      .signOut()
      .then(() => {
        console.log('ログアウトしました');
      })
      .catch(error => {
        console.log(`ログアウト時にエラーが発生しました (${error})`);
      });
  };

  const onFormTextSignUp = () => {
    setFormText('新規登録');
  };

  const onFormTextLogin = () => {
    setFormText('ログイン');
  };

  const renderSwitchLoginOrSignUp = () => {
    // ログインフォームの下にログインとサインアップを切り替える処理
    if (formText === 'ログイン') {
      return (
        <div style={{ textAlign: 'center', margin: 20 }}>
          アカウントをお持ちでないですか?
          <span onClick={onFormTextSignUp} style={{ color: 'rgb(0, 112, 210)', cursor: 'pointer' }}>
            新規登録
          </span>
        </div>
      );
    } else {
      return (
        <div style={{ textAlign: 'center', margin: 20 }}>
          すでにアカウントをお持ちですか?
          <span onClick={onFormTextLogin} style={{ color: 'rgb(0, 112, 210)', cursor: 'pointer' }}>
            ログイン
          </span>
        </div>
      );
    }
  };

  const renderDialog = () => {
    // ログアウト時、ログインフォームをダイアログ表示するボタン配置
    return (
      <Fragment>
        <div>
          <Button color="inherit" onClick={handleClickLoginOpen}>
            ログイン
          </Button>
        </div>
        <div>
          <Button color="inherit" onClick={handleClickSignUpOpen}>
            無料登録
          </Button>
        </div>
        <Dialog open={daialogOpen} onClose={handleClose} fullWidth maxWidth={'sm'}>
          <DialogTitle className={classes.dialogTitle}>{formText} </DialogTitle>
          <DialogContent>
            <LoginForm formText={formText} />
            {renderSwitchLoginOrSignUp()}
          </DialogContent>
        </Dialog>
      </Fragment>
    );
  };

  const renderLogout = () => {
    // ログインしてたら表示、ログアウトボタン
    return (
      <Button
        color="inherit"
        onClick={() => {
          logout();
          handleClose();
        }}
      >
        ログアウト
      </Button>
    );
  };

  const renderAuth = () => {
    if (props.isLoggedIn) {
      // ログインしていたらログアウトボタン表示
      return renderLogout();
    } else {
      // ログアウトしていたらログインボタン表示
      return renderDialog();
    }
  };

  return (
    <AppBar position="static">
      <Toolbar>
        <Typography variant="h6" className={classes.title}>
          News
        </Typography>
        {renderAuth()}
      </Toolbar>
    </AppBar>
  );
}

const mapStateToProps = state => {
  return {
    isLoggedIn: state.auth.isLoggedIn,
  };
};

export default connect(mapStateToProps)(NavBar);

ログインフォームはダイアログで表示するのでLandingPage.jsxのLoginFormコンポーネントは削除しましょう。そしてログイン状態ならLoginedPageにリダイレクトするようにします。

src/components/LandingPage.jsx
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';

function LandingPage(props) {
  if (props.isLoggedIn) {
    return <Redirect to={'/logined'} />;
  } else {
    return <div>LandingPage</div>;
  }
}

const mapStateToProps = state => {
  return { isLoggedIn: state.auth.isLoggedIn };
};

export default connect(mapStateToProps)(LandingPage);

ここで挙動を確認してみましょう。ローカルサーバー起動します。

ヘッダーのログイン、新規登録ボタンでダイアログが表示されます。test@gmail.comなどの適当なEメールとパスワードで登録すると、ページが偏移してLoginedPageになります。

ログアウトをクリックしてLandingPageになっていたら正しく動作しています。

loginform.gif

FirebaseのコンソールでAuthenticationを確認するとユーザーが登録されています!

react-firebase-login3-2-800x366.png

おわり

お疲れさまでした!ほぼ完成です。あとはソーシャルログインを実装するためにfirebaseにそれぞれのプロバイダーapiキーなどを登録するだけです。

今回はダイアログの表示とログイン状態による表示の切り替えが主でした。material-uiを使えば簡単に実装できます。次回でログインフォームは完成です。

全4回

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

テキストエディタライブラリの比較検討

はじめに

以前、ノート共有型SNSの投稿フォーム実装についてご紹介という記事でreact-quillのことについて紹介させていただきました。
今回は、Reactに対応している複数のテキストエディタライブラリがあった中で、どのように比較検討しQuillを選定することになったのか書いていきたいと思います。

比較したテキストエディタライブラリ

比較項目

  • データ構造
    • 編集前と編集後の差分を保持出来る形式であるか
  • 利点
  • 懸念点

react-quill

データ構造

  • Deltaという独自のデータ構造
Delta形式
{
  ops: [
    { insert: 'Gandalf', attributes: { bold: true } },
    { insert: ' the ' },
    { insert: 'Grey', attributes: { color: '#cccccc' } }
  ]
}

利点

  • 日々メンテナンスが行われていて、開発が活発 2020/08/23時点
  • Toolbarのメニューは自由にカスタマイズ可能
  • 入力された情報を都度検知出来る
  • 選択された文字の色・背景色を変更することが可能
  • 写真・URLでビデオが入れられる
  • moduleを使うことでカスタマイズが容易

懸念点

  • 閲覧者モード(Read only mode)は提供されている機能として備わっていない
    • 閲覧者モードとは、プレビューのみで編集は出来ないモードを指します。Qiitaでいうプレビューモードです。
    • Toolbarの位置を変更しUIを調整する & 閲覧者モードでは不要のイベントハンドラを無効にすることで対応可能

Editor.js

データ構造

  • JSON形式

BlockInlineという概念があり、Block単位でオブジェクトが管理される。

JSON形式
{
   "time": 1550476186479,
   "blocks": [
      {
         "type": "header",
         "data": {
            "text": "Editor.js",
            "level": 2
         }
      },
      {
         "type": "paragraph",
         "data": {
            "text": "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration."
         }
      },
      {
         "type": "header",
         "data": {
            "text": "Key features",
            "level": 3
         }
      },
      {
         "type": "list",
         "data": {
            "style": "unordered",
            "items": [
               "It is a block-styled editor",
               "It returns clean data output in JSON",
               "Designed to be extendable and pluggable with a simple API"
            ]
         }
      }
   ],
   "version": "2.8.1"
}

太字やハイパーリンク,文字の色・背景色はInlineで定義され、HTMLタグで表現される。

JSON形式
{
    "type" : "list",
    "data" : {
    "style" : "unordered",
    "items" : [
    "Hey. <b>Meet</b> the new Editor. On this page you can see it in action — try to edit this text."
        ]
    }
}

対応形式

公式で用意されているスタイルは以下の通りです。自作したスタイルを設定することも可能。

Block

  • Header(h1, h2, ...)
  • Image
  • List
  • Link
  • Code
  • Quote
  • Delimiter
  • Table
  • Raw HTML

Inline

  • Bold
  • Italic
  • Link
  • InlineCode
  • Maker

利点

  • 選択文字列の色・背景色を変える機能が用意されている
  • ショートカットが定義可能

image.png

  • Blockから他のBlockへの変換ができる(h1->h2, text->List, etc...)

image.png

  • Blockの入れ替えが出来る

image.png

懸念点

  • 閲覧者モード(Read only mode)は実装中 2020/08/23時点
    • 現段階で実現するのであれば、Block, Inlineツールの閲覧者バージョンを実装する必要があると考えられる。
    • Blockの並び替え機能とBlockの新規追加を無効にできれば、閲覧者モードとして使えそうだが、本家のJSをオーバーライドする必要が生まれるかもしれない。私たちの結論としては、自分たちでカスタマイズするには実装コストが大きい。

MediumEditor

データ構造

  • HTML形式
    • 各要素は<p>タグで区切られる

利点

  • Mediumと同じようなデザインで、ボールド・斜体・アンダーバー・ハイパーリンク・h2タグ・h3タグ・引用の機能が導入出来る
  • ドキュメントが丁寧に書かれている
  • テーマを変えることが出来る

image.png

懸念点

  • Markdown記法に対応していない
  • 選択した文字の色・背景色の変更は対応していない
  • あくまでMediumと似せたデザインというだけで、機能は全くの別物。(例えば、マーカー機能が無いなど)

動作の様子

image.png

PELL

データ構造

  • HTML形式

利点

  • Markdown記法に対応していること
  • サイズが軽量であること

サイズ比較

ライブラリー サイズ(昇順)
pell 3.54kB
medium-editor 105.kB
trix 204kB
quill 205kB

懸念点

  • 開発が2年以上止まっている(2020/08/23時点)
  • 基本的に自分でカスタマイズする方式なので、実装量が多くなりやすい
  • 直っていない不具合がいくつか確認出来た

trix

データ構造

  • HTML形式

利点

  • ファイルアップロードが可能
    • アップロードAPI先も指定出来る
  • 日本語入力中でもちゃんとイベント発火する
  • メンションや値の埋め込みが可能

懸念点

  • 選択した文字の色・背景色を変更する機能が備わっていない

おわりに

データ構造・利点・懸念点をリストアップし、私たちのアプリに必要な機能要件を満たすことが出来るのか1つ1つ比較・検討することで適切な技術選定が行えますね?

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

SPAをStatic Web Appsにデプロイ、本番環境で使ってみた

MicrosoftのカンファレンスMS Build 2020でAzure Static Web Appsが発表され、同時期にちょうどSPA(Single Page Application)でウェブアプリケーションを開発中だったので、まだPreview中でしたが、GitHub Actionsとの連携もできそうだったので、ぜひプロダクションで使おう!と思い現在進行形で運用しています。
なので、私みたいにHTML、CSS、JavaScript等の静的コンテンツを本番環境で運用しようとしている方向けに「Static Web Appsは使えるのか?」という記事を書きたいと思います。

本番環境で動かしてみた所感

先に結論を言うと、Static Web Apps 十分使えます。様々気づきもあったので紹介していきます。まずは出来上がったものをお見せすると以下のサイトです。

こちらのサイト、留学希望者が大学を選ぶ際に、より具体的な情報を得て多くの人が留学に行ってほしい!って想いで作成しました。そうです、こちらのサイトのフロント側(静的コンテンツ部分)が丸っとStatic Web Appsに乗っかって今も現在進行形で本番環境で動いています。

Azure Static Web Appsとは

いわゆるSPA(Single Page Application)やらSSG(Static Site Generator)やらを含むHTML、CSS、JavaScript等の静的コンテンツを配信する新しいAzureサービスです。その他にもカスタムドメインやSSL証明書が無料で設定できたり、Azure Functionsと統合できたり(今回は使ってませんが)、その他もろもろ非機能要件にもしっかり対応してくれています。
また今回使いたい!と思わせてくれた機能のひとつで、ビルド環境としてGitHub Actionsが強制的に使われるように設定されているため、コンテンツを更新する度にビルドを走らせるJAMstack運用も可能となっています。

Azure Static Web Appsの公式サイトはこちら

今回の構成

インフラの運用、管理するのは絶対に嫌だ!ということで考え付く最高のPaaS(Platform as a Services)を採用(Static Web Appsに関してはまだPreviewだが)しました。これでやりたくないインフラの管理はなくなり、また将来の拡張性も考え(モバイルアプリも作るかもだから)フロントエンドとバックエンドを分けて構築しました。以下が構成図です。

image.png

  1. ウェブアプリケーションのフロントエンドはReactを使いTypeScriptで書いています(今回はこの部分の話しかしません
  2. ウェブアプリケーションのバックエンドは.NET Coreを使いC#で書いています
  3. データベースはSQL DBを使用しています

Static Web Appsにデプロイ完了するまでの手順(軽く)

実際のデプロイ完了する手順は以下の記事に良く書かれているのでそちらを参照していただければなと思います。
たぶん10分で試せる。Azure Static Web AppsにWebサイトをデプロイして独自ドメイン設定とFunctionsでAPI公開まで

一応簡単に手順だけは紹介します。
1. GitHubリポジトリを作成し、ソースコードをPush
2. Azureポータル上でStatic Web Appsを作成し、先ほど作ったGitHubのリポジトリを参照させる
3. GitHubのリポジトリ上にgitHub/workflowsのフォルダーが自動的に作成され、ビルド&デプロイの手順が書かれたymlファイルが自動生成される
4. カスタムドメイン購入&ポータルで設定

正直驚くほど簡単にデプロイが完了します。リアルに10分ぐらいで完了してしまいます。

Static Web Appsで運用してみて良かったと感じたところ

今のところ問題なく本番環境でも動いていて、Previewだがかなり良く出来ている印象。以下良かった点まとめています。

  • グローバルコンテンツディストリビューションを使用
  • Static Web Apps作成する際、自動的にビルド&デプロイの手順が書かれたymlファイルを定義してくれる
  • Static Web Appsのルーティング定義は routes.json で記述すれば問題なし
  • やっぱり無料は助かる

グローバルコンテンツディストリビューションを使用

全世界のApp Service内にデプロイ + Traffic Managerで近くのApp Serviceにルーティングしているらしく、高可用性はかなり期待できるかと。

以下が公式のサイトの説明です。
image.png

一応Static Web Appsを作成する際に、リージョン指定をしなくてはいけないのですが、こちらはAzure FunctionsにデプロイするAPIのみ、指定したリージョンにデプロイされるものなので、静的コンテンツのデプロイ先としてはどこのリージョンを指定しても良さそうです。ですので、どのリージョンを選択しようと、アクセスした地域の一番近いApp ServiceにTraffic Managerがルーティングしてくれる仕様となっています。

image.png

具体的な裏側が気になる方は、こちらのブログ記事を読まれるとスッキリすると思います。

App Service Static Web Apps の仕組みを探る(非公式)

Static Web Apps作成する際、自動的にビルド&デプロイの手順が書かれたymlファイルを定義してくれる

最初の説明にもありましたが、Static Web Appsはビルド環境としてGitHub Actionsが強制的に使われるように設定されているため、コンテンツを更新する度にビルドを走らせるJAMstack運用も可能となっています。以下が簡単な手順です。

Static Web Appsを作成する際に、GitHub アカウントと連携するよう聞いてきます。
image.png

認証が完了すると、どの組織、リポジトリ、ブランチとSyncするか聞いてくるので、予めPushしておいたGitHubのリポジトリとブランチを選択します。ただこれで「確認および作成」ではなくて、必ず「次:ビルド >」を選択してください。
image.png

ビルドの設定の中では、「アプリの成果物の場所」を設定します。こちらにはデプロイすべきファイル群が格納されるディレクトリを設定します。今回はTypeScript、ReactでHTML/JSファイルをビルドする場合は、buildといったディレクトリにビルド結果のファイル群を格納しますので、そのディレクトリ名を指定しておきます。リポジトリのルートディレクトリにindex.htmlファイルなどを格納している場合は、この項目は空のままで構いません。「アプリの成果物の場所」が設定できると「確認および作成」を押して、Static Web Appsを実際に作成していきます。
image.png

Static Web Apps自体は1~2分で完成しますが、同時にGitHub Actionsとの連携もされ、自動的にビルド&デプロイが実行されます(こちらは5分ぐらいで完了します)。GitHubアカウントを見れば、Actionsというタブが増えているのが分かると思います。
image.png

またymlファイルも自動的に生成されていることも分かります。
image.png

ビルド&デプロイが完了したら、あとはStatic Web Appsのリソースからサイトを閲覧しにいくだけです!
image.png

無事見れました!これでコンテンツを更新する度にビルドを走らせるJAMstack運用の完成となります。
image.png

Static Web Appsのルーティング定義は routes.json で記述すれば問題なし

今回、若干引っかかったのがこのルーティングの定義。実はAzureのサービスの中に、Static Web Appsと同じように静的コンテンツを配信できるBlob StorageのStatic website hostingというサービスが存在しています。静的コンテンツを配信するという観点では同じようなことができるのですが、Static Web Appsと同じように細かいルーティング設定ができません。「じゃあStatic Web Appsはどうやってルーティング定義するんだろう?」と疑問に思ったのですが、さすが新サービス、routes.json で記述してあげれば何の問題なく動くことが分かりました。

Static Web Appsのルーティングのドキュメントはこちら

Reactの場合は public フォルダー内にroutes.jsonを配置して、ビルド&デプロイすれば自動的に読み取ってルーティングをしてくれます。
image.png

これで、index.html 以外のPathでアクセスしたときも404のNot Foundにならなくて済みます。

まとめ

まだPreviewですが、このクオリティのものを無料で使えるのはやはり助かります。またGAされたとしても、Blob Storageより少し高くて、App Serviceより安い価格が期待できると思うので、今回の私のようにSPAを構築しようとしている方にとってはStatic Web Appsを選択肢のひとつに入れてもいいのではないかなと思います。

一度触ってみたいと思う方は、Microsoftが無償で提供している学習ポータルのMS Learnがあるのでそちらから試してみると良いと思います。
Azure Static Web Apps で Angular、React、Svelte、または Vue の JavaScript アプリと API を発行する

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

【React】【textarea】改行のみ、文字未入力の際に送信ボタンを無効化する

都内スタートアップでフロントエンドエンジニアとしてReactを書いております。
最近業務でチャット機能を実装する際、タイトルどおりの実装が必要だったのでメモ程度に残していきます。

全体イメージ

import React, {useState} from 'react';

const MessageForm = () => {
    const [message, setMessage] = useState('');
    const isExistTextInInput = !!message.match(/[^\n]/)
    const sendMessage = () => {
      if(!isExistTextInInput) return
      //inputの値を送信!!
    }
    return (
        <div>
            <textarea
             value={message}
             onChange={e => setMessage(e.target.velue)}
            />      
            <button 
              onClick={() => isExistTextInInput && sendMessage}
              className={isExistTextInInput ? 'btn' : 'btn--disable'}
            >
             送信
            </button>
        </div>   
    );
};

解説

きもは下記変数です

const isExistTextInInput = !!message.match(/[^\n]/)

String.prototype.match()

正規表現全体に一致したすべての結果を配列で返します
何もないとnullになります

!!でboolに型変換

配列ならfrue, nullならfalseが返ります

条件である[^\n]

  • ^は、〜以外という正規表現
  • \nは改行の正規表現

なので下記条件分岐になります。
改行以外の文字列がある => 配列が返る
文字列がない、改行しかない => nullが返る

まとめ

あとはisExistTextInInputを使ってボタンのメゾット発火を無効にしたり、クラスを付け替えるだけです
めちゃめちゃ簡単なのでお試しあれ

キーボード操作系の
エンターシフトで送信や、","でタグを追加だったりも実装したので後にまとめようかなと思います!

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

【React】デフォルト値もundefinedチェックもいらないcreateContext【Typescript】

Introduction

Typescriptによる型定義とContextによるState管理で、ReactのDX(Developer Experience)は劇的に進化しました。

しかし、素晴らしい型定義によって、謎に苦しめられることもあります。

Problem

createContextにはデフォルト値を渡す必要がありますが、往々にしてそのデフォルト値はundefinedです。

const AuthContext = React.createContext<authContextType | undefined>(undefined);

これの何が問題なのでしょうか?
それはContextを使用する際に致命的になります。

// どこかでuseContextして作られたuseAuth関数
const Auth = useAuth()!;

createContextのデフォルト値としてundefinedを指定したことで、Contextを使用する際にその値がundefinedでないかを”全ての場所で”逐一確認する必要があります。

根本的に、undefinedはアプリケーションにとって何の意味もなさない値です。
それを、わざわざ確認しなければいけないということは、バグやエラーの温床になります。

Solution

デフォルト値をundefinedとせず、未定義チェックを行えるcretaeContextのwrapperを作成して解決します。

function createCtx<ContextType>() {
  const ctx = React.createContext<ContextType | undefined>(undefined);
  function useCtx() {
    const c = React.useContext(ctx);
    if (!c) throw new Error("useCtx must be inside a Provider with a value");
    return c;
  }
  return [useCtx, ctx.Provider] as const;
}

今回の問題は、デフォルト値undefinedのContextに対して、useContextをしてしまうとでundefinedがuseContextに紛れ込んでしまうことでした。

そこで、wrapper関数に未定義チェック関数を定義し、useContextundefinedである場合はエラーをthrowします。

これで、wrapper関数createCtxで定義されたuseCtxは、<ContextType>で渡されるジェネリック型を返します。
無事、useContextからundefinedを駆逐しました。

Example

ユーザー管理、ログイン、ログアウトなどを管理するContextのExampleを作ってみました。

type User = {
  id: string;
  name: string;
};

type authContextType = {
  user: User | null;
  signIn: () => void;
  signUp: () => void;
  signOut: () => void;
};

function createCtx<ContextType>() {
  const ctx = React.createContext<ContextType | undefined>(undefined);
  function useCtx() {
    const c = React.useContext(ctx);
    if (!c) throw new Error("useCtx must be inside a Provider with a value");
    return c;
  }
  return [useCtx, ctx.Provider] as const;
}

const [useAuth, SetAuthProvider] = createCtx<authContextType>();

const AuthProvider: React.FC = (props) => {
  const auth = useAuthCtx();
  return <SetAuthProvider value={auth}>{props.children}</SetAuthProvider>;
};

const useAuthCtx = (): authContextType => {
  const [user, setUser] = React.useState<User | null>(null);
  const signIn = () => {
    // Some sign in action
  };
  const signUp = () => {
    // Some sign up action
  };
  const signOut = () => {
    // Some sign out action
  };

  return { user, signIn, signUp, signOut };
};

export const App = () => {
  const auth = useAuth();
  return (
    <AuthProvider>
      <button onClick={() => {auth.signIn()}}>ログイン</button>
      <button onClick={() => {auth.signUp()}}>サインアップ</button>
    </AuthProvider>
  );
};

Sample

【Qiita記事準備中】 React.createContextで管理するFirebase Authentication

Happy Hacking!!?

苦情受付中...✍️

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

React アプリに Power BI レポートやダッシュボードを埋め込む

本記事では、Microsoft からリリースされている React 用の Power BI クライアントについて見ていきます。

GitHub: powerbi-client-react

powerbi-client-react とは

React Component として powerbi-client がラップされたもので、簡単に React アプリケーションに Power BI を埋め込めるというものでした。これは便利。では早速スクラッチで作ってみましょう。

React アプリを作成

今回は Create React App というツールを使ってアプリを作成します。

1. npx で create-react-app を実行。テンプレートは自分の好みで typescript を指定。

npx create-react-app react-powerbi-embed --template typescript

2. 作成が完了したらフォルダに移動してアプリを一旦実行してアプリが起動する事を確認。

cd react-powerbi-embed
yarn start

image.png

3. powerbi-client-react を追加。

npm install powerbi-client-react

Power BI レポートの選定

埋め込むレポートを用意します。どのレポートでもいいのですが、ここでは既存のアプリにあるレポートを使ってみます。

1. Power BI より「アプリ」を選択し、「アプリの取得」ボタンをクリック。一覧より任意のアプリを選択。ここでは COVID-19 US Tracking Report を選択。
image.png

2. ワークスペースより追加されたレポートを確認。
image.png

3. アドレスより report の Id をコピーしておく。Id は url の reports 直後にある GUID。

アクセストークンの取得

Power BI の埋め込みに使えるアクセストークンは種類がいくつかあるため、この記事では説明しません。機会があれば別の記事で紹介しますが、今回は PowerShell を使って一時的に使えるものを取得します。

1. PowerShell を起動して Power BI 用モジュールを追加。

Install-Module MicrosoftPowerBIMgmt

2. Power BI に接続。ログインが求められるので認証情報を入力。

Connect-PowerBIServiceAccount

3. アクセストークンの取得。出力されたトークンの Bearer 以降をコピー。

Get-PowerBIAccessToken -AsString

トークンは一定期間で無効となるため、エラーが出るようになったらまた再発行してください。

Power BI レポートの埋め込み

GitHub ページにあるサンプルを使っていきます。

1. 作成した react アプリを Visual Studio Code で開き、App.tsx を選択。

2. import を追加。GitHub の例では powerbi-clien-react だけ追加しているが、powerbi-client も必要。

import { PowerBIEmbed } from 'powerbi-client-react';
import { models, Report} from 'powerbi-client';

3. 既存の header 要素の内容を削除して、代わりにサンプルを張り付け。PowerBIEmbed は powerbi-client-react が提供する React コンポーネント。

<PowerBIEmbed
    embedConfig = {{
        type: 'report',   // Supported types: report, dashboard, tile, visual and qna
        id: '<Report Id>',
        embedUrl: '<Embed Url>',
        accessToken: '<Access Token>',
        tokenType: models.TokenType.Embed,
        settings: {
            panes: {
                filters: {
                    expanded: false,
                    visible: false
                }
            },
            background: models.BackgroundType.Transparent,
        }
    }}

    eventHandlers = { 
        new Map([
            ['loaded', function () {console.log('Report loaded');}],
            ['rendered', function () {console.log('Report rendered');}],
            ['error', function (event) {console.log(event.detail);}]
        ])
    }

    cssClassName = { "report-style-class" }

    getEmbeddedComponent = { (embeddedReport) => {
        this.report = embeddedReport as Report;
    }}
/>

4. embedConfig の id と accessToken に取得した値を張り付け。また tokenType を models.TokenType.Aad を指定。

embedConfig = {{
    type: 'report',   // Supported types: report, dashboard, tile, visual and qna
    id: '<取得したレポート ID>',
    embedUrl: 'https://app.powerbi.com/reportEmbed',
    accessToken: '<取得したアクセストークン>',
    tokenType: models.TokenType.Aad,
    settings: {
        panes: {
            filters: {
                expanded: false,
                visible: false
              }
            },
            background: models.BackgroundType.Transparent,
        }
    }}

5. getEmbeddedComponent ではローカル変数であろう report に対して埋め込まれたレポートを取得しているが、React ではステート使う事が多いため、React の import を変更し useState を読み込み。

import React, { useState } from 'react';

6. function 直下にステート管理用のコードを追加。

const [report, setReport] = useState<Report>();

7. 指定した setReport を使うように getEmbeddedComponent を変更。

getEmbeddedComponent = { (embeddedReport) => {
  setReport(embeddedReport as Report);
}}

8. cssClassName に "report-style-class" が指定されているが、そのようなスタイルは存在しないため、App.css に以下を追加。

.report-style-class {
    height: 69vh;
    margin: 1% auto;
    width: 60%;
}

9. サンプルではレポートの背景が透明に指定されているが、見辛いため Default に変更。

background: models.BackgroundType.Default

アプリを確認

コードが追加されたら、自動更新されたアプリを確認してみましょう。レポートが埋め込まれている事が分かります。
image.png

イベントの取得

eventHandlers には各種イベントの処理を指定できます。サンプルではイベント発生時にコンソールにログを出力するようになっています。

eventHandlers = { 
  new Map([
    ['loaded', function () {console.log('Report loaded');}],
    ['rendered', function () {console.log('Report rendered');}],
    ['error', function (event) { if(event!=undefined) console.log(event.detail);}]
  ])
}

ブラウザで F12 キーを押下して、コンソールを確認してください。
image.png

ダッシュボードの埋め込み

Power BI の埋め込みはレポート以外にも、ダッシュボードやタイル、QnA も埋め込めます。ここではダッシュボードを試してみます。

1. Power BI で任意のダッシュボードを追加。ここではレポートから複数のタイルをピン止めしたダッシュボードを作成。
image.png

2. レポートと同様、アドレスから Id をコピー。

3. Visual Studio に戻り、embedConfig の以下項目を変更。

4. アプリを確認してダッシュボードが表示されていることを確認。
image.png

まとめ

今回は React 用の PowerBI クライアントを簡単に紹介しました。より詳細は実際のコードを見ていただくとともに、GitHub にあるデモアプリも使ってみてください。

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

React フック API の使い方メモ

何番煎じだって話ですが、タイトルの通りです。

公式サイトにも使い方はありますが、
表現が自分の中でよく飲み込めないので自分用にサンプルとわかりにくいところだけ書き起こしてみました。

useState

ここに関しては公式を読めば特に補足はいらないかなと思います。

UseStateSample.jsx
import React, { useState } from 'react'

const UseStateSample = () => {
  const [count, setCount] = useState(0)
  return (
    <>
      <h2>Use State Sample</h2>
      Count: {count}
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  )
}

export default UseStateSample;

useEffect

基本機能はレンダリングが行われるタイミングでここの処理を実行します。下記例ではcountの初期値を0に設定していますが、useEffect内で+1されているので1で画面に表示されます。

UseStateSample.jsx
import React, { useState, useEffect } from 'react'

const UseEffectSample = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setCount((count) => count + 1)
  }, []);

  return (
    <>
      Count: {count}      
    </>
  )
}

export default UseEffectSample;

重要なポイントとしては以下3点かなと。

セッタ(上記例でいうところのsetCount)はその時点での値を引数として受け取る

setCount((count) => count + 1)

上記のcountのことです。
例ではuseStateで0を入れているので初回は0が入ってきます。

第2引数に依存する値を配列で指定できる`

これも実は上記例ですでに使用している機能です。
上記例は下記のように空配列を指定してあげないと、無限にレンダリングが起こります。

  useEffect(() => {
    setCount((count) => count + 1)
  }, []);

useEffectでは第2引数に指定がないとprops(上述ではない)かstateが更新されるたびに実行されることになるからです。
countを更新した際に再レンダリングされないようにするために空配列を指定しています。
空配列が指定されるとuseEffectはマウント時とアンマウント時にしか実行されません。

useEffectはレンダリング後に実行される

上記例では特に問題になりませんが下記のようにするとエラーが発生します。

UseStateSample.jsx
import React, { useState, useEffect } from 'react'

const UseEffectSample = () => {
  const [count, setCount] = useState()

  useEffect(() => {
    setCount({ value: 1 })
  }, []);

  return (
    <>
      Count: {count.value}      
    </>
  )
}

export default UseEffectSample;

UseEffectSample.jsx:12 Uncaught TypeError: Cannot read property 'value' of undefined
valueなんて値はないよって言ってます。これはuseEffectが実行される前にレンダリングされてcount.valueにアクセスされていることを意味します。

useContext

私の現在の理解では複数のコンポーネント間で共有できるオブジェクトを提供するもの です。
わかりやすいところでいくと一番最初に挙げたStateを複数コンポーネント間で共有することができるようになります。
ちょっとアレンジした例を紹介。
以下ではcreateContextでuseContextを実装し、 UsingSharedState1UsingSharedState2でuseContextを宣言し、その値を参照及び変更できるようにした例です。

index.jsx
import UseContextSample from  '../components/UseContextSample'
import UsingSharedState1 from  '../components/UsingSharedState1'
import UsingSharedState2 from  '../components/UsingSharedState2'
import styles from '../styles/Home.module.css'
export default function Home() {
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <UseContextSample>
          <UsingSharedState1/>
          <UsingSharedState2/>
        </UseContextSample>
      </main>
    </div>
  )
}

UseContextSample.jsx
import React, { useState } from 'react'

export const UseContext = React.createContext({
  value: 0,
  changeValue: () => {}
});

const UseContextSample = ({ children }) => {
  const [count, setCount] = useState(0)

  return (
    <UseContext.Provider value={{value: count, changeValue: (count) => setCount(count)}}>
      {children}
    </UseContext.Provider>
  )
}

export default UseContextSample;
UsingSharedState1.jsx
import React, { useContext } from 'react'
import { UseContext } from './UseContextSample'

const UsingSharedState1 = () => {
  const sharedData = useContext(UseContext);
  return (
    <>
      <span>UsingSharedState1: {sharedData.value}</span>
      <button onClick={() => sharedData.changeValue(1)}>UsingSharedState1 Button</button>
    </>
  )
}

export default UsingSharedState1;
UsingSharedState2.jsx
import React, { useContext } from 'react'
import { UseContext } from './UseContextSample'

const UseContextSample2 = ({ children }) => {
  const sharedData = useContext(UseContext);
  return (
    <>
      <span>UsingSharedState2: {sharedData.value}</span>
      <button onClick={() => sharedData.changeValue(2)}>UsingSharedState2 Button</button>
    </>
    )
}

export default UseContextSample2;

useMemo

コンポーネントがレンダリングされる度にオブジェクトの再生成を防ぐためのもの
という認識(間違ってるかも)

UseMemoSample.jsx
import React, { useContext, useMemo } from 'react'
import { UseContext } from './UseContextSample'

const UseMemoSample = () => {
  const sharedData = useContext(UseContext);
  const array = [...Array(10000000).keys()]
  const memo = useMemo(() => {
    let temp = 0;
    array.map((index) =>{
      temp = temp + index;
    })
    return temp
  }, [xxx]);
  return (
    <>
      <p>{memo}</p>
    </>
  )
}

export default UseMemoSample;

無理やりな例ですが、上記の例でコンポーネントがレンダリングされるたびにmemoの値を計算するのは無駄に見えますよね。
これを毎回実行しないようにするためのもの。useEffectと同様に第2引数で依存変数を定義すると、実行されるタイミングを制御できます。

useCallback

useMemoの関数版。
関数の場合は実行されなくても、そのインスタンスをコンポーネントとともに生成してしまいます。
性能向上やメモリの節約(?)をするために、毎回生成する必要のないものについてはuseCallbackによって生成します。例によって、第2引数には依存変数を定義できます。

一旦は以上です。
時間があるときに他のHooksも書き残しておこうと思います。

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