もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

Material-uiのstyled-components-apiを使う

今までstyled-componentsmaterial-uiそれぞれ入れて使ってたのですが、material-uiにstyled-componentっぽいAPIが生えてたのに気づいたので使ってみたメモ(キャッチアップできてないだけですね。。。)。@material-ui/coreのバージョンは4.1.0です。

リファレンス

material-ui.com

使ってみる

基本

公式に載ってるコードだとこんな感じですね。本家の方はTemplateStringsArrayでスタイル定義しますが、こちらはデフォルトだとCSSPropertiesのObjectを食わすのでよりJSっぽいスタイルです。

import React from 'react';
import { styled } from '@material-ui/styles';
import Button from '@material-ui/core/Button';

const MyButton = styled(Button)({
  background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
  border: 0,
  borderRadius: 3,
  boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
  color: 'white',
  height: 48,
  padding: '0 30px',
});

MUIのComponentではなく普通のHTMLのタグを指定したいときはstringを指定します。

import React from 'react';
import { styled } from '@material-ui/styles';

const MyButton = styled('button')({
  background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
  border: 0,
  borderRadius: 3,
  boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
  color: 'white',
  height: 48,
  padding: '0 30px',
});

Propsに応じてスタイルを変える

Propsに応じてスタイルを変えたい時はこんな感じです。Componentは必要なPropsを渡せる形にして、スタイルは値ではなくpropsを受け取って値を返す関数を設定します。

import React from 'react';
import { styled } from '@material-ui/styles';

type MyButtonProps = {
  size: 'small' | 'normal';
};

// lintで怒られるの不本意なんですけど・・・
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MyButton = styled(({ size, ...other }: MyButtonProps) => <button {...other} />)({
  background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
  border: 0,
  borderRadius: 3,
  boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
  color: 'white',
  height: 48,
  padding: '0 30px',
  fontSize: ({ size }: MyButtonProps) => (size === 'small' ? '12px' : '16px'),
});

自分で定義したComponentで使う

classNameをPropsで渡してRootのComponentに渡します。

MyComponent.tsx

import React from 'react';
import { styled } from '@material-ui/styles';

type MyComponentProps = {
  className?: string;
};

const MyComponent = ({ className }: MyComponentProps) => (
  <div className={className}>こんぽーねんと</div>
);

export default MyComponent;

CustomComponent.tsx

import React from 'react';
import { styled } from '@material-ui/styles';
import MyComponent from './MyComponent';

const StyledMyComponent = styled(MyComponent)({
 background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
});

const CustomComponent = () => (
  <div><StyledMyComponent /></div>
);

export default CustomComponent;

おしまい

  • 少し試しただけですが最初から作るならMaterialUIに寄せてもいいかなと思いました。

React(Gatsby)+ Firebaseでサーバレス入門した

個人開発でFirebase使ってなんか作ろうかなということで、素振りで作ったものを公開してみました。Reduxのチュートリアルで作るTodoアプリをStoreをFirebaseにした体で作り変えたやつになります。Firebaseは古の時代に触ったときは単なるPub/SubできるDBだったのにいろいろ出来るようになっててビビりますね。触る前に公式のドキュメントをざっと読んでcodelabを試したくらいの事前知識で作りました。

作ったもの

サイトはこちら。単に最近遊んでるという理由だけでGatsby.jsで作ってます。Netlifyでホスティングしてますが、特にFirebase hostingを使ってない理由はありません。AuthとFirestoreだけ試したかったので。

gatsby-firebase-todo.netlify.com

コードはこちら

GitHub - rei-m/gatsby-firebase: Sample of Gatsby.js with Firebase

実装

Firebaseの設定

何はともあれエントリーポイントでfirebase.initializeAppをしなければいけないのですが、Gatsbygatsby-browser.jsにonClientEntryというAPIが生えているのでそこで行いました。これだけですね。

export const onClientEntry = () => {
  const config = {
    // your config
  };
  firebase.initializeApp(config);
};

Firebase.authentication

firebase.auth().onAuthStateChangedにObserverを登録します。これはアプリケーション全体に影響するのでReact.Contextに認証状態を保持してアプリケーション全体を囲むようにしました。こんな感じのcustom hookを作って取れたuserオブジェクトをcontextで持つようにしてます。

useFirebaseAuth.ts

export const useFirebaseAuth = () => {
  const [user, setUser] = useState<User | null>();

  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      if (user) {
        console.info(`firebase: authorized (uid: ${user.uid})`);
        const userName = user.displayName ? user.displayName : '名無し';
        setUser({ uid: user.uid, name: userName });
      } else {
        console.info(`firebase: unauthorized`);
        setUser(null);
      }
    });

    return () => {
      console.info(`firebase: unsubscribe onAuthStateChanged`);
      unsubscribe();
    };
  }, []);

  return user;
};

