20191204のReactに関する記事は25件です。

React x Typescriptで@emotion/coreを使用するために。

cssが認識されずにハマったので共有。
こちらの記事で解決。
cssを認識させるために、jsxに解釈させる必要があるらしい。
Emotion v10 unusable with pure TypeScript · Issue #1046 · emotion-js/emotion · GitHub

tsconfig.jsonにjsxFactoryを追加

"jsxFactory": "jsx",

コンポーネントファイルごとにpragmaを宣言する

/** @jsx jsx */
import { css, jsx } from '@emotion/core';

const theme = css`
    width: 100vw;
    height: 100vh;
    background-color: #000;
`;

const App: React.FC = () => {
  return (
    <div css={theme}>
      <p>hello</p>
    </div>
  );
}

export default App;

pragmaとは?

https://www.gatsbyjs.org/blog/2019-08-02-what-is-jsx-pragma/

A pragma is a compiler directive. It tells the compiler how it should handle the contents of a file.
プラグマはコンパイラ指令です。ファイルの内容を処理する方法をコンパイラに指示します。

jsx pragmaはコンパイラにjsxと解釈させるために必要なもの。

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

ReactとReduxで複数のReducerを使う

簡単な機能を実装する場合にはreducerは一つだけでも問題はないですが、
機能が増えるとreducerをモノリスにしておくわけにもいかなくなります。

複数のreducerを機能別に分けて使うにはreduxのCombineReducersを使う方法があります。
使い方のまとめとして投稿します。

CombineReducersのインポート例

rootReducerにreducerを注ぎ込む。
このrootReducerの部分はuseSelectorで取得することができるstateになる。

index.ts
import { combineReducers } from 'redux'
import {
  FirstReducer,
  SecondReducer,
} from './reducer'

const rootReducer = combineReducers({
  first: FirstReducer,
  second: SecondReducer,
})

export default rootReducer

各reducerの例

actionのpayloadは対象のreducerが担当するstateの分だけ用意すれば良い。
CombineReducersがそれらを結合してくれる。

reducer.ts
const initialFirst = {
  hoge: '',
  foo: '',
}
const initialSecond = {
  bar: '',
  baz: '',
}

export const FirstReducer = (
  state: typeof initialFirst,
  action: {
    type: string,
    payload: typeof initialFirst
  }
): typeof initialFirst => {
  switch(action.type) {
    case 'HOGE':
      return {
        ...action.payload,
        hoge: action.payload,
      }
    case 'FOO':
      return {
        ...action.payload,
        foo: action.payload,
      }
    default:
      return state
  }
}

export const SecondReducer = (
  state: typeof initialSecond,
  action: {
    type: string,
    payload: typeof initialSecond
  }
): typeof initialSecond => {
  switch(action.type) {
    case 'BAR':
      return {
        ...action.payload,
        bar: action.payload,
      }
    case 'BAZ':
      return {
        ...action.payload,
        baz: action.payload,
      }
    default:
      return state
  }
}

combineReducers · Reduxも参考に。

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

[React] Windows 10上でGatsbyJSを利用して開発用サーバでデフォルトの静的サイトを立ち上げる

目次

  • 初めに
  • GatsbyJSとは
  • 前提条件
  • Gatsby CLIのインストール
  • GatsbyJS Quick Start
  • 終わりに

初めに

応用情報の試験が終わった約一か月前からReactの勉強を始めて、最近ではcreate-react-appでひな型を生成してそこから色々書いていました。
ですが最近この記事を読んでGatsbyJSというReactのフレームワークの存在を知って、興味がわいたのでWindows 10上でGatsbyJSを触ってみることにしました。

Reactの最強フレームワークGatsby.jsの良さを伝えたい!! - Qiita

この記事では以下のGatsbyJSのデフォルトのページを立ち上げるところまでやります。
aaaaa.png

GatsbyJSとは

公式→GatsbyJS

私もこの記事を書いてる段階でアーキテクチャ等の理解が追い付いていないので、冒頭で紹介した記事を引用します。

Gatsby.jsはReactで作られた静的サイトジェネレーターです。内部的にGraphQLを用いてデータを取得し、markdownからHTMLを生成、などの処理を簡単に行うことができます。


静的サイトジェネレーターが何かと言うと、何かしらの言語で書かれたソースから、静的なHTML/CCC & JavaScriptを生成するツールのことを言います。

私はこちらの方のブログも参考にさせて頂いてます。

「Gatsby JS」を導入してみた - masalibの日記

セキュリティ面や通信速度面などの様々なメリットが挙げられていますが、Web技術絶賛勉強中の私はこのGatsbyJSを勉強するとHTTP2とかGraphQLとか最近流行りだけど一体なんのこっちゃって感じの技術も一緒に勉強できる点が大変魅力に感じました。

人によってはGatsbyJS以外にもGatsby.jsだったり単にGatsbyだったり呼ばれていますが、本記事では引用部分以外は公式に沿ってGatsbyJSで統一します。

次節で実際にGatsbyJSを触っていきます。

前提条件

以下私が触った時の前提条件になります。
- Windows 10
- node v12.13.1
- npm 6.12.1
- Gatsby CLI version 2.8.15

※以降ではnpmは既にWindows 10上にインストールしてある前提で書いてしまいました。余裕があれば後々追記します。

Gatsby CLIのインストール

公式によるとWindows上でGatsbyJSのCLIをインストールする前にwindows-build-toolsが必要らしい?(Gatsby on Windows | GatsbyJS)
実際私はいきなりnpm install -g gatsby-cliを叩いたら上手くいきませんでした。

公式にはこのwindows-build-toolsがコマンドラインで上手くインストールできなかった場合のケアも記載されていますが結論として私は問題なかったので飛ばします。

ということで私はコマンドプロンプトを開いてwindows-build-toolsをインストールを試みました。

> npm install windows-build-tools -g

すると怒られました。

Starting installation...
Please restart this script from an administrative PowerShell!
The build tools cannot be installed without administrative rights.
To fix, right-click on PowerShell and run "as Administrator".
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! windows-build-tools@5.2.2 postinstall: `node ./dist/index.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the windows-build-tools@5.2.2 postinstall script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\<username>\AppData\Roaming\npm-cache\_logs\2019-12-03T23_19_04_021Z-debug.log

要約すると2-3行目に「管理者権限のPowerShellでやり直せ!」って書いてますね。
公式にもちゃんと管理者権限のPowerShellでやれって書いてました。完全に見落としてました。

> npm install windows-build-tools -g

次にGatsby CLIのインストールに移ります。

> npm install -g gatsby-cli

一応バージョン確認。

> gatsby --version
Gatsby CLI version: 2.8.15

GatsbyJS Quick Start

Gatsby CLIがインストールできたので公式のQuick Startに従って、GatsbyJSで作った静的サイトのデフォルトページを表示してみたいと思います。

Quick Start | GatsbyJS
https://www.gatsbyjs.org/docs/quick-start/

まずは静的サイトのひな型を作成します。

> gatsby new gatsby-site

次にディレクトリを移動して開発サーバの立ち上げ。

> cd gatsby-site
> gatsby develop

開発サーバの立ち上げに成功すると途中で以下のようなメッセージが表示されます。

You can now view gatsby-starter-default in the browser.
⠀
  http://localhost:8000/

言われたとおりにhttp://localhost:8000/にアクセス。

aaaaa.png

冒頭と同じ画面が表示されました。

終わりに

React以外の技術もたくさん学べそうなのでGatsbyJSをこれから触っていこうと思います。

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

react-nativeでロード画面を表示させる

はじめに

前回の記事からの続きです。
前回はstrageに画像をアップロードするまでをやりました。strageに画像をアップしてからURLを入手するまで少し時間がかかります。そこで、待ち時間にロード画面を表示するようにします。(concurrent modeは使いません)

コード

Camera.tsx
import React, { useRef, useState } from 'react';
import { NavigationStackScreenComponent } from 'react-navigation-stack';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import {
  Button, Container, Icon, Text,
} from 'native-base';
import { Camera as ExpoCamera } from 'expo-camera';
import { upLoadImg } from '../../../utils/upLoadImg';
import usePermission from '../../../utils/usePermission';

const styles = StyleSheet.create({
  button: {
    position: 'absolute',
    bottom: 100,
    zIndex: 1,
    alignSelf: 'center',
    height: 80,
    width: 80,
    flex: 1,
    justifyContent: 'center',
  },
  icon: {
    fontSize: 50,
  },
  flexOne: {
    flex: 1,
  },
  activityIndicator: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});

const Camera: NavigationStackScreenComponent = () => {
  const { cameraPermission } = usePermission();
  const [isLoading, setIsLoading] = useState(false);

  const cameraRef = useRef(null);

  const snap = async () => {
    if (cameraRef) {
      const { uri } = await cameraRef.current.takePictureAsync(); // uriはローカルイメージURIで一時的にローカルに保存される
      const response = await fetch(uri);
      const blob = await response.blob();
      const imgName = blob.data.name;
      // console.log(blob.data.name);
      setIsLoading(true);
      upLoadImg(imgName, blob)
        .then((url) => {
          console.log(url);
          setIsLoading(false);
        });
      // .catch((error) => console.log(error));
    }
  };

  const renderCamera = (): React.ReactElement => {
    // console.log(cameraPermission);
    if (cameraPermission === null) {
      return <View />;
    } if (cameraPermission === false) {
      return <Text>No access to camera</Text>;
    }
    return (
      <Container style={styles.flexOne}>
        <ExpoCamera style={styles.flexOne} ref={cameraRef}>
          <View style={styles.flexOne}>
            <Button
              rounded
              icon
              onPress={() => snap()}
              style={styles.button}
            >
              <Icon name="camera" style={styles.icon} />
            </Button>
          </View>
        </ExpoCamera>
      </Container>
    );
  };

  const renderActivityIndicator = () => (
    <View style={styles.activityIndicator}>
      <ActivityIndicator />
    </View>
  );

  return (
    <>
      {
        isLoading
          ? renderActivityIndicator()
          : renderCamera()
      }
    </>
  );
};

export default Camera;

isLoadingというステートを用意しておいて、ロードが始まったらisLoadingをtrueに、終わったらfalseにするというようにして、isLoadingによってロード画面を表示しています。
そんなに大変な作業ではないですね。そうなんです、記事を分ける必要はなかったんです( ´•д•`; )
この記事の本題はここまでですが、僕はこれから尺を稼ぎます。
よかったら読んでください、、、

フックについて

今回はreactのhooksやカスタムフックを使いましたが、hooksについてまとめます。

フックとは

フックはreactの機能の一つで、ステートやライフサイクルなどの機能をクラスを書かずに使えるようにします。react-nativeでは、React Native0.59リリース以降でフックをサポートしています。
一部のコンポーネントだけでフックを使うこともでき、クラスコンポーネントと共存させることができます。(クラスコンポーネント内ではフックを利用することはできません)

フックのルール

  1. 通常のjavascript関数内から呼び出してはいけない(呼び出すのはfunctional componentのみ)
  2. hooksは条件分岐やネストされた関数内から呼び出すことはできない(react関数内のトップレベルでのみ呼び出す)

一つ目のルールについては二つ目のルールを違反しないためのルールだと思います。
二つ目の理由についてはこの記事がとてもわかりやすかったです。
とにかくreactはフックが呼ばれる順番に依存しているということを覚えておけば良さそうです。

カスタムフックを作る時の注意点

カスタムフックとは、名前がuseで始まり、ほかのフックを呼び出すJavaScriptの関数のことです。(javaScriptの関数内から呼び出せるんかいって思いましたが、他のフックを呼び出した時点でそれはもうJavaScript関数ではなくてカスタムフックになります。)
関数名をuseで始める理由は、その関数にフックのルールが適用されるということを知らせるためです。これによってカスタムフックを利用する時に明示的にトップレベルで呼び出すだけで良くなります。(useから始めなくても普通に動きます。)
フックにはルールがあるからフックを使っているロジックをコンポーネント間で共有するときは気をつけようということだと思います。

functional components VS stateless components

ちなみに、こんな関数?は以前までstateless componentsと呼ばれていましたが、hooksが登場した時点でこれらはfunctional componentsと呼ぶようになりました。

import React from 'react';

const Hoge = (props) => (
  <h1>{props.name}</h1>
);

function Hoge(props) {
  return (
    <h1>{props.name}</h1>
  );
};

あと、functional componentで”import React from 'react';”するのなんでなのか、ずっと気になっていました。
functional componentでreactをインポートする理由は、インポートしないと”React.createElement()”が使えないからです。上記のコードではh1タグのようなものが出てきましたが、これはJSXと呼ばれるもので、このJSXを使うとreactは自動的にReact.createElement()によって"react要素"と呼ばれるものに変換しています。

babelによってこんな感じ?に変換されるのですが、その時にReact.createElement()が必要になります。reactを書いているときはbabelによって変換される前のコードしか見ていないため分かりずらいですね。ぜひbabelのアレで試してみてください。

// 変換前
function hoge() {
  return <div className="container">hello world</div>;
}

// 変換後
function hoge() {
  return React.createElement("div", {
    className: "container"
  }, "hello world");
}

途中で出てきた誰やねんって感じの"react要素"ですが、react要素と”ルートDOMノード”をReactDOM.render()に渡すことで画面が表示されます。

// .html
// ルートDOMノード
<div id="root"></div>