useEffectを使ってonAuthStateChangedのsubscribeを開始、componentが破棄されるときにunsbscribeするという感じになりますね。contextはこれだけになります。Gatsbygatsby-browserとgatsby-ssrのwrapRootElementでこのProbiderで包んであげればOKです。

FirebaseAuthProvider.tsx

export const FirebaseAuthContext = React.createContext<{
  user?: User | null;
}>({});

const FirebaseAuthProvider: React.FC<{}> = ({ children }) => (
  <FirebaseAuthContext.Provider value={{ user: useFirebaseAuth() }}>
    {children}
  </FirebaseAuthContext.Provider>
);

これで認証状態が変わったタイミングでcontextのuserが更新されて再描画が走るようになりました。contextの情報はuseContextを使えば参照できます。

Firebase.firestore

今回はよくあるTodoアプリを作るのでユーザーごとのTodoリストをFirestoreに保存します。構成は素朴にUsersというCorrectionの下にUser単位でdocmentを作り、その下にSubCorrectionでtodosを保存します。 FirestoreもonSnapshotでObserverを登録する実装になる(リアルタイム同期が必要なければいらないけど)ので、同様にuseEffectを使ったcustom hookを作ります。

useFirestoreTodos.ts

export const useFirestoreTodos = (uid: string, filter: VisibilityFilter) => {
  const [todos, setTodos] = useState<Todo[]>();

  useEffect(() => {
    const collection = todosCollection(uid);

    let query: firebase.firestore.Query;
    switch (filter) {
      case SHOW_ACTIVE:
        query = collection
          .where(`completed`, `==`, false)
          .orderBy(`createdAt`, `desc`);
        break;
      case SHOW_COMPLETED:
        query = collection
          .where(`completed`, `==`, true)
          .orderBy(`createdAt`, `desc`);
        break;
      default:
        query = collection.orderBy(`createdAt`, `desc`);
        break;
    }

    const unsubscribe = query.onSnapshot(snapshot => {
      console.info(`firestore: receive todos: size=${snapshot.docs.length}`);

      const todos = snapshot.docs.map(doc => toModel(doc.id, doc.data()));
      setTodos(todos);
    });

    return () => {
      console.info(`firestore: unsubscribe onSnapshot:todos`);
      unsubscribe();
    };
  }, [filter]);

  return todos;
};

authと同様ですが、リストの検索条件は変更出来るので、useEffectの第2引数にfilterを指定してfilterが変更されたらobserverを登録し直すようにします。実際にこのhookを使ったcomponentはこんな形になります。

TodoContents.tsc

const TodoContents = ({ user }: Props) => {
  const [filter, setFilter] = useState<VisibilityFilter>(SHOW_ALL);
  const todos = useFirestoreTodos(user.uid, filter);

  const handleAddTodoSubmit = async (todoName: string) => {
    await addTodoAction(user.uid, todoName);
  };

  const handleClickTodo = async (todo: Todo) => {
    await updateTodoAction(user.uid, todo.id, !todo.completed);
  };

  const handleClickDeleteTodo = async (todo: Todo) => {
    await deleteTodoAction(user.uid, todo.id);
  };

  return (
    <section>
      {todos ? (
        <>
          <AddTodoForm onSubmit={handleAddTodoSubmit} />
          <TodoList
            todos={todos}
            onClickTodo={handleClickTodo}
            onClickDeleteTodo={handleClickDeleteTodo}
          />
          <TodoFilter currentFilter={filter} onClick={setFilter} />
        </>
      ) : (
        <div>ろーでいんぐ</div>
      )}
    </section>
  );
};

動かしてみるとこんな感じ

f:id:Rei19:20190608212724g:plain

おしまい

  • 簡単にしか触っていないけどFirestoreまわりの設計がキモになるなーという印象です。RDBでは○○できるのにとか思わずに(そもそも全然別物だけど)、Firestoreに最適化した設計を考えてかないといかんですね。
  • あとはCloudFunctionあたりを素振りすれば、だいたいサービス作るのに必要なものは事足りそう。
  • docment更新したときにonSnapshotで2回通知が流れてくるのがちょっと謎挙動でした。serverTimestampの仕様っぽいけど要調査。
  • テスト書くのはどうやるのかわかってないので後ほど

関連

というようなことを試してたらエンジニアHUBで気合の入った入門記事が流れてきたので紹介など

employment.en-japan.com

TSLintからESLintへ移行した

React/TypeScriptなアプリで TSLint + Prettier でlint/formatをかけていたところを ESLint-TypeScript + Prettierに移行したときのメモ。急いでやる必要はないのですが、仕事の方で使いたかったので個人プロジェクトで試しました。

github.com

github.com

必要なライブラリの追加

yarn add -D eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-react @typescript-eslint/eslint-plugin

eslintの設定の追加

  • .eslintrc.jsonを追加 (フォーマットはいろいろ選べる)
{
  "extends": [
    // とりあえず推奨ルールに従っておく。後でメンテ。
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": [
    "@typescript-eslint", // TypeScript用
    "react"  // react用
  ],
  "env": {
    "browser": true,  // 事前に定義済みの条件を設定。例えばjestを入れないと`describe`や`it`が未定義の参照でlintにひっかかる
    "jest": true,
    "es6": true,
    "node": true
  },
  "parser": "@typescript-eslint/parser", // TypeScript用
  "parserOptions": {
    "sourceType": "module", // iTypeScriptはimportが使われるので
    "project": "./tsconfig.json", // TypeScriptのconfig path
    "ecmaFeatures": {
      "jsx": true  // react用
    }
  },
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], // TypeScript用のno-unused-varsを有効にしている。
    "no-console": ["error", { "allow": ["info", "error"] }], // この辺はなくても
    "react/prop-types": "off"  // TypeScriptではprop-types不要。というかoffらないとlintに引っかかる
  },
  "settings": {
    "react": {
      "version": "detect" // デフォルトっぽい設定だけど入れないとwarningが出た。。。どこか間違ったのだろうか。。。
    }
  }
}
  • tslint.jsonを削除

package.json の更新

  • npmコマンドの修正
 "scripts": {
  "lint": "eslint '{src,helper}/**/*.{ts,tsx}'",
}
  • tslint系の依存を削除

必要な人はVSCodeの設定の更新

  • tslint系の設定を削除して以下を追加。保存時にlintのruleに従って自動で修正されるようになります。
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        { "language": "typescript", "autoFix": true },
        { "language": "typescriptreact", "autoFix": true }
    ]

おしまい

  • 割とすっと移行できました。実際のPRはこちら

github.com

参考

teppeis.hatenablog.com

Gatsby.js + Storybook でStaticQueryが動かなかったのでどうにかした

概要

  • Gatsbyで書いたアプリにStorybook導入してSnapshotのテストはStoryshots使うようにしようと思ったのですが、普通にドキュメントのとおりに導入したらgraphqlを呼んでいるcomponentで以下のようなログが出てコケました。Gatsbyのコンテキストで起動してないとgraphqlは呼ばれたときに死ぬみたいですね。
index.js:39 Error: It appears like Gatsby is misconfigured. Gatsby related `graphql` calls are supposed to only be evaluated at compile time, and then compiled away,. Unfortunately, something went wrong and the query was left in the compiled code.

どうやった

  • ドキュメントやらissueやら掘ってみたものの見つからず。。。しょうがないのでaliasでgatsbyのモジュールをモックに差し替えるようにしました。

  • とりあえず雑にモック作ります。もともとjestを導入するときに作るモックなのですが、jestを使わない体に変え、useStaticQueryのところはQueryの名前に引っ掛けて対応するそれっぽいデータを返すようにします。

mocks/gatsby.js

const linkActionHandler = action("Link:");
const navigateActionHandler = action("NavigateTo:");

module.exports = {
  graphql: (args) => args,
  Link: ({ to, ...rest }) =>
    React.createElement("a", {
      ...rest,
      href: to,
      onClick: (e) => {
        e.preventDefault();
        linkActionHandler(to);
      }
    }),
  StaticQuery: () => {},
  useStaticQuery: (args) => {
    const query = args[0];
    if (query.indexOf("query SEOQuery") > -1) {
      return {
        site: {
          siteMetadata: {
            title: "タイトル",
            description: "説明",
            author: "rei-m",
          },
        },
        ogpImage: {
          publicURL: iconImage,
        },
      };
    }
    
    (中略)

    return {};
  },
  navigate: (to, options) => {
    navigateActionHandler(to, options);
  }
}
  • そして、storybookのwebpackのaliasでgatsbyが呼ばれたらモックが読み込まれるようにします。

.storybook/webpack.config.js

module.exports = ({ config }) => {
  // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
  config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

  (中略)

  config.resolve.alias = {
    ...config.resolve.alias,
    '@src': resolve(__dirname, '../src'),
    '@helper': resolve(__dirname, '../helper'),
    'gatsby': resolve(__dirname, '../__mocks__/gatsby'), // 追加
  }

  return config
}