// .js .jsx
import React from 'react';
import ReactDOM from 'react-dom';
// JSX
const element = <h1>Hello, world</h1>; 
ReactDOM.render(element, document.getElementById('root'));

reactの環境をずっとcreate-react-appで整えてきたためこの辺りはあまり知りませんでした。babelやらwebpackやらは名前かっこいいくらいにしか思っていなかったため、自力でreactの環境を整えながら勉強しようと思いました。

最後に

最後まで読んで頂きありがとうございます。締めの一言的なものが何も思い浮かばないため、世界一どうでもいい情報を発表しときます。
最近ボクサーからトランクスに乗り換えました!!

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

react-firebase-hooksを使ってみた(Firestore Hooks編)

はじめに

前回の記事でreact-firebase-hooksのAuth Hooksを使ってみたのですが、今回はFirestore Hooksに挑戦したいと思います。

react-firebase-hooks

リポジトリはこちらです。

https://github.com/CSFrequency/react-firebase-hooks

このライブラリは、4種類のAPIを提供しています。

  • Auth Hooks
  • Cloud Firestore Hooks
  • Cloud Storage Hooks
  • Realtime Database Hooks

今回対象とするのは2つ目のFirestore Hooksです。

テストデータの作成

最初に、Firestoreのコンソールでコレクションといくつかのドキュメントを作成します。コレクション名はtodosとしました。

image.png

コーディング

モジュールのimport

今回使うモジュールをimportします。

import React, { useState } from "react";
import ReactDOM from "react-dom";

import firebase from "firebase";
import { useCollectionData } from "react-firebase-hooks/firestore";

TodoListコンポーネント

todosコレクションを表示するコンポーネントを作ります。

const TodoList = () => {
  const [values, loading, error] = useCollectionData(
    firebase.firestore().collection("todos"),
    { idField: "id" }
  );
  if (loading) {
    return <div>Loading...</div>;
  }
  if (error) {
    return <div>{`Error: ${error.message}`}</div>;
  }
  return (
    <ul>
      {values.map(value => (
        <li key={value.id}>{value.title}</li>
      ))}
    </ul>
  );
};

idFieldでidを取得するところがポイントです。

NewTodoコンポーネント

todosコレクションに新たなドキュメントを追加するためのコンポーネントを作ります。

const NewTodo = () => {
  const [title, setTitle] = useState("");
  const [pending, setPending] = useState(false);
  const add = async () => {
    setTitle("");
    setPending(true);
    try {
      await firebase
        .firestore()
        .collection("todos")
        .add({ title });
    } finally {
      setPending(false);
    }
  };
  return (
    <div>
      <input value={title} onChange={e => setTitle(e.target.value)} />
      <button type="button" onClick={add}>
        Add
      </button>
      {pending && "Pending..."}
    </div>
  );
};

エラー処理は省略しています。

Appコンポーネント

最後に、全体をつなげるAppコンポーネントとReactDOMのrenderです。

const App = () => {
  return (
    <div>
      <TodoList />
      <NewTodo />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

CodeSandbox

https://codesandbox.io/s/wonderful-browser-zh0wc

完成したものがこちらです、実際に動作させるためにはforkしてfirebaseConfigを置き換える必要がありますのでご注意ください。

image.png

おわりに

Firestoreのリアルタイム更新とReact Hooksはとても相性がいいと感じました。ドキュメントを追加したら、すぐに更新されます。Firestoreのコンソールから追加しても同様です。

今回は、useCollectionDataを使いましたが、用意されているhooksはさらにあります。

  • useCollection
  • useCollectionOnce
  • useCollectionData
  • useCollectionDataOnce
  • useDocument
  • useDocumentOnce
  • useDocumentData
  • useDocumentDataOnce

Once系は一度だけの取得なのですが、その場合はhookがどれだけ役立つかは微妙です。callbackから使うことになることが多い気がします。また、Data系のhookはTypeScriptの型が付けられますが、ソースコード上は単にアサーションしているだけなので、予期せぬランタイムエラーが発生する可能性がありそうです。結局、独自の拡張をしようと思うとcustom hooksを作ることになりそうですが、その先に本ライブラリのhooksから合成できるかはやってみないと分からないといった感じになりそうです。

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

ゆ、useEffectちゃん!初回に動かないで!

TL;DR

よいしょ……よいしょ……
useEffect便利ですよね。
stateの変化を監視し、そのstateの変化に伴うべき処理の流れを一元管理できます。

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

function Counter(props) {
  const [count, setCount] = useState(0)
  const [lastUpdatedAt, setLastUpdatedAt] = useState(null)

  useEffect(() => {// 『count』 が更新された際に、それに伴い必ず実行される
    setLastUpdatedAt(new Date().toString())
  }, [count]) 

  return (
    <div>
      <p>カウント {count} 回目</p>

      {/* 変な要件の機能だなぁ…? */}
      <p>?最終カウントアップ日時? {lastUpdatedAt || ''} </p>

      <p>
        <button onClick={() => setCount(count + 1)}>カウントアップ</button>
      </p>
    </div>
  )
}

ReactDOM.render(
  <Counter />,
  document.getElementById('root')
)

かなしいところ

(上記例の様に)何も考えずそのまま使うと、対象のstateが変更されているか否かに関わらず、初回レンダー時『にも』必ず動いてしまう。

キャプチャ.PNG

カウントまだ1回もしてないのに「最終カウントアップ日時」出てんのおかしいダルルォン!?(うるさいですね・・・)

かいけつ

useRefを使う。

import React, { useState, useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'

function Counter(props) {
  const [count, setCount] = useState(0)
  const [lastUpdatedAt, setLastUpdatedAt] = useState(null)

  const isFirstRender = useRef(false)

  useEffect(() => { // このeffectは初回レンダー時のみ呼ばれるeffect
    isFirstRender.current = true
  }, [])

  useEffect(() => {// 『count』 が更新された場合『と』初回レンダー時に動くeffect
    if(isFirstRender.current) { // 初回レンダー判定
      isFirstRender.current = false // もう初回レンダーじゃないよ代入
    } else {
      setLastUpdatedAt(new Date().toString())      
    }
  }, [count]) 

  return (
    <div>
      <p>カウント {count} 回目</p>

      <p>?最終カウントアップ日時? {lastUpdatedAt || ''} </p>

      <p>
        <button onClick={() => setCount(count + 1)}>カウントアップ</button>
      </p>
    </div>
  )
}

ReactDOM.render(
  <Counter />,
  document.getElementById('root')
)

かつてのクラスコンポーネントで言う所の「componentDidMount」と似た働きが期待出来ます。

けつろん

結果オーライ! 終わりやっぱりReactはたのしい。以上。

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

Nerves と GraphQLsever の組み合わせを考える「ポエム」

この記事は、「NervesJP Advent Calendar 2019」の6日目になります。

「NervesJP Advent Calendar 2019」5日目は、zacky1972さんの「CPU Info や Pelemay を開発している時にわかった Nerves 対応のコツを書きます。(CPU Info 編)」

そしてこの記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2019」の1日目の続きになります。
東京だけど fukuoka.ex の YOSUKENAKAO.me です。

普段は合同会社The Waggleで「教育」に関わるサービス作りのお仕事と学習教材の開発や
研修講座の企画開発をしています。

この記事の構成
このカレンダーでは、以下3つの Advent Calendar に投稿する予定の3部構成の第2弾となります。
そして、Advent Calender で扱うテーマは「GraphQL と Elixir で始めるプロトタイプ開発の未来」
という名のポエムです。

3部構成の大まかな予定は以下です。

Advent Calendar fukuoka.ex 1日目
https://qiita.com/advent-calendar/2019/fukuokaex
GraphCMS から Absinthe を利用して作る Elixir で体験的に GraphQLSever を作る「ポエム」

Advent Calendar NervesJP 6日目
https://qiita.com/advent-calendar/2019/nervesjp
Nerves と GraphQLsever の組み合わせを考える「ポエム」

Advent Calendar Elixir 24日目
https://qiita.com/advent-calendar/2019/elixir
GraphCMS から入り、Absintheを利用して作って動かす「チュートリアル」

となっています。

Nerves と GraphQLsever の組み合わせを考える「ポエム」

前回の実装の続きを書く前に、Nervesについてのポエムを先に書きたいと思います。内容は薄目ですみません!

さて、もともとはフィジカルプログラミングというテーマでプログラミングの学習を5年前にやり始めたのが僕のここ最近のルーツだったりします。当時はArduiunoのLチカから、信号機の歴史を振り返りながらArduiunoの勉強ができる歴史の教材を作っていました。プログラミングではなく、歴史の教材です。ここがポイント。

それが、当時PETSという子供のプログラミング学習ロボットを作っていたメンバーの一人、今はホロラボのファウンダーの1人である方にお声がけ頂いたのをきっかけに、プログラミング学習教材「PETS」という教材を作る事になったきっかけでもあります。PETSは、学校の先生でも45分の授業の中で完結でき、子供の学習意欲を引き出すゲーミフィケーションを取り入れたカリキュラム設計となっています。

学習教材をデジタル化する上で、こだわった事はアナログとの融合です。

そもそも学習教材をデジタル化する上で検討しなければならない大事な観点として以下2つを考える必要があると僕は考えています。

1.それデジタルでやる意味ある?

2.アナログとのコストと比べて学習価値が出せているか?

PETSは、その点を融合して作られたプロトタイプの教材です。

ただし、製品化はされて現在も販売して、いまでこそ大がかりなPRをしてなくても学校に選ばれるようになってきています。

それも先生が欲しがる教材です。

そのようなプロダクトでも、デジタルの持つ特徴の1つで実現したかったけど、当時断念したことが一つあります。

これは今後の課題です。

この問題が解決する事ができる事で、教材として別の次元や違うサービスを構築する事が可能になります。

その課題は、ファームウェアのアップデートです。

そして、教材ですからできれば学習データを取得したいです。しかし、学習データとして何を取得するのか?

これは闇雲に取得しても使えるものにはならないので、柔軟にアップデートとデータの変更ができる仕組みも欲しい所です。

という事で、これらの課題に応えられそうな技術として、NervesとGraphQL Serverの組み合わせに関心があります。

まだ、Nervesについては、Nerves-hubを通じて遠隔のデバイス内のソフトウェアのアップデートをネットを通じてするという
体験くらいしか触れていないのですが、NervesとGraphQLの組み合わせの事例を探して、どんな利用ができるのか?

というのを調べてみると、全然見つからない状態で、やっと1件だけみつけたのですが、遠い、、、。
https://elixirconf.com/2019/training-classes/8

という事でNervesとGraphQLの組み合わせは今後のテーマとして、できたらシェアしていこうかなと思います。

という事で、今回のポエム部分はこれくらいにして、NervesとGraphQLの組み合わせのGraphQLの方の続きという事で以下よりお届けします。

GraphQLのクエリを書く

query {
  authors{
     id
   name
    bibliography
  }
}

クエリを書いて、無事に成功していれば下記のようなデータが返ってきます。

graphql_1.PNG

React のreact-apollo-blog のAbout.jsに上記のクエリを上書きする

src/components/About.js
export const authors = gql`
query authors {
  authors{
    id
    name
    bibliography
  }
}
`

エンドポイントを書き換える

src/index.js
const GRAPHCMS_API = http://localhost:4000/api/

これで、yarn startしてlocalhost:3000/aboutページにアクセスすると、、、見れません。
エラーを確認するとクロスサイトスクリプティングの問題でデータが取得できてないです。そこで、GraphQL server側に機能を追加します。

Cros_plugを追加する

https://hex.pm/packages/cors_plug

mix.exs
  defp deps do
    [
# ~省略
      {:absinthe, "~> 1.4.2"},
      {:absinthe_plug, "~> 1.4.0"},
      {:absinthe_phoenix, "~> 1.4.0"},
      {:cors_plug, "~> 2.0"},     #<- 追加
    ]
  end

機能を追加する。

$ mix deps.get

router.exのパイプラインにプラグを追加

lib/sampleBlog_web/router.ex
  pipeline :api do
    plug CORSPlug, origin: "http://localhost:3000" #<-追加
    plug :accepts, ["json"]
  end

これで、http://localhost:3000/aboutにアクセスで以下のようにデータが取得できたら成功です。

blog.PNG

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

君はVue,Reactの次に来るSvelteを知っているか?

はじめに

この記事はAteam Brides Inc. Advent Calendar 2019 5日目の記事です。

はじめまして、エイチームブライズ新卒1年目の@oekazumaです。最近僕がハマっているSvelteに関して書きたいと思います!

Svelteとは?

1_OJLglSTFZ1PbwpRG0U2xXA.png

SvelteRich Harris氏によって開発されたコンパイラーでVueやReactのようにブラウザー上でコンポーネント化をするフレームワークではなく*.svelteファイルをhtml, js, cssに変換します。
「すらりとした」という意味を持つ名の通り軽量で高速。
ベンチマークでReactの35倍、Vueの50倍速いです。

Svelteの3つの魅力

公式にも書かれている下記の3つを中心に説明していきます!
1. Write less code (より少ないコードを書く)
2. No Virtual DOM (仮想DOMがない)
3. Truly reactive (本当に反応的)

Write less code(記述量が少ない)

入力フォームで変数aとbに値を入力し、足して表示するプログラムを例にしてみると

React 442文字

import React, { useState } from 'react';

export default () => {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);

  function handleChangeA(event) {
    setA(+event.target.value);
  }

  function handleChangeB(event) {
    setB(+event.target.value);
  }

  return (
    <div>
      <input type="number" value={a} onChange={handleChangeA}/>
      <input type="number" value={b} onChange={handleChangeB}/>

      <p>{a} + {b} = {a + b}</p>
    </div>
  );
};

Vue 263文字

<template>
  <div>
    <input type="number" v-model.number="a">
    <input type="number" v-model.number="b">

    <p>{{a}} + {{b}} = {{a + b}}</p>
  </div>
</template>

<script>
  export default {
    data: function() {
      return {
        a: 1,
        b: 2
      };
    }
  };
</script>

Svelte 145文字

<script>
    let a = 1;
    let b = 2;
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {a + b}</p>

すごく記述量が少ないことがわかると思います。
書き方自体はVueに似ている部分もあるので既にVueを書いている方だとそんなに違和感なく開発できそうです。

No Virtual DOM(仮想DOMがない)

仮想DOMはオーバーヘッドであると言っています。大きな理由としては「実DOMとの差分を計算するのって無料じゃないしオーバーヘッドだよね」というところにあります。
Svelteは仮想DOMを使用せずに同様のプログラミングモデルで十分なパフォーマンスで、状態遷移を考慮することなくアプリを構築できます。
以下の流れでいうとSvelteは1と4だけで済むということです。

仮想DOMでHTMLが書き換わるまでの流れ
1. 現在の状態(state)が変わる
2.再レンダリング(仮想DOMの再構成)を実行する
3.実DOMとの差分を計算する
4.実際にHTML(=実DOM)を書き換える

Truly reactive(本当に反応的)

ReactおよびVueは、状態変数が変更されたときに更新する場所を追跡できず、その結果、状態変数が存在するコンポーネント全体とそのすべての子を更新します。
一方、Svelteはアプリケーションを介してデータを追跡し、更新された変数に依存する変数のみを更新できます。

さいごに

日本では正直全然話題になっていませんが、海外のフロントエンド界隈では盛り上がっているようでこれから日本でも流行っていくのではないかなと勝手に思っています。
数年後にはVue,Reactと肩を並べて語られている気がする...(^ω^)
今は日本語文献がかなり少ないので盛り上げていってもっと身近にSvelteを感じられるようになれば嬉しいなと思います!
この記事では実践的な部分がなかったのですが、明日に@mkinsvelte3でToDoリストをチュートリアルと照らし合わせて作るぞ! 【入門編】を書いてくれるので楽しみにしていてください!

私たちのチームで働きませんか?

alt
エイチームは、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける名古屋の総合IT企業です。
そのグループ会社である株式会社エイチームブライズでは、一緒に働く仲間を募集しています!

上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずはチャットでざっくばらんに話をしましょう。
技術的な話だけでなく、私たちが大切にしていることや、お任せしたいお仕事についてなどを詳しくお伝えいたします!

Qiita Jobsよりメッセージお待ちしております!

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

react-firebase-hooksを使ってみた(Auth Hooks編)

はじめに

FirebaseとFirestoreをReact Hooksで使いたいと以前から思っていましたが、react-firebase-hooks v1はあまり納得がいかず、自作のcustom hooksを使っていました。その後v2が出たので、調べなければと思いつつ、半年くらい経ってしまいましたが、とうとう重い腰をあげることにします。

react-firebase-hooks

リポジトリはこちらです。

https://github.com/CSFrequency/react-firebase-hooks

今回はAuth Hooksを試してみようと思います。

コーディング

モジュールのimport

最初に必要なモジュールをimportします。

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";

import firebase from "firebase";
import { useAuthState } from "react-firebase-hooks/auth";

firebaseの初期化

次に、firebaseの初期化をします。

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "..."
};
firebase.initializeApp(firebaseConfig);

Loginコンポーネント

ログイン用のコンポーネントを作ります。

const Login = () => {
  const [email, setEmail] = useState("");
  const [pass, setPass] = useState("");
  const [error, setError] = useState(null);
  const [pending, setPending] = useState(false);
  const mounted = useRef(true);
  useEffect(() => {
    const cleanup = () => {
      mounted.current = false;
    };
    return cleanup;
  }, []);
  const onSubmit = async e => {
    e.preventDefault();
    setError(null);
    setPending(true);
    try {
      await firebase.auth().signInWithEmailAndPassword(email, pass);
    } catch (e) {
      console.log(e.message, mounted);
      if (mounted.current) setError(e);
    } finally {
      if (mounted.current) setPending(false);
    }
  };
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          placeholder="Email..."
        />
        <input
          type="password"
          value={pass}
          onChange={e => setPass(e.target.value)}
          placeholder="Password..."
        />
        <button type="submit">Login</button>
        {pending && "Pending..."}
        {error && `Error: ${error.message}`}
      </form>
    </div>
  );
};