これでめでたく?Storybookが動いてStoryshotsもall greenになりました

f:id:Rei19:20190426221321p:plain

おしまい

  • 最初useStaticQueryをReact.Contextで保持するようにして描画時に差し替える感じで書いてたのですが過剰な気がしたので、上記のような形に。
  • あまりStorybook詳しくないので絶対もっといい方法があると思われます。。。
  • コードはこちら

github.com

百人一首を暗記するサイトをGatsby.jsに乗せてNetlifyで公開した

ここ最近やってた作業がきりのいいところまで終わったのでログ代わりのブログです。もともとピュアなSPAとして作っていたものをGatsbyに乗せて静的化しました。コアのゲーム部分は以前と変わらずSPAとして動きますが、Googleにインデックスさせる部分は実体のHTMLがちゃんと存在する状態になっています。

作ったもの

hyakuninanki.net

前のバージョンの問題点

  • SEOで事故る
    • 最近のGoogle先生BotはJSによるレンダリング後のページを見てくれてるっぽくて、SearchConsoleでFetch as Googleしてちゃんと表示されたし、インデックスにも乗ったので放っておいたのですが、ある時点で検索結果から消滅してキャッシュを見ると真っ白なページになっていました。おそらくレンダリングがされずに空のページが登録されたのだろうというところです。レスポンス自体は常時200返してますしね。
  • アドセンスの審査に落ちる
    • 直接関係があるかは謎ですが、前の状態だとコンテンツ不足と言われ、リメイクした後だとすんなり審査がおりました。

コメント

  • だいたい調べたいことはGatsbyを検証したときに終わっていたので、ページレベルのComponentを多少作り変えるのと画像類の参照をGatsbyの仕組みに載せ替えるくらいで割とすんなり移行できました。もともとReact + TypeScript + styled-componentsで作ってたのでコアな部分は割とそのままですね。今のところ、SPA作るときはこの組み合わせで満足してます。
  • コードはいつもどおりポートフォリオ代わりに公開してます。

GitHub - rei-m/web_hyakuninisshu

参考

去年公開した時の記事

rei19.hatenablog.com

事前の素振りとして検証した記事

rei19.hatenablog.com

Androidアプリ版もあるよの記事

rei19.hatenablog.com

Gatsby.jsでTypeScriptで書かれたReact/ReduxなSPAを配信する

個人で公開しているSPAが素朴なReactで作られていてSSRも何もしておらず、もうちょっとGoogle先生Botに優しく作ってあげようということで、前段階の準備としていろいろ試してみたメモです。対象のサイトはそんなに大きくないので、SSRを頑張るのではなくて事前にレンダリング済のページを静的なHTMLに出力して配信という形式にします。

作ったもの

いきなり作り変えるには知見がないのでReduxのチュートリアルでよくやるTodoアプリケーションをGatsby.jsを使って実装して、できたものをNetlifyにデプロイしました。言語は一部のGatsbyの設定ファイル以外はTypeScriptで書いています。

で、作ってみたのがこちら gatsby-ts-todo.netlify.com

オリジナルのチュートリアルには無いページですがTodoの詳細のページを追加していて 、pathはtodos/:id/ になります。Gatsby.jsで動的に静的なページを生成するための検証として、Todoを2つ初期状態で登録済にしておいて、これらのTodoの詳細ページは静的なページとして配信して、URLを直接叩いて見えるようになっています。

これは直接見える https://gatsby-ts-todo.netlify.com/todos/1/

これは直接見えない。 https://gatsby-ts-todo.netlify.com/todos/3/

はまったところ

ページを動的に生成するためにはgatsby-node.jsに生えているAPIを実装するのですが、ここもTSで書きたいなあというとこで、ちょっと試行錯誤しました。結果としては以下のようになりました。

gatsby-node-config.tsにTSで実装を書いてます。createPagesではStoreの初期データからcreatePageしてるのがわかると思います。

gatsby-node-config.ts

import { resolve } from 'path';
import { GatsbyCreatePages, GatsbyOnCreatePage } from './types';
import { initialData } from '@src/state/todos';
import { Todo } from '@src/types';

export const createPages: GatsbyCreatePages<{ todo: Todo }> = async ({ actions }) => {
  const { createPage } = actions;
  initialData.forEach(todo => {
    createPage({
      path: `/todos/${todo.id}`,
      component: resolve(`./src/templates/todos.tsx`),
      context: {
        todo,
      },
    });
  });
};