ちょっと複雑になりましたが、やっていることは単純です。本来は、テキストフィールドを更新したところで、エラーメッセージをクリアすべきですが、そこは省略。

Logoutコンポーネント

ログアウト用のコンポーネントを作ります。

const Logout = () => {
  const [pending, setPending] = useState(false);
  const mounted = useRef(true);
  useEffect(() => {
    const cleanup = () => {
      mounted.current = false;
    };
    return cleanup;
  }, []);
  const logout = async () => {
    setPending(true);
    await firebase.auth().signOut();
    if (mounted.current) setPending(false);
  };
  return (
    <div>
      <button type="button" onClick={logout}>
        Logout
      </button>
      {pending && "Pending..."}
    </div>
  );
};

Pending表示が短い場合はChrome Dev ToolsのNetwork TabでThrottlingをしましょう。

Appコンポーネント

最後に、全体をつなげるAppコンポーネントとReactDOMのrenderです。

const App = () => {
  const [user, initialising, error] = useAuthState(firebase.auth());
  if (initialising) {
    return <div>Initialising...</div>;
  }
  if (error) {
    return <div>Error: {error}</div>;
  }
  if (!user) {
    return <Login />;
  }
  return (
    <div>
      User: {user.email}
      <Logout />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

今回は、ログインしたらユーザのemailを表示するだけのシンプルなものです。

CodeSandbox

https://codesandbox.io/s/upbeat-chaum-vzpjg

完成したものがこちらです、実際に動作させるためにはforkしてfirebaseConfigを置き換える必要がありますのでご注意ください。

おわりに

今までは、onAuthStateChangedをラップした独自custom hooksを使ってましたが、それがライブラリ化されることで、多少見通しはよくなったような気はします。しかし、loginやlogoutの機能を内包するcustom hooksは提供されていないため、今回のように長いコードになってしまいました。結局、そこには独自custom hooksが必要になりそうです。

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

Reactで作る汎用的なシンプルアコーディオン

背景

クリックしたら開いて閉じるというただ単純な実装が、色々なコンポーネントに散乱しだしていたので、汎用的にその機能を使えるコンポーネントを実装してみました。

実装

import React, { useState } from 'react'

const SimpleAccordion = ({
  defaultShow = false,
  onOpen,
  onClose,
  ...props
}) => {
  const [show, setShow] = useState(defaultShow)
  const toggle = () => {
    const toggled = !show
    setShow(toggled)
    toggled ? onOpen && onOpen() : onClose && onClose()
  }
  const display = show ? 'block' : 'none'
  return (
    <React.Fragment>
      {props.children.map((child, idx) =>
        child.props.name === 'SimpleAccordionSummary' ? (
          <div key={idx} onClick={toggle}>
            {child}
          </div>
        ) : child.props.name === 'SimpleAccordionDetails' ? (
          <div key={idx} style={{ display }}>
            {child}
          </div>
        ) : null
      )}
    </React.Fragment>
  )
}

const SimpleAccordionSummary = ({ children }) => (
  <React.Fragment>{children}</React.Fragment>
)

const SimpleAccordionDetails = ({ children }) => (
  <React.Fragment>{children}</React.Fragment>
)

SimpleAccordionSummary.defaultProps = {
  name: 'SimpleAccordionSummary'
}

SimpleAccordionDetails.defaultProps = {
  name: 'SimpleAccordionDetails'
}

export { SimpleAccordion, SimpleAccordionSummary, SimpleAccordionDetails }

実用例

const Sample = () => (
  <React.Fragment>
    <SimpleAccordion>
      <SimpleAccordionSummary>
        <p>概要</p> {/* ここをクリックすると開いたり閉じたりする */}
      </SimpleAccordionSummary>
      <SimpleAccordionDetails>
        <p>詳細</p>
      </SimpleAccordionDetails>
    </SimpleAccordion>
  </React.Fragment>
)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】マウント時に自動でfocusあてるhooks

useAutoFocus.ts
import * as React from 'react';

export default function useAutoFocus<RefType extends HTMLElement>() {
  const inputRef = React.useRef<RefType>(null);

  React.useEffect(() => {
    const node = inputRef.current;
    if (node) {
      node.focus();
    }
  }, []);

  return inputRef;
}

使う側

function Hoge(props: Props) {
  const [code, setCode] = React.useState('');

  const inputRef = useAutoFocus<HTMLInputElement>();

  return (
    <TextInput
      value={code}
      onChange={(e) => setCode(e.target.value)}
      ref={inputRef}
    />
  );
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"react-beautiful-dnd" で実装する React の Drag&Drop with TypeScript, 関数コンポーネント

はじめに

React で自前の Drag&Drop (DnD) を実装するのは、なかなか大変だと思います。
ライブラリを探していたところ、Atlassian 製の react-beautiful-dnd というライブラリがよさそうだったので使ってみての所感を書きます。

公式サンプル

縦方向リストの DnD
横方向リストの DnD
関数コンポーネントで実装
2カラム間の DnD

ヌルヌル動いて気持ち〜〜〜

dnd_sample_official.gif

手元で実装してみる

バージョン

"typescript": "^3.7.3",
"react": "^16.12.0",
"react-beautiful-dnd": "^12.2.0",
"styled-components": "^4.4.1"

作ったのはこれ

dnd_sample_kats.gif
(見た目はほぼサンプルと同じだねとか、、聞こえてますよ、、ええ)

↓ディレクトリ構成など分かりやすいようにアレンジしてあるので参考にしてみてください↓
https://github.com/kk-web/react-beautiful-dnd_sample

インストール

※ React まわりの構築は割愛します。

まずは、モジュールをインストール!

npm install --save react-beautiful-dnd

元になるリストを作る

src/App.tsx
import React from "react";
import { ItemType } from "./types";
import List from "./List";

const App = () => {
  const initial: ItemType[] = Array.from({ length: 10 }, (v, k) => k).map(k => {
    return {
      id: `id-${k}`,
      content: `Item ${k}`
    };
  });
  return <List items={initial} />;
};

export default App;
src/types.ts
export type ItemType = {
  id: string;
  content: string;
};
src/List.tsx
import React from "react";
import Item from "./Item";

const List = ({ items }) => (
  <>
    {items.map(item => (
      <Item item={item} key={item.id} />
    ))}
  </>
);

export default List;
src/Item.tsx
import React from "react";
import styled from "styled-components";

const StyledItem = styled.div`
  width: 200px;
  border: 1px solid grey;
  margin-bottom: 8px;
  background-color: lightblue;
  padding: 8px;
`;

const Item = ({ item }) => {
  return <StyledItem>{item.content}</StyledItem>;
};

export default Item;

型定義

ドキュメントの型定義セクションには、

Typescript
If you are using TypeScript you can use the community maintained DefinitelyTyped type definitions. Installation instructions.

とあります。型定義モジュールもインストールしておきましょう。

npm install --save @types/react-beautiful-dnd

Here is an example written in typescript.

サンプルも用意されています!

DnD 実装

src/App.tsx
import React, { useState } from "react";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { ItemType } from "./types";
import List from "./List";

const reorder = (
  list: ItemType[],
  startIndex: number,
  endIndex: number
): ItemType[] => {
  const result = Array.from(list);
  const [removed] = result.splice(startIndex, 1);
  result.splice(endIndex, 0, removed);

  return result;
};

const App = () => {
  const initial: ItemType[] = Array.from({ length: 10 }, (v, k) => k).map(k => {
    return {
      id: `id-${k}`,
      content: `Item ${k}`
    };
  });
  const [state, setState] = useState({ items: initial });

  const onDragEnd = result => {
    if (!result.destination) {
      return;
    }

    if (result.destination.index === result.source.index) {
      return;
    }

    const items = reorder(
      state.items,
      result.source.index,
      result.destination.index
    );

    setState({ items });
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId="list">
        {provided => (
          <div ref={provided.innerRef} {...provided.droppableProps}>
            <List items={state.items} />
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

export default App;

↓変更ありません

src/types.ts
export type ItemType = {
  id: string;
  content: string;
};
src/List.tsx
import React from "react";
import Item from "./Item";

const List = React.memo<{ items }>(({ items }) => (
  <>
    {items.map((item, index: number) => (
      <Item item={item} index={index} key={item.id} />
    ))}
  </>
));

export default List;
src/Item.tsx
import React from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";

const StyledItem = styled.div`
  width: 200px;
  border: 1px solid grey;
  margin-bottom: 8px;
  background-color: lightblue;
  padding: 8px;
`;

const Item = ({ item, index }) => {
  return (
    <Draggable draggableId={item.id} index={index}>
      {provided => (
        <StyledItem
          ref={provided.innerRef}
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          {item.content}
        </StyledItem>
      )}
    </Draggable>
  );
};

export default Item;

Server Side Rendering

Next.js で実装したときにつまづきました、、

Drag できなくて、console を見てみると

react-beautiful-dnd A setup problem was encountered.
> Invariant failed: Draggable[id: id-9]: Unable to find drag handle?‍ This is a development only message. It will be removed in production builds.

とエラーが出ていました。
ドキュメントを見てみると

API ?
(中略)
resetServerContext() - Utility for server side rendering (SSR)

と記載が!
SSR の場合は、任意の場所でこの関数を実行しましょう!

おわりに

DnD を自作で実装しようとすると手間がかかりますが、react-beautiful-dnd を使えば数行の実装用関数と state や props を設定するだけで、ヌルヌル動くものができるのはありがたいですね!
公式サンプルではコンポーネントが1つのファイルにまとめられていますが、今回の実装のようにリストやアイテムで分解すれば、既存のリストにも適用しやすいのではないかと思います。

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

【React】 二種類のコンポーネント class component と functional component について

class component

class App extends Componentにて、AppクラスがComponentクラスを継承している。このAppのことをクラスコンポーネントと呼ぶ。

/src/App.js
import React, {Component} from 'react';

class App extends Component{
  render(){
    return(
      <React.Fragment>
        <label htmlFor="bar">bar </label>
        <input type="tect" onChange={() => {console.log("i am clicked")}} />
      </React.Fragment>
    )
  }
}
export default App;

functional component

関数のみをエクスポートしているから関数コンポーネントと呼ばれる

/src/App.js
import React, {Component} from 'react';

const App = () => {
  return <div>Hi</div>
}
export default App;

import React, {Component} from 'react';としているが、ここでは次のワーニングメッセージが表示される。

Compiled with warnings.

./src/App.js
  Line 1:16:  'Component' is defined but never used  no-unused-vars

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

クラスコンポーネントと異なり、Componentを継承する必要がないため。
故にimport React from 'react';としてよい。

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

最速のフレームワーク(というのは存在しない)

何日か前にTwitterでこの投稿が話題になりました。

ReactのConcurrent Modeでは、ステートを持つ2000個のコンポーネントを安定した60fpsで再レンダーさせられるようです。一方で、ReactのいわゆるLegacy Modeでは全然60fpsにならない。何人かが、Svelteで同じデモできないかとツイートし、Svelteの創始者Rich Harris氏もデモ作ってくれました。

dev環境なので60fpsまでは出ないが、React版と違って、コンポーネントの個数を選ぶ度に遅延がないとのことです。(数に関わらず)

しかし、フレームワークの比較はそもそもこれでいいのか?

そもそも上のような数千ポリゴンの3Dボールをレンダーしたい時に、わざわざReactなどのFWを使うのはおかしくないか?実際のアプリと上のデモはかなり違うし、仮に同じものをページに置きたいとしても、Three.jsなどのライブラリですでにできることだから、それをFWで抽象化する意味は何か?ということを考えないといけないです。

何が本当に遅いかというと、ユーザーのコードです。FWのコントリビュータはパフォーマンスの最適化にベストを尽くしているが、任意のユーザーのコードはもちろん最適化できない(が、Svelteの斬新なアプローチで、ステートの更新など、期待できるパターンも最適化の対象にはなっているようです)。あまり例としては現実的ではないのですが、ユーザー(開発者)が無限ループを書いたらFWどころではなくなります。

さらに、実際のアプリで不可欠なIO処理は、ベンチマークでは測られていないです。データの取得とレンダリングはどう設計されているか?という点ではFWは多少違います。たとえば、ReactのConcurrent Modeでは、ネットワークのIO処理を待っている間に、アイドリングしているCPUを有効に使って、一部の要素をプリレンダーできます。IO処理が終わってからレンダリングを始める場合は、必然的に表示が遅くなります。

他にも指摘されたのは、測る過程。マイナーな原因だと思いますが、JSのJITコンパイラ仕様上、上のようなデモでは、1回目のレンダリング(初期ロード・マウント)を測っているか、時間が経ってから測っているかによって差が出るようです。なぜかというと、JITコンパイラがcoldな(まだ実行されていない)コードをインタプリターとして解釈するけど、よく実行されるhotなコードを、コンパイルかつ最適化してくれるからです。なので複数回レンダリングされたコンポーネントのコードがコンパイルされ速くなっている可能性もあります。それと比べて、新規マウントが多い実際のアプリでは、coldなコードが多いです。

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

react

https://www.youtube.com/watch?v=-edmQKcOW8s&t=12s

のsrc/context.jsについて

import React, { Component } from "react";
import { storeProducts, detailProduct } from "./data";
const ProductContext = React.createContext();

class ProductProvider extends Component {
  state = {
    products: [],
    detailProduct: detailProduct,
    cart: [],
    modalOpen: false,
    modalProduct: detailProduct,
    cartSubTotal: 0,
    cartTax: 0,
    cartTotal: 0
  };
  componentDidMount() {
    this.setProducts();
  }

  setProducts = () => {
    let products = [];
    storeProducts.forEach(item => {
      const singleItem = { ...item };
      products = [...products, singleItem];
    });
    this.setState(() => {
      return { products };
    }, this.checkCartItems);
  };

  getItem = id => {
    const product = this.state.products.find(item => item.id === id);
    return product;
  };
  handleDetail = id => {
    const product = this.getItem(id);
    this.setState(() => {
      return { detailProduct: product };
    });
  };
  addToCart = id => {
    let tempProducts = [...this.state.products];
    const index = tempProducts.indexOf(this.getItem(id));
    const product = tempProducts[index];
    product.inCart = true;
    product.count = 1;
    const price = product.price;
    product.total = price;

    this.setState(() => {
      return {
        products: [...tempProducts],
        cart: [...this.state.cart, product],
        detailProduct: { ...product }
      };
    }, this.addTotals);
  };
  openModal = id => {
    const product = this.getItem(id);
    this.setState(() => {
      return { modalProduct: product, modalOpen: true };
    });
  };
  closeModal = () => {
    this.setState(() => {
      return { modalOpen: false };
    });
  };
  increment = id => {
    let tempCart = [...this.state.cart];
    const selectedProduct = tempCart.find(item => {
      return item.id === id;
    });
    const index = tempCart.indexOf(selectedProduct);
    const product = tempCart[index];
    product.count = product.count + 1;
    product.total = product.count * product.price;
    this.setState(() => {
      return {
        cart: [...tempCart]
      };
    }, this.addTotals);
  };
  decrement = id => {
    let tempCart = [...this.state.cart];
    const selectedProduct = tempCart.find(item => {
      return item.id === id;
    });
    const index = tempCart.indexOf(selectedProduct);
    const product = tempCart[index];
    product.count = product.count - 1;
    if (product.count === 0) {
      this.removeItem(id);
    } else {
      product.total = product.count * product.price;
      this.setState(() => {
        return { cart: [...tempCart] };
      }, this.addTotals);
    }
  };
  getTotals = () => {
    // const subTotal = this.state.cart
    //   .map(item => item.total)
    //   .reduce((acc, curr) => {
    //     acc = acc + curr;
    //     return acc;
    //   }, 0);
    let subTotal = 0;
    this.state.cart.map(item => (subTotal += item.total));
    const tempTax = subTotal * 0.1;
    const tax = parseFloat(tempTax.toFixed(2));
    const total = subTotal + tax;
    return {
      subTotal,
      tax,
      total
    };
  };
  addTotals = () => {
    const totals = this.getTotals();
    this.setState(
      () => {
        return {
          cartSubTotal: totals.subTotal,
          cartTax: totals.tax,
          cartTotal: totals.total
        };
      },
      () => {
        // console.log(this.state);
      }
    );
  };
  removeItem = id => {
    let tempProducts = [...this.state.products];
    let tempCart = [...this.state.cart];

    const index = tempProducts.indexOf(this.getItem(id));
    let removedProduct = tempProducts[index];
    removedProduct.inCart = false;
    removedProduct.count = 0;
    removedProduct.total = 0;

    tempCart = tempCart.filter(item => {
      return item.id !== id;
    });

    this.setState(() => {
      return {
        cart: [...tempCart],
        products: [...tempProducts]
      };
    }, this.addTotals);
  };
  clearCart = () => {
    this.setState(
      () => {
        return { cart: [] };
      },
      () => {
        this.setProducts();
        this.addTotals();
      }
    );
  };
  render() {
    return (
      <ProductContext.Provider
        value={{
          ...this.state,
          handleDetail: this.handleDetail,
          addToCart: this.addToCart,
          openModal: this.openModal,
          closeModal: this.closeModal,
          increment: this.increment,
          decrement: this.decrement,
          removeItem: this.removeItem,
          clearCart: this.clearCart
        }}
      >
        {this.props.children}
      </ProductContext.Provider>
    );
  }
}

const ProductConsumer = ProductContext.Consumer;

export { ProductProvider, ProductConsumer };
import React, { Component } from "react";
import { storeProducts, detailProduct } from "./data";
const ProductContext = React.createContext();

class ProductProvider extends Component {
  state = {
    products: [],
    detailProduct: detailProduct,
    cart: [],
    modalOpen: false,
    modalProduct: detailProduct,
    cartSubTotal: 0,
    cartTax: 0,
    cartTotal: 0
  };
  componentDidMount() {
    this.setProducts();
  }

  setProducts = () => {
    let products = [];
    storeProducts.forEach(item => {
      const singleItem = { ...item };
      products = [...products, singleItem];
    });
    this.setState(() => {
      return { products };
    }, this.checkCartItems);
  };

  getItem = id => {
    const product = this.state.products.find(item => item.id === id);
    return product;
  };
  handleDetail = id => {
    const product = this.getItem(id);
    this.setState(() => {
      return { detailProduct: product };
    });
  };
  addToCart = id => {
    let tempProducts = [...this.state.products];
    const index = tempProducts.indexOf(this.getItem(id));
    const product = tempProducts[index];
    product.inCart = true;
    product.count = 1;
    const price = product.price;
    product.total = price;

    this.setState(() => {
      return {
        products: [...tempProducts],
        cart: [...this.state.cart, product],
        detailProduct: { ...product }
      };
    }, this.addTotals);
  };
  openModal = id => {
    const product = this.getItem(id);
    this.setState(() => {
      return { modalProduct: product, modalOpen: true };
    });
  };
  closeModal = () => {
    this.setState(() => {
      return { modalOpen: false };
    });
  };
  increment = id => {
    let tempCart = [...this.state.cart];
    const selectedProduct = tempCart.find(item => {
      return item.id === id;
    });
    const index = tempCart.indexOf(selectedProduct);
    const product = tempCart[index];
    product.count = product.count + 1;
    product.total = product.count * product.price;
    this.setState(() => {
      return {
        cart: [...tempCart]
      };
    }, this.addTotals);
  };
  decrement = id => {
    let tempCart = [...this.state.cart];
    const selectedProduct = tempCart.find(item => {
      return item.id === id;
    });
    const index = tempCart.indexOf(selectedProduct);
    const product = tempCart[index];
    product.count = product.count - 1;
    if (product.count === 0) {
      this.removeItem(id);
    } else {
      product.total = product.count * product.price;
      this.setState(() => {
        return { cart: [...tempCart] };
      }, this.addTotals);
    }
  };
  getTotals = () => {
    // const subTotal = this.state.cart
    //   .map(item => item.total)
    //   .reduce((acc, curr) => {
    //     acc = acc + curr;
    //     return acc;
    //   }, 0);
    let subTotal = 0;
    this.state.cart.map(item => (subTotal += item.total));
    const tempTax = subTotal * 0.1;
    const tax = parseFloat(tempTax.toFixed(2));
    const total = subTotal + tax;
    return {
      subTotal,
      tax,
      total
    };
  };
  addTotals = () => {
    const totals = this.getTotals();
    this.setState(
      () => {
        return {
          cartSubTotal: totals.subTotal,
          cartTax: totals.tax,
          cartTotal: totals.total
        };
      },
      () => {
        // console.log(this.state);
      }
    );
  };
  removeItem = id => {
    let tempProducts = [...this.state.products];
    let tempCart = [...this.state.cart];

    const index = tempProducts.indexOf(this.getItem(id));
    let removedProduct = tempProducts[index];
    removedProduct.inCart = false;
    removedProduct.count = 0;
    removedProduct.total = 0;

    tempCart = tempCart.filter(item => {
      return item.id !== id;
    });

    this.setState(() => {
      return {
        cart: [...tempCart],
        products: [...tempProducts]
      };
    }, this.addTotals);
  };
  clearCart = () => {
    this.setState(
      () => {
        return { cart: [] };
      },
      () => {
        this.setProducts();
        this.addTotals();
      }
    );
  };
  render() {
    return (
      <ProductContext.Provider
        value={{
          ...this.state,
          handleDetail: this.handleDetail,
          addToCart: this.addToCart,
          openModal: this.openModal,
          closeModal: this.closeModal,
          increment: this.increment,
          decrement: this.decrement,
          removeItem: this.removeItem,
          clearCart: this.clearCart
        }}
      >
        {this.props.children}
      </ProductContext.Provider>
    );
  }
}

const ProductConsumer = ProductContext.Consumer;

export { ProductProvider, ProductConsumer };

addToCart = id => {
    let tempProducts = [...this.state.products];
    const index = tempProducts.indexOf(this.getItem(id));
    const product = tempProducts[index];
    product.inCart = true;
    product.count = 1;
    const price = product.price;
    product.total = price;

    this.setState(() => {
      return {
        products: [...tempProducts],
        cart: [...this.state.cart, product],
        detailProduct: { ...product }
      };
    }, this.addTotals);
  };

tempProductsにstateのデータ、indexにid情報を、productに商品データを、商品データがカートに入ってるという情報をtrueにし、priceに商品の値段を当てはめます。
this.setState(() => { でproducts、cart、detailProductを使って合計金額を求めています。

他の関数についてはaddToCartを転用したような作りとなっています。

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

create-react-app Advanced Configuration まとめ 2019年末版

VISITS Technologies Advent Calendar 2019 5日目は @overgoro56 が担当します。

create-react-appはejectすればカスタマイズ可能ですがしなくても設定でいろいろできます。
詳細な情報は公式ドキュメントを見れば書いてあるので、ここでは簡単な使い方を紹介します。
一部、勉強のためにcreate-react-appのソースも紹介します。

Advanced Configurationの公式ドキュメント

GitHubのドキュメント

create-react-appページ

設定方法

shellで設定する場合には

package.json
  "scripts": {
    "start": "CI=true react-scripts start",

のようにするか、.envファイルで

.env.development
CI=true

のように環境ごとに設定します。

Advanced Configuration

BROWSER

ブラウザを指定することにより react-scripts start した時に開くブラウザを指定できます。

package.jsonscriptsstart:safari のように用意して、普段はchromeで実行するけど、ブラウザ検証のためSafariで実行したい場合に用意しておくとか。

設定例.
BROWSER=/Applications/Safari.app/Contents/MacOS/Safari

BROWSER_ARGS

BROWSER を設定しているときに渡す引数を設定。

HOST

デフォルトだとdevice上の全てのhostnamesでアクセス可能にするが明示的に指定も可能。

社内の安全なネットワーク以外の環境で開発する場合にHOST=localhostを設定してネットワーク上に公開しないようにしたり。
カフェや外のワーキングスペースで仕事する場合には重要な設定。

PORT

ポートを指定可能。
複数アプリケーションを同時に起動して開発する時に指定したり。

HTTPS

ネットワークに公開する時にHTTPSにしたい時。

PUBLIC_URL

アセットの参照先URLを変えたい時など。

CI

ビルド中の警告をエラーとして扱う。
CIで使うための設定。

REACT_EDITOR

エラーが発生した場合、ブラウザ上のエラーリンクから指定したエディタにジャンプできる。
例えばVisual Studio Codeで開きたい場合には、shellでcodeでVisual Studio Codeを起動できるように設定した上でREACT_EDITOR=codeと設定しておく。

CHOKIDAR_USEPOLLING

VM上で動作させる時にソースの変更を検出してくれない時に設定。

GENERATE_SOURCEMAP

本番環境でユーザーにソースを見られないようにmapファイル生成したくない時に使用。

NODE_PATH

absolute pathでimportする時に使用していた。
最近はjsconfig.jsonまたはtsconfig.jsonbaseUrlを設定することで対応が可能。
baseUrlを設定してない場合には、NODE_PATHを使うようになってるっぽい。

以下のソースで確認できる。

create-react-appのソース

INLINE_RUNTIME_CHUNK

デフォルトだとindex.htmlruntime scriptがインラインで埋め込まれる。
CSPを厳格に行う場合にはこの設定をfalseにすることにより他のスクリプト同様インポートする。

IMAGE_INLINE_SIZE_LIMIT

デフォルト値は10KB。
この値よりも小さいサイズの画像をビルド時にbase64に変換してインラインで埋め込むことにより画像取得のリクエストを削減。
0を設定すると無効化。

ビルド後の結果を比較すると挙動がわかりやすいので確認してみるといい。

EXTEND_ESLINT

デフォルトだとreact-scripts実行時は、.eslintignoreは使わない、baseConfigeslint-config-react-appを使いますが、この設定をtrueにすると.eslintignore.eslintrcを参照するように設定できます。
最近追加された設定です。

追加後に修正・改善が行われているので使う場合にはreact-scriptsのバージョンを3.2.0以降にするのが良さそう。

やってることは以下のソースで確認できる。

create-react-appのソース

参考:eslintの設定

TSC_COMPILE_ON_ERROR

TypeScriptエラーが発生していてもコンパイルできるようにする。
プロトタイプの時はいいかもしれないが、製品開発では使わない方がよいと思う。

まとめ

自分が使っている設定はHOSTPORTCIREACT_EDITORGENERATE_SOURCEMAPEXTEND_ESLINTです。
他は必要が出た時に導入予定です。

いろいろ設定がありますが、その目的を確認したりソースを実際に見てどんなことしてるか理解すると安心して使えると思います。
使ってない設定で便利そうだなと思ったものがあれば是非導入してみてください。

また、create-react-appのソースを見るとejectすると消されてしまうソースもがあることもわかるので直接ソースを見た方が勉強になります。
ここまで確認できれば、あとは必要な設定をcreate-react-appに対してPR出してコミッターの仲間入りもできそうですね!

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

React アプリケーションのボイラープレート CLI を作って使っている話

この記事は ミクシィグループ Advent Calendar 2019 の5日目の記事です。

React で CLI というと create-react-app が有名です。
格好良いベースを作ってくれるのですが個人的には依存 package が多いので、自分用の CLI を作ってそちらを使っています。

@yami-beta/create-ts-app

TypeScript を使ったアプリケーションのベースを作る対話型のインターフェースを持った CLI ツールです。
https://www.npmjs.com/package/@yami-beta/create-ts-app
create-ts-app.gif
意外と色々な package を用意する必要がある ESLint + Prettier の設定を含めていたり、author や LICENSE を設定できます。
(あくまで個人用なので自分の好みによせたボイラープレートになっています)

現在は React のシンプルなボイラープレートしかありませんが

  • React, React Router, Redux 等が含まれた Single Page Application
  • express によるサーバアプリケーション

のボイラープレートを追加していく予定です。

仕組み

この CLI ですが SAO というライブラリを使って実装しています。
create-nuxt-app も SAO を利用していたりします)

以下のようなコードを書くことで対話型のインターフェースを用意したり、テンプレートからファイルをコピーやリネームといったことが出来ます。

module.exports = {
  prompts() {
    return [
      {
        name: 'name',
        message: 'What is the name of the new project',
        default: this.outFolder,
        filter: val => val.toLowerCase()
      }
    ]
  },
  actions: [
    {
      type: 'add',
      files: '**'
    },
    {
      type: "move",
      patterns: {
        "LICENSE_*": "LICENSE"
      }
    }
  ],
  async completed() {
    this.gitInit()
    await this.npmInstall()
    this.showProjectTips()
  }
}

@yami-beta/create-ts-app では このような実装 になっています。
一部を抜粋すると、以下のようにコマンド実行時の回答に応じて package.json に記載する依存関係を編集することも可能です。

const config = {
  actions() {
    const { answers } = this;
    return [
      // 略
      {
        type: "modify",
        files: "package.json",
        handler(data: any, filepath: string) {
          return {
            name: answers.name || data.name,
            version: answers.version || data.version,
            main: data.main,
            author: answers.author,
            license: answers.license || data.license,
            scripts: data.scripts,
            dependencies: {
              ...data.dependencies
            },
            devDependencies: {
              ...data.devDependencies,
              "@typescript-eslint/eslint-plugin": answers.features.includes(
                "eslint"
              )
                ? data.devDependencies["@typescript-eslint/eslint-plugin"]
                : undefined,
              "@typescript-eslint/parser": answers.features.includes("eslint")
                ? data.devDependencies["@typescript-eslint/parser"]
                : undefined,
              eslint: answers.features.includes("eslint")
                ? data.devDependencies["eslint"]
                : undefined,
              "eslint-config-prettier":
                answers.features.includes("eslint") &&
                answers.features.includes("prettier")
                  ? data.devDependencies["eslint-config-prettier"]
                  : undefined,
              "eslint-plugin-prettier":
                answers.features.includes("eslint") &&
                answers.features.includes("prettier")
                  ? data.devDependencies["eslint-plugin-prettier"]
                  : undefined,
              prettier: answers.features.includes("prettier")
                ? data.devDependencies["prettier"]
                : undefined
            }
          };
        }
      },
      // 略
    ].filter(Boolean);
  }
};

CLI を作るほどでもない場合

ボイラープレートは欲しいけれども CLI を作るほどでは無い、という場合もあるかと思います。
そういう場合は GitHub のテンプレートリポジトリでボイラープレートを活用する方法があります。

詳細は上記のドキュメントを参照してください。

まとめ

  • React アプリケーションのボイラープレートを生成する CLI を作っている
    • テンプレートからファイルをコピー、リネーム、編集することが出来るので複数のボイラープレート生成が可能
  • 手軽にボイラープレートを作る場合は GitHub のテンプレートリポジトリが活用出来そう

備考

  • SAO という見覚えのある名前ですが egoist 氏のライブラリです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

妥協しないTypescript

テックタッチアドベントカレンダー5日目を担当する@takakobemです。
4日目は @terunuma による 4Kモニタ環境で1年間Web開発してみた所感 でした。
4Kモニタいいですよね。ただ、以前私は43型の4Kモニタを使っていたのですが、正直でかすぎて画面の端が見づらかったです。。30インチぐらいがベストかもしれませんね。

まえがき

本記事はTypescriptを触ったことがある人を対象にしています。
「Typescriptは使っているけど、ちゃんと使いこなせているかわからない」という人に一番しっくりくる内容だと思います。
私が以前C++を触っていたこともあり、結構C++を引き合いに出しています。
また、私がReactやReduxを触っていることもあり、オブジェクト指向よりも関数型プログラミングを意識したものになっています。

はじめに

Typescriptは非常に便利なツールです。
Javascriptそれ自体は静的な型チェックができないため、コーディング中に実装ミスに気づきにくいのですが、Typescriptを導入することで型チェックが効くようになり、VSCodeなどのエディターを使えば補完も利用できるようになります。
ただ、TypescriptはC++やJavaなどといった言語とは違い、型の定義が中途半端でも動いてしまいます
一見型をしっかりつけているつもりで書いていても、ちょっとしたことで信頼性がないコードができあがってしまいます。
今回は型を妥協してしまっている書き方Badケースとして例に上げ、どうすればよいかをGoodケースとして紹介していきます。

※本記事はTypescript3.7時点のものです。

1. anyは使わない

Typescriptを触ったことがある人であれば、anyは知っているかと思います。
極端に言ってしまうと、anyは型を付けるのを放棄したのと同義です。
Typescript は Javascript に型を付けるための言語なのに、型を付けるのを放棄してしまっては元も子もありません。
Typescript で型を付けることを選んだのであれば、anyを使うことは避けましょう。

Bad :cry:

numberを引数に取りたいのに、anyにしてしまうとstring型でも通ってしまいます。

declare function hoge(num: any): void
hoge(1) // ok
hoge("hoge") // これもokになってしまう

Good :smile:

ちゃんと型を定義してあげましょう。

declare function fuga(num: number): void
fuga(1) // ok
fuga("fuga") // error

2. !は使わない

!non-null assertion operatorと呼ばれています。
これをつけることで、nullかもしれないようなものを強制的にnullじゃないものとしてみなすことができます。
「ここはnullはありえないはずだから〜」という理由でよく使いがちなのですが、ありえないと思ってしまっているだけかもしれませんし、今後nullが入ってしまっても気づけなくなりますよね。
なので、そもそもnullをとらないようにしてしまうか、ちゃんと実行時にチェックをするべきです。
Typescript3.7からの機能であるOptional Chainingを使うのもありです。

Bad :cry:

function hoge(str: string | null) {
  str.toUpperCase() // Object is possibly 'null'.
  str!.toUpperCase() // ok
}

Good :smile:

ちゃんと実行時にチェックしてあげましょう。
もしくはnullをとらないようにしてあげましょう。

function hoge(str: string | null) {
  if (str !== null) {
    str.toUpperCase() // ok
  }
}

// or

function fuga(str: string | null) {
  str?.toUpperCase() // ok
}

// or

function fuga(str: string) {
  str.toUpperCase() // ok
}

3. {}を使う時は注意

例えばkeyに名前を持ち、valueに年齢を持つようなオブジェクトを定義したいとします。
keyは名前だからstring, valueは年齢だからnumberと安直にやってしまうと、型安全が崩れてしまいます。

Bad :cry:

安直に上記の通りに定義してみます。

const ageData: Record<string, number> = {
  yamada: 10,
  tanaka: 20
}
console.log("山田さんの年齢=", ageData.yamada)  // 10
console.log("田中さんの年齢=", ageData.tanaka)  // 20
console.log("鈴木さんの年齢=", ageData.suzuki)  // undefined

この時、山田さんのデータは入っていますが、鈴木さんのデータは入っていないため、鈴木さんの年齢はundefinedとして返ってきます。
しかし、Typescript的にはエラーと認識してくれません。

Good :smile:

keyが予め予測可能なものであれば、keyを定義しておきましょう。

const ageData: Record<"yamada" | "tanaka", number> = {
  yamada: 10,
  tanaka: 20
}
console.log("鈴木さんの年齢=", ageData.suzuki)  // Property 'suzuki' does not exist on type 'Record<"yamada" | "tanaka", number>'.(2339)

idなど、keyが事前に定義不能な場合は、valueにundefinedも定義しておきましょう。

const ageData: Record<string, number | undefined> = {
  yamada_id: 10,
  tanaka_id: 20
}

console.log("鈴木さんの年齢=", ageData.suzuki + 1) // Object is possibly 'undefined'. 

4. asは使わない

以下のような定義があったとしましょう。

type Base = {
  x: number
}

type Derived = Base & {
  y: number
}

これは、オブジェクト指向でいう基底クラスと派生クラスの関係に近いです。
Typescript では、このように Intersection Types を使うことで継承のようなものを表現することができます。
ここで問題なのが、asを使うことでこれら2つの型が双方向にキャストできてしまうということです。

Bad :cry:

例えば、以下のようなアップキャストは問題なく行なえます。

const derived: Derived = {
    x: 1,
    y: 2
}
const base = derived as Base
console.log(base.x) // 1

元のderivedにはxが含まれているので何も問題ありませんね。
次はダウンキャストです。

const base: Base = {
    x: 1,
}
const derived = base as Derived // ダウンキャストができてしまう
console.log(derived.y) // undefined

baseにはyが無いのに、エラーなく実行されてしまいます。
asを使うとダウンキャストがすんなりできてしまいます

Good :smile:

ではどうすればよいのか。
いえ、どうもしなくていいんです。
そもそもTypescriptが提供するものはただの型です。
継承といっても、カプセル化や関数のオーバーライドなどはありません。
そもそもキャストする必要がないのです。

ただ、引数に基底クラスを受け、条件に応じて派生クラスに変換し結果を返したいようなケースはありますよね。それを次に説明します。

5. Union Types + String Literalを使いこなす

以下のような、CircleとSquareという型があるとします。

type Circle = {
  radius: number
}

type Square = {
  height: number
  width: number
}

これはどちらも図形ですね。Typescriptでは以下のようにUnion Typesを使うことで、異なる2つの型を1つの型として扱うことができます。

type Figure = Circle | Square

これは、派生クラスと派生クラスから基底クラスを作るようなものです。
C++から入った僕は非常に理解に苦しみました。
Typescript恐ろしい。。

Bad :cry:

このようにすることで、例えば以下のように図形を引数にその面積を返すような関数が定義できます。

function area(figure: Figure): number {
  if ("height" in figure) {
    return figure.width * figure.height
  }
  if ("radius" in figure) {
    return figure.radius * figure.radius * Math.PI
  }
  throw new Error("no such figure")
}

あれ?なんか奇妙ですよね。
そう、上記の例では、図形に特定のプロパティがあるかどうかで図形を判定し、面積を計算しています。
でもこれって気持ち悪いですよね。例えばTriangleという型が追加になると、Triangleheightを持つので、条件式を見直さなければならなくなります。
「クラスを使ってメンバ関数で計算しろよ」って言われそうですが、クラスにしなくてもオブジェクトにちょこっと細工することで解決できちゃうんです。
(なぜクラスを頑なに使わないのか、React/Redux使いの人なら知ってるかと思いますが、その理由はまたいつか機会があれば記事書きます。)

Good :smile:

ではどうするかというと、こういう時は その型が何かstring literal で定義しておけばよいです。

type Circle = {
  kind: "circle"
  radius: number
}

type Square = {
  kind: "square"
  height: number
  width: number
}

こうすることで、プロパティの存在によってif文を分けなくても、kindというkeyによってswitch文で処理を分けることができるようになります。
また、例えば case "circle" 内で figure.width にアクセスしようとするとエラーになります。

function area(figure: Figure): number {
  switch (figure.kind) {
    case "circle":
      // figure.width // Property 'width' does not exist on type 'Circle'.
      return figure.radius * figure.radius * Math.PI
    case "square":
      return figure.height * figure.width
  }
}

Typescript賢すぎる!

最後に

いかがでしたでしょうか。
Typescriptは非常に便利なツールですが、使いこなすにはいくつかコツがいりますし、正直難しいです。
今回挙げた内容もTypescriptができることのほんの一部に過ぎません。
しかし、使いこなせれば非常に便利なツールです。こんなことできるかな?と思ったことが大抵できますので、是非いろいろ試してみてください。

6日目は @ihiroky による「JSXとvirtual-domで遊ぶ」です。

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

個人開発したサイトを見直し、リニューアルすることで技術を得る

本記事は個人開発 Advent Calendar 2019 4日目の記事です。

概要

普段は企業でビッグデータ×マーケティングでAWSメインにフルスタック寄りのエンジニアをしています。ですので、本記事で言う個人開発は趣味の日曜開発といった度合が強いのですが、業務外の開発によってこそやりやすいインプットとアウトプットがある、というポエムとなります。

背景

昨年書かせていただいた 個人ファンサイトは、無事安定して稼働しています。応援先グループも稼働しています(これは幸せな事です)。

しかしながら
・インフラ構築が2017年当初なので見直したい
・フロントエンドが簡素なので見直したい
・応援先グループの新たな取り組みに最適化したサイトにしたい
といった、自分の作ったものに対する課題感と解決モチベーションが沸いてくる訳です。

インフラの見直し

リージョンの見直し

2017年初頭に構築した基盤に使っている Amazon Lightsailがバージニアリージョンで稼働していました。これは当時東京リージョンがまだ無かったためです。当時としては割と新しいサービスを試していたのですね。

ネットワークの見直し

ふと見ていると「静的IPを作成」というボタンが。
なんと静的IPじゃありませんでした。。。

SSL対応

httpでした:punch_tone2:
2017年に作ったんなら当時の時点でやれよという話は分かりつつ先延ばしにしてしましまいたが、何かを応援しているサイトがhttpsで無いというのは、応援先にも悪影響を及ぼしかねません。直さねば! 応援先グループの公式サイトはSSL非対応ですが。

フロントエンドの見直し

利用技術とデザイン面

必要最低限で軽快に動く、ことを目指してはいたものの、ある意味でデザイン面からの逃げ(私はBootstrapすら使えない子)でもありました。それとjQuery。

また、いわゆる今風のUI/UXにチャレンジしてみたいと考えました。

丁度、社内で若手がReactを使って素敵UIを生み出しているのを見ておりました。私は広めに業務に関わっていますが、フロントエンドが一番離れている場所でした。ここらでキャッチアップしたい。

そうだ、Reactしよう。

※Vueと迷いましたが、社内でReact活用が進む流れもあり、そこは乗っかりました。

やるぞ

昨年の状態

qiita_20191203.jpg

まずは構成を思い出すところから始めました。思い出し工数というやつです。

インフラ見直し

Amazon Lightsail を東京リージョンに変更

・・・ボタン1つでは出来ないですよね。はい。
あとUbuntuを使っていたのですが、やっぱAmazon Linuxの方が好き、、、となったので、この機会に東京リージョン&Amazon Linuxで作成し直すこととしました。

移植作業

ディストリビューションが違うもので作成し直すということは、色々動かない可能性もあります、というか動きません。API連携も結構やってます。

しかし
qiita_20191203_a.jpg

図の通り部分を整頓していくとGoogle スプレッドシートへの書き込み処理のみ気を付ければ、むしろ別インスタンスを作成するのであれば、移行時のテスト実行にリスクが無いことが分かります。まさか昨年、図に描いておいたことがここで役に立つとは。

そのほか、RDBMSなども無くリアルタイムで更新されるトランザクションデータが無いことも大きいですね。
新たに稼働させたインスタンスに、S3に控えておいた主なリソースを移し、主にShellScriptのあたりが雑だったので書き換え、ひたすら適用していくだけです。

といった感じで、思っていたよりもサクサク移行できました。
サクサクと言えば、東京リージョンで立ち上げてSSH接続時の操作もサクサクになりました。
普段本業ではEC2等は東京リージョンしか使っていないのですが、あれが海外リージョンのレイテンシだったのでしょうか。

静的IP付与

EC2の場合だと、Elastic IPを発行して、、、と簡単ですが、Lightsailの場合は更に簡単で、管理画面からポチポチと数クリックで発行されました。ようやく恩恵に預かりました。
発行されたIPアドレスをRoute53へ反映。

しかしよく今まで大丈夫だったな・・・

SSL対応

ZeroSSLを用いてSSL対応をしました。
マジでブラウザだけで証明書の発行が完了してびっくりしました。
このあたりも、業務では中々使えていないので良い体験になりました。
(なお、この記事を書いている時点で期間が3ヵ月なので処置が必要なことに気付く。)

フロントエンド見直し

要件:応援先グループの活動の変化

・メンバーがそれぞれ個人チャンネルを持った
・ゲストメンバーという概念が出現
・動画が変わらぬペースで増え続けている(余裕の1,000件突破)

ようは情報量が1年前に比べて更に増えて、単純に表示、だけでは物足りなくなった。

やりたいこと(ざっくり)

・メンバーに紐づく情報が公式サイトプロフィール、個人チャンネル、Twitterアカウントと複数存在したため、プロフィール的なものを画面の邪魔にならない形で表示させたい
・とにかく今風にしたい

やってみた(ざっくり)

・全くReact分からん
・とりあえずひたすらReact入門記事を読む。
・直感でMaterial-UIに絞った
・そのうちサンプルを扱えるようになる
・あれもこれも出来るようになる

結果として、以下のComponentsを利用して
- Popover
- Avatar
- Paper
- Expansion Panel
- Table(material-table)

以下のようなことを実現できた
・出演メンバーの可視化
 → Avatarを用いて、アイコンで誰が出演しているかを賑やかな感じで可視化
・増えた情報の出し入れ
 → 動画の収集元が7チャンネルになったのでそれぞれのExpansion Panelで表示
 → メンバーが4人+ゲスト3人構成になったのでExpansion Panelで見やすく表示
・動画サムネイル表示
 → マウスカーソルをあてることでサムネイルを表示することで解決。ちゃんとカーソルにあたったタイミングで読み込みに行く。

この期に及んでスクリーンショットすらも載せませんが、Material-UIを使っているので綺麗です。
フレームワークなのだからそうやろという話ではありますが、サンプルをもとにデザイン+機能が簡単確実に実装出来た体験は、かなりの衝撃でした。
qiita_20191203_c.jpg

まとめ、得られたものなど

サクサクなSSH環境

 リージョンでここまで違うとは。日曜開発の環境がサクサクになるのは良い事です。

フロントエンド事情に少し追いつけた

 実際触ってみるまで、Reactがどういったものか分かりませんでした。今のフロントエンド開発はこういうことになっているんだ、と手遅れになる前に追いつけた感じが良かったというか、やらねばヤバかったなと思いました。
 やらねばヤバかったなと思う一方、通常の業務をやっているとこのキャッチアップは出来なかったなぁ、、、というのが今回の個人開発サイトリニューアルで感じた一番のポイントです。

 もちろん業務においてもフロントエンジニアとの会話がしやすくなる、といったメリットもあるのですが自身の技術レベルを少しアップ出来たのが何より大きかったです。

業務と個人開発の違いとモチベーション

自社のお仕事の場合、どうしてもコスト、スピード、信頼性を考慮して最適解というものを選んでしまいます。新しいものが歓迎される文化もある一方で、費用対効果が明確だったら私は費用対効果が明確な手段を優先してしまいます。
では個人開発をしまくれば良いかと言うと、業務も普通に楽しい中で更にインプットとアウトプットを増やすのは私には結構困難です。
と考えた時に「趣味を利用した強烈なモチベーションを軸に、使ってみたい技術領域を試そう」といった考えで今回リニューアルをやってみました。結果、良いインプットとアウトプットが得られたと感じています。

今後の展望

個人開発サイトを、よりサーバーレスで完結するような世界にしてゆきたいです。
- AWS Amplify
- Lambda(RPA部分)
とか。re:Inventにも期待。

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

個人開発しているサイトをリニューアルした話と得られたもの

本記事は個人開発 Advent Calendar 2019 4日目の記事です。

概要

普段は企業でビッグデータ×マーケティングでAWSメインにフルスタック寄りのエンジニアをしています。ですので、本記事で言う個人開発は趣味の日曜開発といった度合が強いのですが、業務外の開発によってこそやりやすいインプットとアウトプットがある、というポエムとなります。

背景

昨年書かせていただいた 個人ファンサイトは、無事安定して稼働しています。応援先グループも稼働しています(これは幸せな事です)。

しかしながら
・インフラ構築が2017年当初なので見直したい
・フロントエンドが簡素なので見直したい
・応援先グループの新たな取り組みに最適化したサイトにしたい
といった、自分の作ったものに対する課題感と解決モチベーションが沸いてくる訳です。

インフラの見直し

リージョンの見直し

2017年初頭に構築した基盤に使っている Amazon Lightsailがバージニアリージョンで稼働していました。これは当時東京リージョンがまだ無かったためです。当時としては割と新しいサービスを試していたのですね。

ネットワークの見直し

ふと見ていると「静的IPを作成」というボタンが。
なんと静的IPじゃありませんでした。。。

SSL対応

httpでした:punch_tone2:
2017年に作ったんなら当時の時点でやれよという話は分かりつつ先延ばしにしてしましまいたが、何かを応援しているサイトがhttpsで無いというのは、応援先にも悪影響を及ぼしかねません。直さねば! 応援先グループの公式サイトはSSL非対応ですが。

フロントエンドの見直し

利用技術とデザイン面

必要最低限で軽快に動く、ことを目指してはいたものの、ある意味でデザイン面からの逃げ(私はBootstrapすら使えない子)でもありました。それとjQuery。

また、いわゆる今風のUI/UXにチャレンジしてみたいと考えました。

丁度、社内で若手がReactを使って素敵UIを生み出しているのを見ておりました。私は広めに業務に関わっていますが、フロントエンドが一番離れている場所でした。ここらでキャッチアップしたい。

そうだ、Reactしよう。

※Vueと迷いましたが、社内でReact活用が進む流れもあり、そこは乗っかりました。

やるぞ

昨年の状態

qiita_20191203.jpg

まずは構成を思い出すところから始めました。思い出し工数というやつです。

インフラ見直し

Amazon Lightsail を東京リージョンに変更

・・・ボタン1つでは出来ないですよね。はい。
あとUbuntuを使っていたのですが、やっぱAmazon Linuxの方が好き、、、となったので、この機会に東京リージョン&Amazon Linuxで作成し直すこととしました。

移植作業

ディストリビューションが違うもので作成し直すということは、色々動かない可能性もあります、というか動きません。API連携も結構やってます。

しかし
qiita_20191203_a.jpg

図の通り部分を整頓していくとGoogle スプレッドシートへの書き込み処理のみ気を付ければ、むしろ別インスタンスを作成するのであれば、移行時のテスト実行にリスクが無いことが分かります。まさか昨年、図に描いておいたことがここで役に立つとは。

そのほか、RDBMSなども無くリアルタイムで更新されるトランザクションデータが無いことも大きいですね。
新たに稼働させたインスタンスに、S3に控えておいた主なリソースを移し、主にShellScriptのあたりが雑だったので書き換え、ひたすら適用していくだけです。

といった感じで、思っていたよりもサクサク移行できました。
サクサクと言えば、東京リージョンで立ち上げてSSH接続時の操作もサクサクになりました。
普段本業ではEC2等は東京リージョンしか使っていないのですが、あれが海外リージョンのレイテンシだったのでしょうか。

静的IP付与

EC2の場合だと、Elastic IPを発行して、、、と簡単ですが、Lightsailの場合は更に簡単で、管理画面からポチポチと数クリックで発行されました。ようやく恩恵に預かりました。
発行されたIPアドレスをRoute53へ反映。

しかしよく今まで大丈夫だったな・・・

SSL対応

ZeroSSLを用いてSSL対応をしました。
マジでブラウザだけで証明書の発行が完了してびっくりしました。
このあたりも、業務では中々使えていないので良い体験になりました。
(なお、この記事を書いている時点で期間が3ヵ月なので処置が必要なことに気付く。)

フロントエンド見直し

要件:応援先グループの活動の変化

・メンバーがそれぞれ個人チャンネルを持った
・ゲストメンバーという概念が出現
・動画が変わらぬペースで増え続けている(余裕の1,000件突破)

ようは情報量が1年前に比べて更に増えて、単純に表示、だけでは物足りなくなった。

やりたいこと(ざっくり)

・メンバーに紐づく情報が公式サイトプロフィール、個人チャンネル、Twitterアカウントと複数存在したため、プロフィール的なものを画面の邪魔にならない形で表示させたい
・とにかく今風にしたい

やってみた(ざっくり)

・全くReact分からん
・とりあえずひたすらReact入門記事を読む。
・直感でMaterial-UIに絞った
・そのうちサンプルを扱えるようになる
・あれもこれも出来るようになる

結果として、以下のComponentsを利用して
- Popover
- Avatar
- Paper
- Expansion Panel
- Table(material-table)

以下のようなことを実現できた
・出演メンバーの可視化
 → Avatarを用いて、アイコンで誰が出演しているかを賑やかな感じで可視化
・増えた情報の出し入れ
 → 動画の収集元が7チャンネルになったのでそれぞれのExpansion Panelで表示
 → メンバーが4人+ゲスト3人構成になったのでExpansion Panelで見やすく表示
・動画サムネイル表示
 → マウスカーソルをあてることでサムネイルを表示することで解決。ちゃんとカーソルにあたったタイミングで読み込みに行く。

この期に及んでスクリーンショットすらも載せませんが、Material-UIを使っているので綺麗です。
フレームワークなのだからそうやろという話ではありますが、サンプルをもとにデザイン+機能が簡単確実に実装出来た体験は、かなりの衝撃でした。
qiita_20191203_c.jpg

まとめ、得られたものなど

サクサクなSSH環境

 リージョンでここまで違うとは。日曜開発の環境がサクサクになるのは良い事です。

フロントエンド事情に少し追いつけた

 実際触ってみるまで、Reactがどういったものか分かりませんでした。今のフロントエンド開発はこういうことになっているんだ、と手遅れになる前に追いつけた感じが良かったというか、やらねばヤバかったなと思いました。
 やらねばヤバかったなと思う一方、通常の業務をやっているとこのキャッチアップは出来なかったなぁ、、、というのが今回の個人開発サイトリニューアルで感じた一番のポイントです。

 もちろん業務においてもフロントエンジニアとの会話がしやすくなる、といったメリットもあるのですが自身の技術レベルを少しアップ出来たのが何より大きかったです。

業務と個人開発の違いとモチベーション

自社のお仕事の場合、どうしてもコスト、スピード、信頼性を考慮して最適解というものを選んでしまいます。新しいものが歓迎される文化もある一方で、費用対効果が明確だったら私は費用対効果が明確な手段を優先してしまいます。
では個人開発をしまくれば良いかと言うと、業務も普通に楽しい中で更にインプットとアウトプットを増やすのは私には結構困難です。
と考えた時に「趣味を利用した強烈なモチベーションを軸に、使ってみたい技術領域を試そう」といった考えで今回リニューアルをやってみました。結果、良いインプットとアウトプットが得られたと感じています。

今後の展望

個人開発サイトを、よりサーバーレスで完結するような世界にしてゆきたいです。
- AWS Amplify
- Lambda(RPA部分)
とか。re:Inventにも期待。

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

react-hooks-workerの紹介

はじめに

React Hooksを使うと、非同期処理を比較的簡単に書くことができます。つまり、async/awaitをhooks内に隠蔽することができます。Web Workerを手軽に利用する有名なライブラリとしてcomlinkがありますが、WebWorkerとの通信は非同期であるためawaitをつけなければいけません。そこで、より手軽に扱えるReact Hooks専用のWeb Workerラッパーを紹介します。

react-hooks-worker

リポジトリはこちらです。

https://github.com/dai-shi/react-hooks-worker

このライブラリでは、Web Workerとの通信をstateを介して行うため、async/awaitをあまり意識する必要がないことが特徴です。

Web Workerを手軽に使うには、bundlerのサポートが重要です。各種プラグイン等がありますが、今回使用したのはwebpackのworker-pluginです。これにより、workerでも外部ライブラリが使えるようになります(本記事での使用例の紹介は無し)。

使用例

フィボナッチ数を計算する例を紹介します。まずは、workerの実装です。

// slow_fib.worker.js:

import { exposeWorker } from 'react-hooks-worker';

const fib = i => (i <= 1 ? i : fib(i - 1) + fib(i - 2));

exposeWorker(fib);

次に、これを利用するReactコンポーネントの実装です。

// app.js:

import React from 'react';
import { useWorker } from 'react-hooks-worker';

const createWorker = () => new Worker('./slow_fib.worker', { type: 'module' });

const CalcFib = ({ count }) => {
  const { result, error } = useWorker(createWorker, count);
  if (error) return <div>Error: {error}</div>;
  return <div>Result: {result}</div>;
};

const App = () => (
  <div>
    <CalcFib count={5} />
  </div>
);

これだけで、workerが使えるようになります。重い計算処理(かつ、結果が小さい場合)は、どんどんworkerにoffloadしましょう。

発展的な使い方

上記の例ではworkerの実装は単純な関数でしたが、実は、非同期関数やgeneratorでも動きます。列挙すると使えるパターンは下記になります。

  • sync function
  • async function
  • sync generator function
  • async generator function

参考までに、async generatorでフィボナッチ数の計算過程をゆっくりと出力するworker関数を載せます。

// fib-steps.worker.js

import { exposeWorker } from 'react-hooks-worker';

async function* fib(x) {
  let x1 = 0;
  let x2 = 1;
  let i = 0;
  while (i < x) {
    yield `(calculating...) ${x1}`;
    await new Promise(r => setTimeout(r, 100));
    [x1, x2] = [x2, x1 + x2];
    i += 1;
  }
  yield x1;
}

exposeWorker(fib);

workerの処理をプログレッシブに表示する場合などにこのパターンが使えるのではないでしょうか。

おわりに

本ライブラリではworkerは関数として表現されますが、comlinkは様々なオブジェクトをサポートしています。よく紹介されるのはworkerをclassとして実装する例ですが、このパターンを好む人がいることを知りました。react-hooks-workerでも様々なパターンをサポートすることが今後の改題の一つになりそうです。

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

useStateとuseReducerの関係(どっちが強力?同じ?わずかな違い?決定的な違い?)

はじめに

React HooksのuseStateとuseReducerに関する小ネタです。

useStateはuseReducerで実装されている

内部実装ではuseStateはuseReducerで実装されていると、どこかに書いてありました。こちらのブログ記事にuserlandでの実装例が載っています。

const stateReducer = (prevState, newState) =>
  typeof newState === 'function' ? newState(prevState) : newState;

const stateInitializer = initialValue =>
  typeof initialValue === 'function' ? initialValue() : initialValue;

const useState = initialValue =>
  useReducer(stateReducer, initialValue, stateInitializer);

こんな感じになります。

つまり、useStateでできることは全てuseReducerでもできるということです。では、useReducerだけでしかできないことはあるのでしょうか。

useReducerはuseStateでも実装できる

実は、useReducerはuseStateでも実装できます。こちらのブログ記事にuserlandでの実装例があります。

const useReducer = (reducer, initialArg, init) => {
  const [state, setState] = useState(
    init ? () => init(initialArg) : initialArg,
  );
  const dispatch = useCallback(
    action => setState(prev => reducer(prev, action)),
    [reducer],
  );
  return useMemo(() => [state, dispatch], [state, dispatch]);
};

こんな感じになります。つまり、機能的には同等ということになります。

実はわずかな違いがある

こちらのツイートにあるように、useReducerではinitArgとinitが分離されているため、initをpropsに依存しないように書くことができます。これにより、hookの外側で関数定義することもできますし、inline関数で書いたとしてもJavaScriptのランタイムエンジンで最適化される可能性が高いです。

決定的な違いもあるにはある

お勧めしませんが、useReducerには非常に特殊な利用法もあります。それはreducerをコンポーネント内で定義できることです。reducerをコンポーネント内で定義すると未来のpropsを読み込めちゃいます。"cheat mode"と呼ばれたりします。

詳細は、こちらのブログ記事のセクションをご参照ください。

追記(12/4)

このcheat modeは上記のuserland実装では再現できていないですね。

おわりに

小ネタにしようかと思っていましたが、書き始めたらマニアックなネタになってしまいました。楽しんでくれた方がいらしたら幸いです。

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

Next.jsを使用してGoogle Chromeの拡張機能を作るとレイアウトとルーティングができるので結構楽に拡張の開発ができる。

はじめに

@yushimatenjinです。Next.jsアドベントカレンダー4日目ですね。
Next.jsを使うと簡単にGoogle Chromeの拡張機能が作れるんじゃないかなと思ったら簡単に作ることが出来たのでその紹介を今回しようかなと思います。

このときに得た知見をもとに書いています。

Chrome ExtensionをNext.jsで作ってみた

こちらが先日作成した、Next.jsで作ったChrome拡張を作るためのリポジトリです。
https://github.com/yushimatenjin/next-js-chrome-extension-starterkit

Next.jsのはじめ方

Next.jsを始めるには、Reactの使える環境がある方でしたら簡単に始められます。

1. 必要なパッケージをインストール

小さい構成で始めるにはこれだけで大丈夫です。

yarn init
yarn add react react-dom next

2.Pagesディレクトリを作る

mkdir pages

pages/index.jsを作成 & 書き換える

pages/index.js
const React from 'react'

export default () => <div>Hello, World!</div>

これだけ書くだけで/にアクセスしたときにHello, Worldを表示するページを作ることが出来ます。

.
├── package.json
├── pages
│   └── index.js
└── yarn.lock

起動をするためのスクリプトをpackage.jsonに追加する

package.json
  ...

   scripts{
      "dev": "next",
      "start": "next start",
      "build": "next build",
      "export": "next build && next export"
   }
  ...

起動する

index.jsの内容を表示するためには先程設定したyarn devのコマンドを打つことでlocalhost:3000にアクセスできるようになります。
ScreenShot 23.jpg

yarn dev

これがNext.jsで最小限に起動をする方法になります。開発環境で起動する場合にはyarn dev、ビルド&起動する場合にはyarn build && yarn startでサーバーが立ち上がります。

ChromeのExtensionを作る場合にはexportを使用して静的なページとして出力をします。

Chrome Extensionを作る

ChromeのExtensionを作るためにいくつのファイルを追加&変更します。

追加するフォルダ & 追加するファイル

  1. extensions // フォルダ
  2. extensions/manifest.json // 拡張機能の詳細を記述するファイル

manifest.jsonはこちらを参考にして記述をしていきます。

manifest.json
{
    "name": "extension",
    "description": "extension",
    "version": "1.0.0",
    "browser_action": {
      "default_popup": "./dist/index.html"
    },
    "permissions": [
      "bookmarks",
      "tabs",
      "activeTab"
    ],
    "manifest_version": 2
  }

変更するファイル

  1. package.json
変更前
package.json
   ....
  "scripts": {
    ...
    "export": "next build && next export"
  },
   ...
変更後

export-oオプションを付けて拡張機能の出力先を変更します。

package.json
   ....
  "scripts": {
    ...
    "export": "next build && next export -o extensions/dist"
  },
   ...

この3つの新しく変更を加えた状態の現在のディレクトリ構成はこのような状態となっております。

.
├── README.md
├── extensions
│   ├── dist
│   └── manifest.json
├── package.json
├── pages
│   └── index.js
└── yarn.lock

ちなみに上記の設定を行ったものがこのリポジトリとなりますので、このリポジトリをクローンをするとすぐに開発をし始めることが出来ます。

next-js-chrome-extension-starterkit

こちらのリポジトリはtsxファイルを追加したため少しファイルの構成が増えています。Next.jsではTypeScriptを扱おうとするとある程度必要なファイルを自動的にファイルを追加してくれます。

Chromeの拡張としてエクスポート

ScreenShot 23.jpg

この状態で起動をすると、白い背景にHello, World!と表示されていると思います。この状態のまま拡張にしてみましょう。

yarn export 

出力先をextension/distにpackage.jsonで設定しているので、静的なファイルが生成されます。

エクスポートされた拡張を読み込む

ScreenShot 26.jpg

  1. エクスポートされたChromeの拡張機能を読み込むためにはGoogle Chromeの設定 → その他ツール拡張機能を選択

ScreenShot 28.jpg

  1. パッケージ化されていない拡張を読み込む

ScreenShot 27.jpg

  1. プロジェクト → extensions ファイルを選択します

これで拡張機能を読み込むことが出来ます。

Chrome拡張を作ることが出来た(やったー!)

ScreenShot 30.jpg

ルーティングを追加する

Next.jsの特徴として静的サイトのルーティングを簡単に作れるので、新しいページを追加してみます。

Next.jsのLinkは可能ですが、URLの指定を.htmlまで記述する必要があります。開発環境では必要ないのでページのパスを取得するための関数を追加します。

constants/page.js
const Pages = () => {
    const suffix = process.env.NODE_ENV === "development" ? "" : ".html"
    const data = {
        index: `index${suffix}`,
        about: `about${suffix}`
    }
    return data
}

export default Pages

トップページから飛ぶ先のページ名aboutを追加します。

pages/about.js
import React from "react";
import Link from "next/link";
import Page from "../constants/page";

const About = () => {
  return (
    <div>
      このExtension@mxcn3が作成しました。
      <Link href={Page().index}>
        <a>トップページへ</a>
      </Link>
    </div>
  );
};

export default About;

トップページからページ名 aboutに飛べるようにします。

pages/index.js
import React from "react";
import Link from "next/link";
import Page from "../constants/page";

const Index = () => {
  return (
    <div>
      Hello World
      <Link href={Page().about}>
        <a>この拡張について</a>
      </Link>
    </div>
  );
};

export default Index;

yarn export

を再度実行することで拡張機能が更新されます。

output.gif

ページのサイズを変更する

ページのサイズをインラインで変更します。

あまり推奨されていないやり方みたいですが便利上インラインでスタイを変更します。
https://reactjs.org/docs/dom-elements.html#style

pages/index.js
import React from "react";
import Link from "next/link";
import Page from "../constants/page";

const Index = () => {
  return (
    //   ページの大きさを変更
    <div
      style={{
        width: 400,
        height: 400
      }}
    >
      Hello World
      <Link href={Page().about}>
        <a>この拡張について</a>
      </Link>
      {/* iframeを追加 */}
      <iframe
        style={{
          width: "100%",
          height: "100%",
          margin: 0,
          border: "none"
        }}
        src="https://playcanv.as/p/8NZ92jAY/"
      />
    </div>
  );
};

export default Index;

output2

このような形で表示されます。

まとめ

Next.jsはReactを使用して記述ができるので、Next.jsでGoogle Chromeの拡張機能を書くことができれば比較的作りやすいのではないでしょうか。Next.jsを使用したChrome拡張の記事などをあまり見なかったので記事にさせていただきました。

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

Next.jsを使用してGoogel Chromeの拡張機能を作るとレイアウトとルーティングができるので結構楽に拡張の開発ができる。

はじめに

@yushimatenjinです。Next.jsアドベントカレンダー4日目ですね。
Next.jsを使うと簡単にGoogle Chromeの拡張機能が作れるんじゃないかなと思ったら簡単に作ることが出来たのでその紹介を今回しようかなと思います。

このときに得た知見をもとに書いています。

Chrome ExtensionをNext.jsで作ってみた

こちらが先日作成した、Next.jsで作ったChrome拡張を作るためのリポジトリです。
https://github.com/yushimatenjin/next-js-chrome-extension-starterkit

Next.jsのはじめ方

Next.jsを始めるには、Reactの使える環境がある方でしたら簡単に始められます。

1. 必要なパッケージをインストール

小さい構成で始めるにはこれだけで大丈夫です。

yarn init
yarn add react react-dom next

2.Pagesディレクトリを作る

mkdir pages

pages/index.jsを作成 & 書き換える

pages/index.js
const React from 'react'

export default () => <div>Hello, World!</div>

これだけ書くだけで/にアクセスしたときにHello, Worldを表示するページを作ることが出来ます。

.
├── package.json
├── pages
│   └── index.js
└── yarn.lock

起動をするためのスクリプトをpackage.jsonに追加する

package.json
  ...

   scripts{
      "dev": "next",
      "start": "next start",
      "build": "next build",
      "export": "next build && next export"
   }
  ...

起動する

index.jsの内容を表示するためには先程設定したyarn devのコマンドを打つことでlocalhost:3000にアクセスできるようになります。
ScreenShot 23.jpg

yarn dev

これがNext.jsで最小限に起動をする方法になります。開発環境で起動する場合にはyarn dev、ビルド&起動する場合にはyarn build && yarn startでサーバーが立ち上がります。

ChromeのExtensionを作る場合にはexportを使用して静的なページとして出力をします。

Chrome Extensionを作る

ChromeのExtensionを作るためにいくつのファイルを追加&変更します。

追加するフォルダ & 追加するファイル

  1. extensions // フォルダ
  2. extensions/manifest.json // 拡張機能の詳細を記述するファイル

manifest.jsonはこちらを参考にして記述をしていきます。

manifest.json
{
    "name": "extension",
    "description": "extension",
    "version": "1.0.0",
    "browser_action": {
      "default_popup": "./dist/index.html"
    },
    "permissions": [
      "bookmarks",
      "tabs",
      "activeTab"
    ],
    "manifest_version": 2
  }

変更するファイル

  1. package.json
変更前
package.json
   ....
  "scripts": {
    ...
    "export": "next build && next export"
  },
   ...
変更後

export-oオプションを付けて拡張機能の出力先を変更します。

package.json
   ....
  "scripts": {
    ...
    "export": "next build && next export -o extensions/dist"
  },
   ...

この3つの新しく変更を加えた状態の現在のディレクトリ構成はこのような状態となっております。

.
├── README.md
├── extensions
│   ├── dist
│   └── manifest.json
├── package.json
├── pages
│   └── index.js
└── yarn.lock

ちなみに上記の設定を行ったものがこのリポジトリとなりますので、このリポジトリをクローンをするとすぐに開発をし始めることが出来ます。

next-js-chrome-extension-starterkit

こちらのリポジトリはtsxファイルを追加したため少しファイルの構成が増えています。Next.jsではTypeScriptを扱おうとするとある程度必要なファイルを自動的にファイルを追加してくれます。

Chromeの拡張としてエクスポート

ScreenShot 23.jpg

この状態で起動をすると、白い背景にHello, World!と表示されていると思います。この状態のまま拡張にしてみましょう。

yarn export 

出力先をextension/distにpackage.jsonで設定しているので、静的なファイルが生成されます。

エクスポートされた拡張を読み込む

ScreenShot 26.jpg

  1. エクスポートされたChromeの拡張機能を読み込むためにはGoogle Chromeの設定 → その他ツール拡張機能を選択

ScreenShot 28.jpg

  1. パッケージ化されていない拡張を読み込む

ScreenShot 27.jpg

  1. プロジェクト → extensions ファイルを選択します

これで拡張機能を読み込むことが出来ます。

Chrome拡張を作ることが出来た(やったー!)

ScreenShot 30.jpg

ルーティングを追加する

Next.jsの特徴として静的サイトのルーティングを簡単に作れるので、新しいページを追加してみます。

Next.jsのLinkは可能ですが、URLの指定を.htmlまで記述する必要があります。開発環境では必要ないのでページのパスを取得するための関数を追加します。

constants/page.js
const Pages = () => {
    const suffix = process.env.NODE_ENV === "development" ? "" : ".html"
    const data = {
        index: `index${suffix}`,
        about: `about${suffix}`
    }
    return data
}

export default Pages

トップページから飛ぶ先のページ名aboutを追加します。

pages/about.js
import React from "react";
import Link from "next/link";
import Page from "../constants/page";

const About = () => {
  return (
    <div>
      このExtension@mxcn3が作成しました。
      <Link href={Page().index}>
        <a>トップページへ</a>
      </Link>
    </div>
  );
};

export default About;

トップページからページ名 aboutに飛べるようにします。

pages/index.js
import React from "react";
import Link from "next/link";
import Page from "../constants/page";

const Index = () => {
  return (
    <div>
      Hello World
      <Link href={Page().about}>
        <a>この拡張について</a>
      </Link>
    </div>
  );
};

export default Index;

yarn export

を再度実行することで拡張機能が更新されます。

output.gif

ページのサイズを変更する

ページのサイズをインラインで変更します。

あまり推奨されていないやり方みたいですが便利上インラインでスタイを変更します。
https://reactjs.org/docs/dom-elements.html#style

pages/index.js
import React from "react";
import Link from "next/link";
import Page from "../constants/page";

const Index = () => {
  return (
    //   ページの大きさを変更
    <div
      style={{
        width: 400,
        height: 400
      }}
    >
      Hello World
      <Link href={Page().about}>
        <a>この拡張について</a>
      </Link>
      {/* iframeを追加 */}
      <iframe
        style={{
          width: "100%",
          height: "100%",
          margin: 0,
          border: "none"
        }}
        src="https://playcanv.as/p/8NZ92jAY/"
      />
    </div>
  );
};

export default Index;

output2

このような形で表示されます。

まとめ

Next.jsはReactを使用して記述ができるので、Next.jsでGoogle Chromeの拡張機能を書くことができれば比較的作りやすいのではないでしょうか。Next.jsを使用したChrome拡張の記事などをあまり見なかったので記事にさせていただきました。

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

DenoでReact Server Side Renderingした話

概要

最近何かと注目が集まってるDenoですが、なんとDenoでjsxが動くみたいなのでDenoでReactが動くかやってみました。

Denoってなんぞや

これについては、今Deno Advent Calendarにて@kt3kさんが Deno ってなんだっけ?と言う記事をあげているので、そちらを参考にしてみてください。

Denoのインストール

何はともあれ、Denoを導入してみましょう!

brew install deno

インストールが完了したら、deno -vで以下のような画面が出てきたらインストール完了です。
スクリーンショット 2019-12-03 23.26.35.png

コードを書いてみよう

早速コードを書いていきたいわけですが、今回二つファイルを作ります。

  • index.tsx
  • App.tsx

App.tsx

こちらはいつも通りのreactコンポーネントです。一つ違うのはDenoはnode_modulesではなく直接ダウンロードする方式ですね。後のコードはいつも通りです。

Apptsx
import React from 'https://dev.jspm.io/react';

const App = () => <div>Hello Deno React</div>;
export default App;

index.tsx

次にindexファイルですが、

index.tsx
import { createRouter } from 'https://denopkg.com/keroxp/servest/router.ts';
import React from 'https://dev.jspm.io/react';
import ReactDOMServer from 'https://dev.jspm.io/react-dom/server';

import App from './app.tsx';

const router = createRouter();
router.handle('/', async req => {
  await req.respond({
    headers: new Headers({
      'content-type': 'text/html; charset=UTF-8',
      status: 200,
    }),
    body: ReactDOMServer.renderToString(
      <html>
    <head>
          <title>deno react ssr</title>
    </head>
    <body>
          <App />
    </body>
      </html>
    )
  })
});

router.listen(':8000');

ファイルの準備は以上になります。
ここで、以下コマンドを実行した上でhttp:/localhost:8000/にアクセスしてみましょう!

deno index.tsx --allow-net // denoの起動

これで画面上に Hello Deno Reactが出れば成功です!
Deno、未来ありますよね!では!

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