export const onCreatePage: GatsbyOnCreatePage<{ todo: Todo }> = async ({ page, actions }) => {
  const { createPage } = actions;

  // page.matchPath is a special key that's used for matching pages
  // only on the client.
  if (page.path.match(/^\/todos/)) {
    page.matchPath = '/todos/:id';
    createPage(page);
  }
};

で、本来実装する gatsby-node.js では gatsby-node-config からrequireしてexportしてスルーパスしています。configのビルドはts-nodeを使うことでうまいこと通りました。

gatsby-node.js

'use strict'

require('source-map-support').install()

require("tsconfig-paths").register({
  baseUrl: './',
  paths: {
    '@src/*': [ 'src/*' ],
    '@test/*': [ 'test/*' ]
  }
});

require('ts-node').register({
  compilerOptions: {
    module: 'commonjs',
    target: 'es2017',
    noImplicitAny: false
  }
})

const { resolve } = require('path');

const config = require('./gatsby-node-config');

exports.createPages = config.createPages;

exports.onCreatePage = config.onCreatePage;

exports.onCreateWebpackConfig = ({ actions }) => {
  actions.setWebpackConfig({
    resolve: {
      alias: {
        '@src': resolve(__dirname, 'src/'),
        '@test': resolve(__dirname, 'test/')
      }
    }
  })
}

おしまい

Gatsbyチュートリアルが充実してたので、Reactを触ったことがあれば基本的な機能は割とすぐ使えるかなという印象です。GraphQLまわりのところがちょっとまだ完全には把握できてないので、そのあたりをもうちょっとドキュメントを読みつつ試してみようかなと思います。

全体のコードはこちら

GitHub - rei-m/gatsby-todo: Todo Application created by Gatsby with TypeScript.

2018年を振り返る

このエントリは、今年一年の自己の振り返り Advent Calendar 2018 - Adventar の23日目の記事で、34歳のWeb界隈で働いているおじさんが2018年を振り返ります。

仕事の開発話

  • 去年の春ぐらいに転してから変わらず10人程度のベンチャーで2BのWebサービスを作り続けてます。幸いなことにユーザーの評判もよく、ヘビーに使っていただいているのでいろいろ作ったかいはあったかなというところです。
  • 使っている技術スタックはバックエンドはRoR、フロントエンドはReact/Redux with TypeScriptでベンチャーによくある構成です。他と多少違うのはバックエンドもフロントエンドもテストを結構ガチガチに書いているところですかね。2Bでユーザーの業務に影響が出るものを作ってるので、速度 < 安定 で開発してます。

個人の開発話

  • アプリを1本クローズしてWebサイトを2本公開しました。 Webサイトは最初はAWSで公開してたのですが、Netlifyに切り替えました。今のスキルセット的にはAWSを習熟するよりはJAMStackな構成で高速にサービスを立ち上げられる方に寄せたほうがいいかなーと思ったのでNetlifyにしてます。
  • 公開してるもの1

play.google.com

2016年の12月にリリースしたやつです。順調にDL数が伸びてこの記事を書いてる時点で13.4Kでした。あんまり機能改善はできてないけど今年はAndroidX対応したり、AAC取り入れたりと、まあまあモダンな感じに手を入れました。

  • 公開してるもの2

https://hyakuninanki.net/

↑のアプリのWeb版てことで春ぐらいにリリースしました。しばらく放置していてユーザーも全然来なかったのですが、10月くらいにSEO意識してコンテンツ見直したら改善しました。今は百人一首で検索すると1ページの3〜5番目の表示で安定しています。↓は劇的に改善されたのがよくわかるグラフ。

f:id:Rei19:20181222225931p:plain

  • 公開してるもの3

https://osechi.jp/

osechi.jpというドメイン買ったのでおせちのメタサーチサイト作りました。が、改善する前にシーズンのピークが過ぎたのでまた来年。

その他四方山

  • 娘が無事に1歳になりました。同時に保育園にも入り妻も仕事し始めたので、土日はワンオペで子供の面倒を見ることも少なくありません。必然的に自分がコードに向き合える時間は深夜くらいしかないので、いろいろやりたいことに対して物理的な時間が足りないですね。まあ子供優先だし人生の中ではそんな時期もあるだろうという感じです。
  • 前ほど勉強会に顔を出さなくなりました。↑な状態なので勉強会のインプットを自分のアウトプットに変換できないためです。DroidKaigiとか大きいカンファレンスには引き続き参加します。
  • 故あって青色申告するために開業届を出しました。屋号はまだない。せっかくならなんか軽い副業くらいしてみたいすな。