20191218のReactに関する記事は18件です。

React + chart.jsで「動く」グラフサンプル

はじめに

こちらのようなリアルタイムに動いていくグラフを作りたかったのですが、Reactのドキュメントがあまり充実していなかったので色々と調べて動くコードを作ったのでメモしておきます。
(そのままでも動きますがリファクタリングしたほうが良いと思います。)

※blueCount, redCount, whiteCountの値をなにかのタイミングで変えればグラフも変わります。
(Stateを使用して変数の変化を監視する必要がありません)

スクリーンショット 2019-12-18 21.15.17.png

demo.js
import React from 'react'
import { Bar, Chart } from 'react-chartjs-2'
import 'chartjs-plugin-streaming'

let blueCount = 5
let redCount = 5
let whiteCount = 5
let RealTimeChart = (props) => {

  function onRefresh(chart) {
    chart.config.data.datasets.map((dataset) => {
      switch (dataset.label) {
        case 'blue':
          dataset.data.push({
            x: Date.now() - 1,
            y: blueCount,
          })
          return null
        case 'white':
          dataset.data.push({
            x: Date.now(),
            y: whiteCount,
          })
          return null
        case 'red':
          dataset.data.push({
            x: Date.now() + 1,
            y: redCount,
          })
          return null
        default:
          console.log('default')
          return null
      }
    })
  }

  var color = Chart.helpers.color

  return (
    <div>
      <Bar
        height={100}
        data={{
          datasets: [
            {
              label: 'blue',
              stack: 'blue',
              backgroundColor: color('#516897')
                .alpha(1)
                .rgbString(),
              data: [],
            },
            {
              label: 'white',
              stack: 'white',
              backgroundColor: color('#B9B9B9')
                .alpha(1)
                .rgbString(),
              data: [],
            },
            {
              label: 'red',
              stack: 'red',
              backgroundColor: color('#AC3A38')
                .alpha(1)
                .rgbString(),
              data: [],
            },
          ],
        }}
        barSize={100}
        options={{
          responsive: true,
          legend: {
            display: false,
          },

          scales: {
            xAxes: [
              {
                stacked: true,
                type: 'realtime',
                realtime: {
                  duration: 15000,
                  refresh: 3000,
                  delay: 1000,
                  onRefresh: onRefresh,
                },
                gridLines: {
                  color: '#4d4d4d',
                },
                ticks: {
                  display: false,
                },
              },
            ],
            yAxes: [
              {
                stacked: true,
                gridLines: {
                  color: '#4d4d4d',
                },
                ticks: {
                  min: 0,
                  beginAtZero: true,
                  callback: function(value) {
                    if (value % 1 === 0) {
                      return value
                    }
                  },
                },
              },
            ],
          },
        }}
      />
    </div>
  )
}
export default RealTimeChart

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

React Context APIを使った非同期通信のハンドリング

本稿はReact Advent Calendar 2019 18日目の記事です!

はじめに

Reactにおける非同期通信のハンドリングどうしていますか?

通信中のローディングアイコンの表示や、エラーハンドリング・・・
正解がわからない?

そこで今回はReactのContext APIを使ってハンドリングしてみました!
これが正解だとは思いませんが、一例として共有させていただきます :pray:

TL;DR

  • Redux使わないよ
  • Context APIでエラーハンドリングとダイアログコンポーネントの表示やってみたよ

Reduxを使うパターン

よくありがちな、リクエスト毎に成功時と失敗時のアクションを用意するパターン。
Storeにエラー内容を突っ込んで、エラー表示のためのコンポーネントを作ってよしなにやるイメージ。

const GET_ITEMS_REQUEST = 'GET_ITEMS_REQUEST';
const GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS';
const GET_ITEMS_FAILURE = 'GET_ITEMS_FAILURE';

const getItemsRequest = () => { ... }
const getItemsSuccess = () => { ... }
const getItemsFailure = () => { ... }

const getItems = () => {
  return (dispatch) => {
    dispatch(getItemsRequest);

    return axios.get(`http://localhost/api/items`)
      .then(res =>
        dispatch(getItemsSuccess(res.data))
      ).catch(err =>
        dispatch(getItemsFailure(err))
      );
  }
}

const initialState = { isFetching: false,  error: null, ...};

const reducer = (state = initialState, action) {
  switch (action.type) {
    case GET_ITEMS_FAILURE:
      return {
        ...state,
        isFetching: true,
      };
    case GET_ITEMS_FAILURE:
      return {
        ...state,
        isFetching: false,
        error: action.error,
      };
    ...
  }
}

// jsx 
{error && <Dialog>Error!!</Dialog>}

なんか冗長でしんどい?

Context APIで実装してみる

Context APIに関しては公式リファレンスをご参照くださいませ?‍♂️
コンテクスト – React

以下、作ったものです!
https://codesandbox.io/s/loving-wiles-tvdjy?fontsize=14&hidenavigation=1&theme=dark

index.js

APIのサンプルとして、QiitaのAPI叩かせてもらっています。
failureRequest内のpostは、tokenがなくて認証エラーになる形です。

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

import {
  ApiRequestHandleContext,
  ApiRequestHandleContextProvider
} from "./apiRequestHandleContext";

const successRequest = async params => {
  return axios.get("https://qiita.com/api/v2/items", { params });
};

const failureRequest = async () => {
  return axios.post("https://qiita.com/api/v2/items");
};

const App = () => {
  const { execRequest, isRequesting } = useContext(ApiRequestHandleContext);
  const handleOnSuccessClick = () => {
    // APIのレスポンスが返ってくる
    execRequest(successRequest, { page: 2 }).then(console.log);
  };

  const handleOnFailureClick = () => {
    execRequest(failureRequest);
  };

  return (
    <div className="App">
      <button onClick={handleOnSuccessClick}>Success Button</button>
      <button onClick={handleOnFailureClick}>Failure Button</button>
      {isRequesting && <div>Now Requesting!!</div>}
    </div>
  );
};

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

apiRequestHandleContext.js

import React, { useState, createContext } from "react";
import Dialog from "./Dialog";

export const ApiRequestHandleContext = createContext({
  execRequest: () => {},
  isRequesting: false
});

export const ApiRequestHandleContextProvider = props => {
  const [isRequesting, setIsRequesting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);

  const handleError = error => {
    setErrorMessage(error.response.data.message);
  };

  const execRequest = async (requestFn, ...args) => {
    setIsRequesting(true);
    const res = await requestFn(...args).catch(handleError);
    setIsRequesting(false);
    return res;
  };

  return (
    <ApiRequestHandleContext.Provider
      value={{
        isRequesting,
        execRequest
      }}
    >
      {errorMessage && <Dialog>{errorMessage}</Dialog>}
      {props.children}
    </ApiRequestHandleContext.Provider>
  );
};

解説

apiRequestHandleContextのexecRequestがポイント

 const execRequest = async (requestFn, ...args) => {
    setIsRequesting(true);
    const res = await requestFn(...args).catch(handleError);
    setIsRequesting(false);
    return res;
  };

requestFnが実行する非同期通信処理で、可変長引数 ...argsをパラメータとして渡します。

非同期通信でエラーがあった場合、catchに渡されているhandleErrorが実行され、エラーレスポンス内のmessageをDialogとして表示という仕組みになっています。

また、リクエストの前後でuseStateを利用してリクエスト中かのフラグ isRequesting をハンドリング。
このisRequestingはcontextとして提供されているので、リクエスト中はindex.js側で「Now Requesting!!」というテキストを表示しています。

あとは ApiRequestHandleContextProvider でラップしてあげれば?‍♂️

const { execRequest, isRequesting } = useContext(ApiRequestHandleContext);
  const handleOnSuccessClick = () => {
    // execute someAsyncFunction(param1, param2, param3);
    execRequest(someAsyncFunction, param1, param2, param3}).then(res => { console.log(res)});
  };

注意

今回の実装だと、並列で複数のリクエストが呼ばれた際にリクエスト状態は1つのisRequestingを参照しているので、実際はまだ終了していないリクエストがある場合もisRequestingはfalseになってしまいます。

コードが複雑になるのを避けたかったので今回は実装していませんが、リクエスト毎にユニークキーを振って、それぞれのリクエストの状態を1つずつ管理するような実装もしたりしました :innocent:

おわりに

  • ロジックをview側に寄せる形になるので抵抗ある人はあるかも・・・
  • でもReducerを肥大化させるのも辛い?
  • たぶん色々なハンドリングパターンがあると思うので、もっと色々調べてみたい
  • Hooksは偉い!?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hover.cssをReact上で使って、要素をブルンブルン動かしたい!

はじめに

タイトル通り、Reactで生成した要素にホバーアクションを実装してブルンブルン動かしたくなったのでやってみました。

開発環境

VsCode
npm 6.12.1

セットアップ

Hover.cssのインストール

何パターンか方法があるらしいですが、今回はGithubからソースファイルを引っ張ってきてローカルに保存する方法でやってきます。

https://github.com/IanLunn/Hover
上記サイトのcssフォルダ配下にあるhover-min.cssをダウンロードして、ローカルの適当なフォルダに配置します。

hover-min.cssを上位のスタイルファイルでインポート

プロジェクトの上位のスタイルファイルで、保存したhover-min.cssをimportします。

@import url("hover-min.css");

classをつける

これでHover.cssを使える状態になったので、ブルンブルンさせたい要素に対してクラスをつけてあげます。

    <div className="hvr-grow">

動作確認

サーバを起動して確認してみましょう。
screen.gif

終わりに

楽ちんですね。

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

<day2>Webアプリ完成するまで続ける開発日誌

こんにちは!山形大学のもえとです!

day1と同じ日ですがday2をリアルタイムで勉強しながら書いていきます。

day1:https://qiita.com/se_n_pu_u_ki/items/e92bfa9bdadc1316d2f1

今日から早速Reactの基本的な概念とか使い方を学んでいきます。

今日学ぶのは

・JSX = Reactの文法
・コンポーネント = Reactの基本
・state = Webサイトの動きを作る
・props = JSXのパラメータ
・input要素 =入力テキストボックス

ざっくりこんな感じ。
盛りだくさんだけどReactの基本なので頑張ろう!

ReactのJSXについて

JSXとは、HTMLとJacascriptごちゃ混ぜにできる文法です。

< day1 >で書いたclient.jsの文法でこんなのがありました。

src/js/client.js
  render() {
    return (
      <h1>It works!</h1>
    );
  }

return まではJavascriptの文法ですが < h1 > ってHTMLですよね。

webpackでコンパイルするとclient.js

src/js/client.js
    return React.createElement(
      "h1",
      null,
      "Welcome!"
    );

このように変換される(HTML→Javascript)ことで読み込んでいるんだそうです。

コンパイルが優秀なおかげで楽に書くことができるのがJSX。

ちなみにHTML→Javascript変換してくれてるのは「Babel」と呼ばれるツール。

JSXで変数

JSXの中にJavascriptで定義した関数を使いたい時は{変数名}のように記載すると埋め込むことができます。
やってみましょう

src/js/clinent.js
import React from "react";
import ReactDOM from "react-dom";

class Layout extends React.Component {
  render() {
    let name = "Moeto";
      return (
        <h1> It's {name}!</h1>
      );
    }
  }

const app = document.getElementById('app');
ReactDOM.render(<Layout/>,app);

name変数は"Moeto"が入っていたため画面には

It's Moeto

と表示されます

Reactのコンポーネントについて

Reactはコンポーネントというものを定義していき、それを組み合わせることで画面を作っていきます。いくつかのファイルに分けることができるため保守性、再利用性が高いのです。

実際に前のclient.jsにあるLayoutクラスをsrc/js/componetnts.Layout.js`に移動してみましょう。

まずはファイルとディレクトリ作成

$ mkdir -p ./src/js/components
$ touch src/js/components/Layout.js

そしてLayout.jsを以下のように書き込み。

src/js/components/Layout.js
import React from "react";

export default class Layout extends React.Component{
  constructor() {
    super();
      this.name = "Moeto";
  }
  render() {
    return (
      <h1>It's {this.name}!</h1>
    );
  }
}

export defaultはJavascriptの文法で他のファイルに読み込ませるためのもので、Layoutクラスを外部アクセス可能にします。

そしてclient.jsにも変更を加えます。さっきのLayoutクラスは消し、import構文をつかってLayout.jsにあるLayoutクラスを読み込ませます。
次のように変更してください。

src/js/client.js
import React from "react";
import ReactDOM from "react-dom";
import Layout from "./components/Layout";

const app = document.getElementById('app');
ReactDOM.render(<Layout/>,app);

行数がかなり減りましたね。class Layoutと書いていたところをimport Layoutにすることで分かりやすく変更を加えることができます。

実際にここでhttp://localhost:8080をみてみてもIt's Moetoなどと表示されているかと思います。

Header、Footerコンポーネント

ページのHeader、Footerも別コンポーネントにしておくと便利ですね。
作成しましょう

$ touch ./src/js/components/Header.js
$ touch ./src/js/components/Feader.js

作成したHeader.jsはこのように書き込み。

src/js/components/Header.js
import React form "react";

export default class Header extends React.Component{
  render() {
    return (
      <div>header</div>
    );
  }
}

Footer.jsにも書き込み。

src/js/components/Footer.js
import React from "react";

export default class Footer extends React.Component {
  render() {
    return (
      <footer>footer</footer>
    );
  }
}

キーボードで打っている方はなんとなく似た文法がわかってきたかもしれません。コピペを使わずにキーボードで打っているだけでも少しずつReact文法が把握できます。

Header.jsとFooter.jsができたのでLayout.jsに読み込ませましょう。

src/js/components/Layout.js
import React from "react";
import Header from "./Header";

export default class Layout extends React.Component {
  render() {
    return(
      <div>
        <Header />
        <Footer />
      <div>
    );
  }
}

これでブラウザを確認すると、

header
footer

と表示されていれば成功です!

コンポーネントのお話終了。

stateについて

stateはアプリケーションの状態を保持するもので、コンポーネントをどのようにレンダリング(表示)するのかという情報を格納する場所のこと。

まぁまずは使ってみましょう。
これやると動きをつけることができます!
 
Layout.jsを以下のように変更。

src/js/components/Layout.js
import React from "react";
import Header from "./Header"
import Footer from "./Footer";

export default class Layout extends React.Component{
    constructor(){
        super();
        this.state = {name:"Moeto"};
    }
    render(){
        setTimeout(
            () => { this.setState({name:"Hello"});}
        ,1000);
        return(
            <div>
                {this.state.name}
                <Header />
                <Footer />
            </div>
        );
    }
}

ブラウザを再読み込みしてみましょう。
一番上の行が最初は"Moeto"などです。
5秒待ってみると"Hello"に変わりましたね!

setTimeoutの下にある数字を変えてみると変更までの待機時間が変わることが分かります。

なるほど、このように動きを付け加えられるのがstateなんですね。

Propsについて

ひとつのコンポーネントに対し、一部だけ別な色にしたい、などを可能にするのがProps。HTMLではタグ要素に対してパラメータ(classなど)を持たせてそのパラメータをCSSで変更することで一部の色を変えたりイベントを登録したりできます。JSXでもPropsをつかうことで同じようにパラメータを渡すことができます。

Propsをつかって書き換えてみましょう。

まずLayout.jsを以下のように書き換えます。

src/js/components/Layout.js
import React from "react";
import Header from "./Header"
import Footer from "./Footer";

export default class Layout extends React.Component{
    render(){
        const title = "Welcome Moeto!";
        return(
            <div>
                <Header title={title}/>
                <Footer />
            </div>
        );
    }
}

title={title}という部分の書き方がPropsですね。
続いてHeader.jsを書き換えていきます。

src/js/components/Header.js
import React from "react";
import Title from "./Header/Title"

export default class Header extends React.Component{
    render(){
        console.log(this.props);
        return (
            <div>
                <Title />
            </div>
        );
    }
}

Layout.js殻渡されたPropsはthis.propsでアクセスすることができます。

ブラウザの開発者ウィンドウのコンソールに

{title: "Welcome Moeto!"}

などど書かれていたら成功!

※開発者ウィンドウは、chromeの場合はF12で開くことができます。

また、Headerコンポーネントを複数作成して異なるPropsを渡すことで異なるパラメータを持ったHeaderを呼び出すことができます。

src/js/components/Layout.js
import React from "react";
import Header from "./Header"
import Footer from "./Footer";

export default class Layout extends React.Component{
    render(){
        const title = "Welcome Moeto!";
        return(
            <div>
                <Header title={title}/>
                <Header title={"Thank You!"}/>

                <Footer />
            </div>
        );
    }
}

HeaderコンポーネントからTitleコンポーネントへPropsを渡してみましょう。

src/js/components/Header.js
import React from "react";
import Title from "./Header/Title"

export default class Header extends React.Component{
    render(){
        console.log(this.props);
        return (
            <div>
                <Title title={this.props.title}/>
            </div>
        );
    }
}
src/js/components/Title.js
import React from "react";

export default class Title extends React.Component{
    render(){
        return (
            <h1>{this.props.title}</h1>
        )
    }
}

これでブラウザを見てみると

Welcome Moeto!

Thank you!
footer

と書かれていますね!

input要素

input要素を追加するとブラウザで表示した人にデータ入力を要求し、データを受け取ります。

Header.jsにinput要素を追加。

src/js/components/Header.js
import React from "react";
import Title from "./Header/Title"

export default class Header extends React.Component{
    render(){
        console.log(this.props);
        return (
            <div>
                <Title title={this.props.title}/>
                <input />
            </div>
        );
    }
}

はい、これでブラウザ見てみると
なんか空白のテキストボックスができており、文字を入力できるのが分かります。
今はinput要素を付けただけなので入力しても何も起きません。

ではこれから入力フォームに入ったテキストを取得してタイトルを表示する処理をだんだんと追加していきます。

inputの中身を取得

Layout.jschageTitleメソッドを作成し、changeTitleメソッドHeaderコンポーネントへ渡すようにしてみましょう、
と書かれていますね。カタカナアレルギーの人はここで挫折しそう笑
 
一旦ここでは写経することにしましょう!
こういうのは成果物ができた後に意味がわかるってもんです

src/js/components/Layout.js
import React from "react";
import Header from "./Header"
import Footer from "./Footer";

export default class Layout extends React.Component{
        constructor() {
            super();
            this.state = {title:"Welcome"};
        }
        changeTitle(title){
            this.setState({title});
        }
        render(){
        return(
            <div>
                <Header changeTitle={this.changeTitle.bind(this)} title={this.state.title} />
                <Footer />
            </div>
        );
    }
}

さっき説明を飛ばしましたが、ReactのPropsに関数(=メソッド)を指定することも可能です。
今回のchangeTitle PropsにはchangeTitleメソッドを呼び出していますね。
ん、よくみてみると
this.changeTitle.bind(this)
bind?誰?

これは関数のスコープの問題らしいです。
これってLayout.jsからHeader.jsに渡してるじゃないですか。
これbindなしでthis.changeTitle(this)でもHeader.js内で呼び出せるんだけどLayout.jsでのchangeTitleとはまた別な関数になってしまうんだそうです。
そーすると
changeTitle(title){
this.setState({title});
}
のthisがLayoutではなくなってしまい予想外の動作をする可能性がある。
bindを使うことで確実にLayoutからHeaderに渡すことができているんですね!

bindの謎が解けたところでHeader.jsの編集。

src/js/components/Header.js
import React from "react";
import Title from "./Header/Title"

export default class Header extends React.Component{
    handleChange(e){
        const title = e.target.value;
        this.props.changeTitle(title);
    }
    render(){
        console.log(this.props);
        return (
            <div>
                <Title title={this.props.title}/>
                <input value={this.props.title} onChange={this.handleChange.bind(this)}/>
            </div>
        );
    }
}

ここまで書いたらテキストボックスになにか入力してみましょう!

入力したその場でタイトルが書き変わりますね!

(2019/12/18作成。)

以降の日誌

day3作成中...12/18

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

tailwndcss+React でカードデザインのポートレートサイトを作ってみる

はじめに

Reactでカードを並べたポートレートサイトを作ってみたかったので、tailwindcssを用いて実装してみます。

イメージこんな感じ。
image.png
https://scrapbox.io/help-jp/

開発環境

VsCode
npm 6.12.1

tailwindcss
react-router-dom

セットアップ

必要なライブラリがインストールされている前提で進めていきます。

コンポーネント設計

index.js
--App.js
----HomePage.js
------ContentCard.js   //カードコンポーネント
--------SNSSample.js  //カードから遷移する先のコンポーネント
--------ShopSample.js  //カードから遷移する先のコンポーネント

カードから遷移する先のコンポーネントを作成

先に遷移先のコンポーネントを作っておきましょう。

ShopPage.js
import React from "react";

export function ShopPage() {
  return (
    <div className="ShopPage">
        ShopPage is working!
    </div>
  );
}

SNSPage.js
import React from "react";

export function SNSPage() {
  return (
    <div className="SNSPage">
        SNSPage is working!
    </div>
  );
}

App.jsでのルーティング

次にApp.jsで、作成したコンポーネントに対して、ルーティングを行ってやります。

App.js
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { HomePage } from "./HomePage";
import { SNSPage } from './SNSPage';
import { ShopPage } from './ShopPage';
import { Header } from './Header';


function App() {
  return (
    <Router>
      <div className="App">
        <Header/>
        <Route exact path="/" component={HomePage} />
        <Route path="/sns" component={SNSPage} />
        <Route path="/shop" component={ShopPage} />
      </div>
    </Router>
  );
}

export default App;

BrowserRouterタグでRouteタグを囲うことで、タグ内でのルーティングを指定できます。

カードコンポーネントを作成する。

次に、並べる用のカードコンポーネントを作成していきます。

ContentCard.js
import React from "react";

export function ContentCard(props) {
  return (
    <div className="ContentCard p-4">
      <div class="max-w-sm rounded overflow-hidden shadow-lg text-center">
        <img
          class="w-full"
          src="https://source.unsplash.com/random/1600x900/"
          alt="Sunset in the mountains"
        ></img>
        <div class="px-6 py-4">
        <div class="font-bold text-xl mb-2">{props.pageName}</div>
          <p class="text-gray-700 text-base">
            {props.description}
          </p>
        </div>
      </div>
    </div>
  );
}

こいつは汎用的に使いたかったので、親コンポーネントからpropsを受け取り、そいつを表示するだけという作りになってます。

HomePage.jsからカードコンポーネントを呼び出す。

次にHomePage.jsから、作成したCardContents.jsにpropsを渡しつつ呼び出しましょう。

HomePage.js
import React from "react";
import { ContentCard } from "./ContentCard";
import { Link } from "react-router-dom";

export function HomePage() {
  return (
    <div className="HomePage flex mb-4">
      <Link to="/sns" className="w-1/3">
        <ContentCard
          pageName="SNS"
          pageUrl=""
          cmpName=""
          imgSrc=""
          description="SNSSample page!"
        />
      </Link>
      <Link to="/shop" className="w-1/3">
        <ContentCard
          pageName="Shop"
          pageUrl=""
          cmpName=""
          imgSrc=""
          description="ShopSample page!"
        />
      </Link>
    </div>
  );
}

呼び出す際にLink toを用いて、クリック時遷移するようにしてます。

動作確認

ここまでできれば完了です。
サーバを起動して確認してみましょう。

npm start

image.png

こんな感じになってると思います。
2つだけだと寂しいので、カード増やしてヘッダーつけるといい感じになります。
image.png
(画像が全部一緒なのは大目に見て。。)

終わりに

githubにコード挙げてあるので、こうしたほうがいいよ!ってあったら教えてください!
https://github.com/Anno328/Dev/tree/master/portrait/src

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

フルスタックエンジニアへの道(CakePHP/React)

はじめに

こんにちは、 @IZUMIRU0313 です。
ランサーズ Advent Calendar 2019 23日目の記事です。

法人向けの社外人材活用サービス「Lancers Enterprise」のフルスタックエンジニアです。
まだよわよわなので恐縮ですが、api blueprintでAPI仕様書、CakePHPでAPI、ReactでUIを実装しています?

想定する読者は、サーバーサイドエンジニアでフロントエンド(React)も学習していこうとしている方です。

Lancers Enterprise

エンジニア経歴

学生時代は、主にRails、Swift、AWS(EC2、S3)、Heroku、WordPressを利用して、サービス開発やインターンに取り組んでいました。特に以下2つのサービスは、すべての設計および開発をやっていたため、努力は報われると今日でも思える貴重な経験になっています。
フロントは、SassとjQueryが多少書けるレベルでした?

学生時代

ランサーズには、SREとしてジョインしました。当時、ターミナルはgitと多少のコマンドを知っているレベルであり、@yakitori009さんに、何から何まで教えていただきながら取り組んでいました?‍♂️
LPICでインプットしながら取り組んでいたため、座学と実務の両輪が上手く回せていました。

  • 踏み台サーバーの移行
  • Let's Encryptワイルドカード証明書の導入
  • AutoScaling
  • AutoScaling中ではデプロイ不可
  • docker-compose対応
  • MySQLコンテナ、WordPressコンテナの構築
  • MySQLのバージョンアップ5.6->5.7
  • LambdaでGitHubとChatworkの連携
  • LambdaでAthenaのload partitionを自動実行

その後、サーバーサイドエンジニアとしてCakePHPでプラットフォームの開発をすることにしました。まともにチーム開発とCakePHPを書くのは初めてだったため、@waldo0515さんや@numanomanuさん、井上さんにシステム設計からプロジェクトマネジメント、コーディングに渡るまで大変お世話になりました?‍♂️
インプットは、オブジェクト指向やドメイン駆動設計、クリーンアーキテクチャ、リーダブルコード等に努めました。

JavaScriptの習得

正直まだまだ未熟であり器用貧乏になる可能性も大いにあるのですが、自分が目指したいエンジニア像のために本格的にJavaScriptに力を入れることにしました?
まずは、半年後業務でReactを書けるレベルになることを目標に、GASでの個人開発から始めました。SREの際にLambdaでnode.jsを書いていたこともあり、特に詰まることなく開発できました。
インプットは、改訂新版JavaScript本格入門を読んでいました。

Reactの習得

ES6のお作法や非同期通信の変遷等も一通り理解することができたので、本格的にReactの学習を始めました。
元々副業や個人開発でVueやNuxtを触る機会があったのですが、個人的にはReactの方が学習ハードルが高かった印象です。
特にJSX(TSX)、TypeScript、Redux、redux-sagaは業務で開発するまで理解できませんでした。

半年ほど、@intrudercl14さんと@takepo0928さんにキャリアやJavaScript、Reactのアドバイスをいただき、なんとか「Lancers Enterprise」の開発にジョインすることができました?‍♂️

特にりあクト!は、対話形式で先輩エンジニアが後輩エンジニアに教えるというストーリーなので、非常に読みやすくオススメです。

りあクト!

またVue、React、React(Redux)で同じアプリケーションを実装することは、共通点と相違点を把握でき学習促進に繋がったのでオススメです。

comparison-vue-react-redux

Reactの学習と合わせて、APIの学習にも努めました。APIは学生時代のサービスでRailsでAPIを生やし、Swiftでキャッチするという経験等はありましたが、なんちゃってAPIレベルだったので1から学習しました。

展望

ReactやTypeScriptの学習は継続していますが、ReactNativeやFlutterの学習もし始めたため、@sayanetさんと@terukuraさんとともに「Lancers Enterprise」をより良くした後は、アプリの改善にコミットできたらと考えています。

またモチベーション高く学習するには、自分の性格を理解することが大事だなと非常に思いました。家だと怠惰なので仕事終わり必ずカフェに行く、まずは簡単なアプリケーションを開発した後に体系だった書籍で質を上げていく等。長くなりそうなので、個人開発のすゝめ的な記事は別途書けたら良いなと思います。

QiitaいいねやTwitterフォローは励みになります?

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

WordPress ブロックエディター(Gutenberg)ブロック一覧

WordPress ブロックエディター(Gutenberg)のコアブロック一覧です。
WordPress 5.3 時点。

一般ブロック(common)

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/paragraph 段落
core/heading 見出し ○(文字色のみ)
core/image 画像
core/gallery ギャラリー
core/list リスト
core/quote 引用
core/audio 音声
core/cover カバー
core/file ファイル
core/video 動画

フォーマット(formatting)

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/code コード
core/freeform クラシック
core/html カスタムHTML
core/preformatted 整形済み
core/pullquote プルクオート
core/table
core/verse

カラム

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/columns カラム
core/button ボタン
core/group グループ ○(背景色のみ)
core/media-text メディアとテキスト ○(背景色のみ)
core/more 続きを読む
core/nextpage 改ページ
core/separator 区切り ○(色のみ)
core/spacer スペーサー

ウィジェット

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/shortcode ショートコード
core/archives アーカイブ
core/calendar カレンダー
core/categories カテゴリー
core/latest-comments 最近のコメント
core/latest-posts 最近の記事
core/rss RSS
core/search 検索
core/tag-cloud タグクラウド

埋め込み

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/embed 埋め込み
core-embed/twitter Twitter
core-embed/youtube YouTube
core-embed/facebook Facebook
core-embed/instagram Instagram
core-embed/wordpress WordPress
core-embed/soundcloud SoundCloud
core-embed/spotify Spotify
core-embed/flickr Flickr
core-embed/vimeo Vimeo
core-embed/animoto Animoto
core-embed/cloudup Cloudup
core-embed/collegehumor CollegeHumor
core-embed/dailymotion Dailymotion
core-embed/funnyordie Funny or Die
core-embed/hulu Hulu
core-embed/imgur Imgur
core-embed/issuu Issuu
core-embed/kickstarter Kickstarter
core-embed/meetup-com Meetup.com
core-embed/mixcloud Mixcloud
core-embed/photobucket Photobucket
core-embed/polldaddy Polldaddy
core-embed/reddit Reddit
core-embed/reverbnation ReverbNation
core-embed/screencast Screencast
core-embed/scribd Scribd
core-embed/slideshare Slideshare
core-embed/smugmug SmugMug
core-embed/speaker-deck Speaker Deck
core-embed/ted TED
core-embed/tumblr Tumblr
core-embed/videopress VideoPress
core-embed/wordpress-tv WordPress.tv

変更等ありましたら編集リクエストください!

LIQUID BLOCKS Advent Calendar 2019

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

WordPress ブロックエディター(Gutenberg)ブロック一覧表

WordPress ブロックエディター(Gutenberg)のコアブロック一覧です。
WordPress 5.3 時点。

一般ブロック(common)

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/paragraph 段落
core/heading 見出し ○(文字色のみ)
core/image 画像
core/gallery ギャラリー
core/list リスト
core/quote 引用
core/audio 音声
core/cover カバー
core/file ファイル
core/video 動画

フォーマット(formatting)

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/code コード
core/freeform クラシック
core/html カスタムHTML
core/preformatted 整形済み
core/pullquote プルクオート
core/table
core/verse

カラム

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/columns カラム
core/button ボタン
core/group グループ ○(背景色のみ)
core/media-text メディアとテキスト ○(背景色のみ)
core/more 続きを読む
core/nextpage 改ページ
core/separator 区切り ○(色のみ)
core/spacer スペーサー

ウィジェット

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/shortcode ショートコード
core/archives アーカイブ
core/calendar カレンダー
core/categories カテゴリー
core/latest-comments 最近のコメント
core/latest-posts 最近の記事
core/rss RSS
core/search 検索
core/tag-cloud タグクラウド

埋め込み

名前 配置揃え 幅広/全幅 色設定 HTML アンカー
core/embed 埋め込み
core-embed/twitter Twitter
core-embed/youtube YouTube
core-embed/facebook Facebook
core-embed/instagram Instagram
core-embed/wordpress WordPress
core-embed/soundcloud SoundCloud
core-embed/spotify Spotify
core-embed/flickr Flickr
core-embed/vimeo Vimeo
core-embed/animoto Animoto
core-embed/cloudup Cloudup
core-embed/collegehumor CollegeHumor
core-embed/dailymotion Dailymotion
core-embed/funnyordie Funny or Die
core-embed/hulu Hulu
core-embed/imgur Imgur
core-embed/issuu Issuu
core-embed/kickstarter Kickstarter
core-embed/meetup-com Meetup.com
core-embed/mixcloud Mixcloud
core-embed/photobucket Photobucket
core-embed/polldaddy Polldaddy
core-embed/reddit Reddit
core-embed/reverbnation ReverbNation
core-embed/screencast Screencast
core-embed/scribd Scribd
core-embed/slideshare Slideshare
core-embed/smugmug SmugMug
core-embed/speaker-deck Speaker Deck
core-embed/ted TED
core-embed/tumblr Tumblr
core-embed/videopress VideoPress
core-embed/wordpress-tv WordPress.tv

変更等ありましたら編集リクエストください!

LIQUID BLOCKS Advent Calendar 2019

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

Reactで日付入力にカレンダー(DatePicker)を使う ※年をプルダウンで指定して元号も表示する

概要

Reactで日付入力にカレンダー(DatePicker)を使う。

実装

React Datepickerを使うと楽。

サンプル


年:プルダウン指定(元号付き)
月:プルダウン指定
<InputeDate /> で。

import getMonth from 'date-fns/getMonth';
import getYear from 'date-fns/getYear';
import ja from 'date-fns/locale/ja';
import React from "react";
import DatePicker, { registerLocale } from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";

// jaのロケールの設定が週頭が月曜始まりになっているので日曜始まりにする
ja.options.weekStartsOn = 0;
// ReactDatepickerのロケール登録
registerLocale('ja', ja);

class InputDate extends React.Component {
    state = {
        startDate: null
    };

    handleChange = date => {
        this.setState({
            startDate: date
        });
    };

    eraHandler = yearNow => {

        const generate = (era, startYear) => {
            let yearDsp = yearNow - startYear + 1;
            if (yearDsp === 1) {
                yearDsp = "";
            } else {
                yearDsp = ('00' + yearDsp).slice(-2);
            }
            return `${era}${yearDsp}年`;
        };

        if (yearNow >= 2019) {
            return generate('令和', 2019);
        }

        if (yearNow >= 1989) {
            return generate('平成', 1989);
        }

        if (yearNow >= 1926) {
            return generate('昭和', 1926);
        }

        if (yearNow >= 1912) {
            return generate('大正', 1912);
        }
    }

    render() {
        var startYear = 1912; // カレンダーに表示する最初の西暦(大正元年となる1912を指定)
        var futureListUp = 5; // カレンダーに表示する未来の年数
        var years = Array.from({ length: getYear(new Date()) - startYear + futureListUp }, (v, k) => k + startYear).reverse();
        const months = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];

        return (
            <React.Fragment>
                <DatePicker
                    locale='ja'
                    selected={this.state.startDate}
                    onChange={this.handleChange}
                    placeholderText="日付を選択してください"
                    dateFormat="yyyy/MM/dd"
                    isClearable
                    showMonthDropdown
                    showYearDropdown
                    todayButton="今日"
                    dropdownMode="select"
                    // カレンダーのヘッダ部分をカスタマイズする
                    renderCustomHeader={({
                        date,
                        changeYear,
                        changeMonth,
                        decreaseMonth,
                        increaseMonth,
                        prevMonthButtonDisabled,
                        nextMonthButtonDisabled
                    }) => {
                        return (
                            <div
                                style={{ margin: 10, display: "flex", justifyContent: "center" }}>

                                {/* 前月ボタン */}
                                <button onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>{"<"}</button>

                                {/* 年の部分 */}
                                <select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)} >
                                    {years.map((option) => (
                                        // eraHandler()で年のプルダウンに元号を付ける
                                        <option key={option} value={option}>{option}年({this.eraHandler(option)}</option>
                                    ))}
                                </select>

                                {/* 月の部分 */}
                                <select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))} >
                                    {months.map(option => (
                                        <option key={option} value={option}> {option}</option>
                                    ))}
                                </select>

                                {/* 次月ボタン */}
                                <button onClick={increaseMonth} disabled={nextMonthButtonDisabled}>{">"}</button>
                            </div>
                        );
                    }
                    }
                />
            </React.Fragment>
        );
    }
}

参考

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

React(Hooks)で認証付きリアルタイムチャットアプリを作ってみた。

はじめに

Reactの勉強を始めて、Hooksを使ってchatアプリを作成してみました。
一度作ってからこの記事を作成したので、説明する順序がおかしいと感じる点があるかもしれません。
特に注意して作成した箇所をまとめてみたので全体のコードを確認したい場合は
https://github.com/m-shichida/chat_app_with_react_hooks
ここにコード置いています。作成途中ですが、最低限の

  • ログインユーザーの管理
  • メッセージの送受信、表示

はできています。
firebaseの設定等は割愛します。

  • React(Hooks)
  • firebase
  • ちょこっとRedux(複数のstate管理のためcombineReducers)

使いました。
修正点あれば教えてください。

下準備

まずこのチャットアプリでは

  • ①ログインユーザーの情報管理
  • ②送受信されたメッセージの管理

この2つのstate/dispatch(stateの状態を変更させる動き)を管理させるため、複数のstate/dispatch管理と、全ての階層でstate/dispatchが使えるようにしなくてはなりません。

そのため、components/App.jscomponentsのルートファイル)に

  • stateの初期値
  • reducers内のstateを変更させるdispatchを使用するとの宣言
  • ③全ての階層でstate/dispatchが使用できるようにする。

この3つを記述しました。

components/App.js
function App() {
  // ①stateの初期値の設定
  const initialState = {
    currentUserInfos: '',
    messages: []
  }
  // ②reducers内のstateの状態を更新する関数を呼び込む。
  const [state, dispatch] = useReducer(reducers, initialState);

  // ここはあとで出てきます。
  useEffect(() => {
    firebaseDb.ref('messages/').on('child_added', (snapshot) => {
      const messages = snapshot.val()
      dispatch({
        type: SET_MESSAGES,
        messages
      })
    });
  }, [])

  // ページリロードしてもログインしているユーザーの情報をlocalStorageから持ってくる。
  useEffect(() => {
    dispatch({
      type: SET_CURRENT_USER_INFO_FROM_LOCALSTORAGE
    })
  }, []);

  return (
    // ③AppContextに囲まれたコンポーネントでuseReducerで定義したstate, dispatchが使えるよう宣言する。
    <AppContext.Provider value={ { state, dispatch } }>
      { state.currentUserInfos ? (<MainContent />) : (<Login />) }
    </AppContext.Provider>
  );
}

export default App;

そして複数のstate/dispatchを管理できるようにするためにReduxのcombineReducersを使用します。

reducers/index.js
import { combineReducers } from 'redux';
import currentUserInfos from './currentUserInfos';
import messages from './messages';

// 複数のstateとdispatchを管理することが可能になる。
export default combineReducers({
  messages,
  currentUserInfos
})

ログイン状態を保持する

components/login.js
import React, { useContext } from 'react';
import AppContext from '../contexts/AppContext';
import { Button, Card, CardActions, CardContent, Grid } from '@material-ui/core';
import firebase from 'firebase';
import { firebaseApp } from '../firebase/index';
import GoogleLoginImage from '../images/btn_google_signin.png';
import { ADD_CURRENT_USER_INFO } from '../actions';

const Login = () => {
  // AppContextからstateの状態を変更させるdispatchを呼び出す。
  const { dispatch } = useContext(AppContext);

  // 匿名ユーザーとしてログインする。
  const loginAsAnonymousUser = e => {
    e.preventDefault();
    firebaseApp.auth().signInAnonymously().catch(function(error) {
      const errorCode = error.code;
      const errorMessage = error.message;
      alert(`エラーが発生しました。エラーコード${ errorCode }:${ errorMessage }`)
    });

    firebaseApp.auth().onAuthStateChanged(function(user) {
      if (user) {
        const uid = user.uid;
        const name = `ゲストユーザー${ uid }`
        dispatch({
          type: ADD_CURRENT_USER_INFO,
          uid,
          name
        });
        alert(`${ name }としてログインしました。`)
      }
    });
  }

  // Googleアカウントを利用してログインする。
  const loginAsGoogleAccount = () => {
    let provider = new firebase.auth.GoogleAuthProvider();
    firebaseApp.auth().signInWithPopup(provider).then(function(result) {
      const user = result.user;
      const uid = user.uid
      const name = user.displayName;
      const image = user.photoURL
      dispatch({
        type: ADD_CURRENT_USER_INFO,
        uid,
        name,
        image
      })
    }).catch(function(error) {
      const errorCode = error.code;
      const errorMessage = error.message;
      alert(`エラーが発生しました。エラーコード${ errorCode }:${ errorMessage }`)
    });
  }

  return (
    <Grid
      container
      justify='center'
      style={ { position: 'fixed', top: '35%' } }
    >
      <Card style={ { width: '300px' } }>
        <CardContent>
          <h3>ログインして利用する</h3>
        </CardContent>
        <CardActions style={ { display: 'flex', flexDirection: 'column' } }>
          <Button
            className='loginBtn'
            variant='contained'
            color='primary'
            onClick={ loginAsAnonymousUser }
            style={ { marginBottom: '8px' } }
          >
            匿名ログイン
          </Button>
          <img
            className='loginBtn'
            alt='GoogleLoginImage'
            style={ { cursor: 'pointer' } }
            src={ GoogleLoginImage }
            onClick={ loginAsGoogleAccount }
          />
        </CardActions>
      </Card>
    </Grid>
  )
};

export default Login;

匿名ログイン、グーグルアカウントを利用してのログイン両方とも、ログインに成功してもstate管理しなければ、ログイン状態を保持することができないため、dispatchを使ってstate管理します。

またログイン後にページをリロードしてしまうとこれもまたログイン情報を管理したstateが全てなくなってしまうので、stateの更新とともにlocalstorageに保存します。

login.js
dispatch({
  type: ADD_CURRENT_USER_INFO,
  uid,
  name,
  image // 匿名ログインのときはいらないです。
})

このdispatchが以下のcurrentUserInfosに渡ります。state/dispatchが一つだけの管理で済むのであれば、格納場所はreducers/index.jsで大丈夫です。

reducers/currentUserInfos.js
import { ADD_CURRENT_USER_INFO, DELETE_CURRENT_USER_INFO, SET_CURRENT_USER_INFO_FROM_LOCALSTORAGE } from '../actions';
import { APP_KEY } from '../shared';

const currentUserInfos = (state = [], action) => {
  switch(action.type) {
    case ADD_CURRENT_USER_INFO:
      const image = action.image ? action.image : '';
      const params = { uid: action.uid, name: action.name, image }
      localStorage.setItem(APP_KEY, JSON.stringify(params)) // localstorageにログイン情報を保存する。
      return params
    case DELETE_CURRENT_USER_INFO:
      localStorage.removeItem(APP_KEY)
      return '' // localStorageに保存したログイン情報を削除し、stateを空で返す。
    case SET_CURRENT_USER_INFO_FROM_LOCALSTORAGE:
      const currentUserInfo = JSON.parse(localStorage.getItem(APP_KEY, JSON.stringify(state)))
      return currentUserInfo
    default:
      return state
  }
}

export default currentUserInfos;

localStorageに使ったこのAPP_KEYの中身は別のフォルダ(shared)に格納しています。
ディレクトリはsharedではなくhelpersの方が多いかな?

shared/index.js
export const APP_KEY = 'currentUserInfo';

メッセージを送信する

次にmessages.js
dispatch内でstate管理をするとともに、firebaseへメッセージの情報を格納しました。

reducers/messages.js
import { ADD_MESSAGE, SET_MESSAGES } from '../actions'
import currentDate from '../shared';
import { firebaseDb } from '../firebase';

const messages = (state = [], action) => {
  switch(action.type) {
    case ADD_MESSAGE:
      // state管理する
      const message = { uid: action.uid,
                        userImage: action.image,
                        content: action.content,
                        createdAt: action.createdAt ? action.createdAt : currentDate() }
      // firebaseにメッセージ情報を保存する。
      firebaseDb.ref('messages/').push(message);
      return [...state, { ...message }]
    case SET_MESSAGES:
      return [...state, action.messages]
    default:
      return state
  }
}

export default messages;

データに変更があった時

firebaseではデータに変更があった場合に

firebaseDb.ref('messages/').on('child_added', (snapshot)

でリアルタイムでデータを取ってくることができます。

components/App.js
// あとで出てくると書いてあったとこ。
// メッセージのデータに変更があった場合、'初回の一回だけ'stateを更新する。
  useEffect(() => {
    firebaseDb.ref('messages/').on('child_added', (snapshot) => {
      const messages = snapshot.val()
      dispatch({
        type: SET_MESSAGES,
        messages
      })
    });
  }, [])

components/Messages.js(複数メッセージを表示させるコンポーネント)にこれを書くのかな、と思ったのですが、ボトムナビゲーションなどを使用している時にページ切り替えのたびにMessagesコンポーネントが呼ばれることになり、何度も重なってメッセージが呼ばれることになります。
そのため、一回だけ呼び出させるコンポーネント、components/App.jsにこれを定義しました。

終わり

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

Webアプリ完成するまで続ける開発日誌<day1>

こんにちは!山形大学のもえとです!
とあるGLSで日本人のコミュニケーションストレスを少しでも解消したくITサービスを作っています。

しいたけ占いで天秤座の仕事運は発信力に連動とのことでしたのでQiitaでどんどん発信していきます
しいたけ占い2020年上半期:https://voguegirl.jp/horoscope/shiitake2020-h1/

noteも是非に。
https://note.com/se_n_pu_u_ki

対象レベル(自己紹介)

ProgateのHTML、CSS、Javascriptあたり2周とかしたことある
授業でC言語、自分でPythonなど

やったこと

Reactの勉強からはじめます。

この記事、とっても丁寧に書いてくださっています。
https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048
これを勉強していてエラーの対策などを書いたのが僕の記事です

前準備

だいたいエラーで苦労するのって前準備の環境構築のところですね。
まずはエラーコードをそのままググってみる癖をつけようって誰かが言ってました。

ワークスペースを作りましょう。
Macの方はターミナル.appを起動し以下をひとつひとつ入力してください。

$ mkdir react-tutorial
$ cd react-tutorial
$ mkdir -p src/js

mkdirは「make directory」の命令で、これを実行するとreact-tutorialという名前のファイルが作成されます。Finderでみてみてもちゃんとファイルが作られているのが分かります。
cdは「change directory」の命令で、これを実行するとカレントディレクトリ(ファイル構造の中の自分の現在地)を移動することができます。
mkdirのあとについている-pは、階層構造を1度に複数まで作ることができるオプションです。

react-tutorial
$ npm init

このコマンドを実行するといろいろ初期設定みたいなのがはじまります。

......
package name: (react-tutorial) 
version: (1.0.0) 
description: 
entry point: (index.js) webpack.config.js   
test command: 
git repository: 
keywords: 
author: Your Name
license: (ISC) 
......

おそらくここまでは問題なく進む。

この次の工程で僕はエラー祭りでした。

webpackのパッケージをインストールしているようです
$ npm install --save-dev webpack webpack-cli webpack-dev-server
$ npm install -g webpack webpack-cli
$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
$ npm install --save-dev react react-dom

これの1つ目を実行したところでエラーが大量に出てきました。
まずはエラーコードを見てみると前半の方に
No Xcode or CLT version detected!
と書かれていましたのでこのままコピペしてググってみました。

→たどり着いたサイト:https://qiita.com/UTA6966/items/6f8b1fd21c2dc9591488

実行したコマンドを載せます。

CommandLineToolsの設定変更
$ nodebrew install 8
$ sudo xcode-select --switch /Applications/Xcode.app
$ npm insatall node-gyp

おそらく2つ目のやつが必要だったっぽいです。

Command Line Toolsとは...
Macのコマンドを実行するためのコマンドツール群らしく、homebrew等のプログラムのインストールにも使うそうです。

んでここの

$ sudo xcode-select --switch /Applications/Xcode.app

Command Line ToolsをXcode同梱版に設定した、ということのようです。これによって無事エラーなく実行することができました。
あと、

無視していいエラーコード
npm WARN react-tutorial@1.0.0 No description
npm WARN react-tutorial@1.0.0 No repository field.

こんなエラーコードが出るかと思いますがこれは無視して大丈夫だそうです。

では気を取り直して次!

webpack.config.jsファイルを作る
$ touch webpack.config.js

touchコマンドはファイルを作ることができるコマンド。

webpack.confing.jsのファイルに「バンドリングルール」を書いていくそうです。
バンドリングルールとは、おそらくweb読み込むときに「ここにこれがあるよ!」とかを教えてあげるためのモノな気がします()

バンドリングルール書いていきましょう

webpack.config.js
var debug   = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path    = require('path');

module.exports = {
  context: path.join(__dirname, "src"),
  entry: "./js/client.js",
  module: {
    rules: [{
      test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        use: [{
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env']
          }
        }]
      }]
    },
    output: {
      path: __dirname + "/src/",
      filename: "client.min.js"
    },
    plugins: debug ? [] : [
      new webpack.optimize.OccurrenceOrderPlugin(),
      new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
    ]
};

これの書き方などは別途後で調べます。
メモ:https://original-game.com/how-to-use-webpack-config-js/

続いて、src/index.htmlを作成

$ touch ./src/index.html

そしてhtmlファイルに次のように書き込みましょう。

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>React Tutorials</title>
    <!-- change this up! http://www.bootstrapcdn.com/bootswatch/ -->
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css" rel="stylesheet"/>
  </head>

  <body>
    <div id="app"></div>
    <script src="client.min.js"></script>
  </body>
</html>

htmlファイルの中でclient.min.jsを読み込むように指定しています。webpackによって生成される、必要最低限のコードになったclient.jsファイルのようです。

client.jsを書いてあげればそれをhtmlが読みこんで表示してくれるのですね。
では早速client.jsを書いていきましょう。

やっとReact初登場

src/js/client.js
import React from "react";
import ReactDOM from "react-dom";

class Layout extends React.Component {
  render() {
    return (
      <h1>Welcome!</h1>
    );
  }
}

const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);

このファイル含めReactの理解は後でするとして、ここまででとりあえず準備完了のため
webpackコマンドからclient.min.jsファイルを作成したあとにindex.htmlファイルを開きましょう。

$ webpack --mode development

これでclient.jsなどをコンパイルしました。

じゃあ早速chromeに表示しよう!
開発用webサーバを起動していきます

$ ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --content-base src --mode development

これを実行すると開発用webサーバが起動し、htmlファイルなどに変更を加えるたびに画面が更新されます

chromeなどで
http://localhost:8080
とURLにうってみてください!

It works!

と表示されていたら成功です!

また
./src/js/index.jsに変更を加えてみましょう

src/js/index.js
 import React from "react";
 import ReactDOM from "react-dom";

 class Layout extends React.Component {
   render() {
     return (
       <h1>Welcome!</h1>  
     );
   }
 }

 const app = document.getElementById('app');
 ReactDOM.render(<Layout/>, app);

It works!をWelcome!にしました

こうして保存すると

Welcome!

と変更されました!

前準備終了!

やっとReactの文法などについて始まります。

以降の日誌

 
day2作成中...(12/18)

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

<day1>Webアプリ完成するまで続ける開発日誌

こんにちは!山形大学のもえとです!
とあるGLSで日本人のコミュニケーションストレスを少しでも解消したくITサービスを作っています。

しいたけ占いで天秤座の仕事運は発信力に連動とのことでしたのでQiitaでどんどん発信していきます
しいたけ占い2020年上半期:https://voguegirl.jp/horoscope/shiitake2020-h1/

noteも是非に。
https://note.com/se_n_pu_u_ki

対象レベル(自己紹介)

ProgateのHTML、CSS、Javascriptあたり2周とかしたことある
授業でC言語、自分でPythonなど

やったこと

Reactの勉強からはじめます。

この記事、とっても丁寧に書いてくださっています。
https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048
これを勉強していてエラーの対策などを書いたのが僕の記事です

前準備

だいたいエラーで苦労するのって前準備の環境構築のところですね。
まずはエラーコードをそのままググってみる癖をつけようって誰かが言ってました。

ワークスペースを作りましょう。
Macの方はターミナル.appを起動し以下をひとつひとつ入力してください。

$ mkdir react-tutorial
$ cd react-tutorial
$ mkdir -p src/js

mkdirは「make directory」の命令で、これを実行するとreact-tutorialという名前のファイルが作成されます。Finderでみてみてもちゃんとファイルが作られているのが分かります。
cdは「change directory」の命令で、これを実行するとカレントディレクトリ(ファイル構造の中の自分の現在地)を移動することができます。
mkdirのあとについている-pは、階層構造を1度に複数まで作ることができるオプションです。

react-tutorial
$ npm init

このコマンドを実行するといろいろ初期設定みたいなのがはじまります。

......
package name: (react-tutorial) 
version: (1.0.0) 
description: 
entry point: (index.js) webpack.config.js   
test command: 
git repository: 
keywords: 
author: Your Name
license: (ISC) 
......

おそらくここまでは問題なく進む。

この次の工程で僕はエラー祭りでした。

webpackのパッケージをインストールしているようです
$ npm install --save-dev webpack webpack-cli webpack-dev-server
$ npm install -g webpack webpack-cli
$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
$ npm install --save-dev react react-dom

これの1つ目を実行したところでエラーが大量に出てきました。
まずはエラーコードを見てみると前半の方に
No Xcode or CLT version detected!
と書かれていましたのでこのままコピペしてググってみました。

→たどり着いたサイト:https://qiita.com/UTA6966/items/6f8b1fd21c2dc9591488

実行したコマンドを載せます。

CommandLineToolsの設定変更
$ nodebrew install 8
$ sudo xcode-select --switch /Applications/Xcode.app
$ npm insatall node-gyp

おそらく2つ目のやつが必要だったっぽいです。

Command Line Toolsとは...
Macのコマンドを実行するためのコマンドツール群らしく、homebrew等のプログラムのインストールにも使うそうです。

んでここの

$ sudo xcode-select --switch /Applications/Xcode.app

Command Line ToolsをXcode同梱版に設定した、ということのようです。これによって無事エラーなく実行することができました。
あと、

無視していいエラーコード
npm WARN react-tutorial@1.0.0 No description
npm WARN react-tutorial@1.0.0 No repository field.

こんなエラーコードが出るかと思いますがこれは無視して大丈夫だそうです。

では気を取り直して次!

webpack.config.jsファイルを作る
$ touch webpack.config.js

touchコマンドはファイルを作ることができるコマンド。

webpack.confing.jsのファイルに「バンドリングルール」を書いていくそうです。
バンドリングルールとは、おそらくweb読み込むときに「ここにこれがあるよ!」とかを教えてあげるためのモノな気がします()

バンドリングルール書いていきましょう

webpack.config.js
var debug   = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path    = require('path');

module.exports = {
  context: path.join(__dirname, "src"),
  entry: "./js/client.js",
  module: {
    rules: [{
      test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        use: [{
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env']
          }
        }]
      }]
    },
    output: {
      path: __dirname + "/src/",
      filename: "client.min.js"
    },
    plugins: debug ? [] : [
      new webpack.optimize.OccurrenceOrderPlugin(),
      new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
    ]
};

これの書き方などは別途後で調べます。
メモ:https://original-game.com/how-to-use-webpack-config-js/

続いて、src/index.htmlを作成

$ touch ./src/index.html

そしてhtmlファイルに次のように書き込みましょう。

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>React Tutorials</title>
    <!-- change this up! http://www.bootstrapcdn.com/bootswatch/ -->
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css" rel="stylesheet"/>
  </head>

  <body>
    <div id="app"></div>
    <script src="client.min.js"></script>
  </body>
</html>

htmlファイルの中でclient.min.jsを読み込むように指定しています。webpackによって生成される、必要最低限のコードになったclient.jsファイルのようです。

client.jsを書いてあげればそれをhtmlが読みこんで表示してくれるのですね。
では早速client.jsを書いていきましょう。

やっとReact初登場

src/js/client.js
import React from "react";
import ReactDOM from "react-dom";

class Layout extends React.Component {
  render() {
    return (
      <h1>Welcome!</h1>
    );
  }
}

const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);

このファイル含めReactの理解は後でするとして、ここまででとりあえず準備完了のため
webpackコマンドからclient.min.jsファイルを作成したあとにindex.htmlファイルを開きましょう。

$ webpack --mode development

これでclient.jsなどをコンパイルしました。

じゃあ早速chromeに表示しよう!
開発用webサーバを起動していきます

$ ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --content-base src --mode development

これを実行すると開発用webサーバが起動し、htmlファイルなどに変更を加えるたびに画面が更新されます

chromeなどで
http://localhost:8080
とURLにうってみてください!

It works!

と表示されていたら成功です!

また
./src/js/index.jsに変更を加えてみましょう

src/js/index.js
 import React from "react";
 import ReactDOM from "react-dom";

 class Layout extends React.Component {
   render() {
     return (
       <h1>Welcome!</h1>  
     );
   }
 }

 const app = document.getElementById('app');
 ReactDOM.render(<Layout/>, app);

It works!をWelcome!にしました

こうして保存すると

Welcome!

と変更されました!

前準備終了!

やっとReactの文法などについて始まります。

  
  
(2019/12/18作成。)

以降の日誌

【day2】同日更新
https://qiita.com/se_n_pu_u_ki/items/50a64ffc3f643e9fd15f

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

Redux公式のBasics TutorialでToDoリストを作ってみた

Reactで、状態管理ライブラリとしてよく使われるRedux。
使い方を調べようと、公式サイトにあるBasics TutorialでToDoリストを作りにチャレンジしてみた。
ただ、このチュートリアル、ステップ・バイ・ステップになっていないので、なんだかわかりにくい。

そこで、どんな内容なのか、簡単にメモを残します。

内容

チュートリアルは、こちら(英語)。

Redux単独のチュートリアルのように見えるけど、じつはReactと組み合わせてToDo Listを作っている。

以下は、(ほぼ)同じ内容を日本語でなぞっている。

こちらで、完成版のデモを試すことができる。

はじめ方

ReactとReduxで作るので、create-react-appする。

$ create-react-app basic-tutorial-todo
$ cd basic-tutorial-todo
$ npm install --save redux react-redux redux-logger

ソースコード

ToDoリストのソースコードは、こちらで入手できる。
チュートリアルだけ読んでいても、全体像がわかりにくい。
デモ版をみるか、create-react-appしたところにソースコードを流し込んだ方がいいと思う。

Example: Todo List · Redux
https://redux.js.org/basics/example

Github - reduxjs/redux
https://github.com/reduxjs/redux/tree/master/examples/todos

ディレクトリ構成

完成すると、こんな構成になる。意外と大きい。

/basic-tutorial-todo
│  index.js
│  serviceWorker.js
├─actions
│      index.js
├─components
│      App.js
│      Footer.js
│      Link.js
│      Todo.js
│      TodoList.js
├─containers
│      AddTodo.js
│      FilterLink.js
│      VisibleTodoList.js
└─reducers
        index.js
        todos.js
        visibilityFilter.js

実行する

$ npm start

参考になるページ

Basic exampleは、ちょっと大きいので、こちらにもう少し小さいサンプルがある。

そもそも、Reduxとは何なのか、これが分かりやすかった。

ReactとReduxの連携は、これが参考になった。

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

GraphQLでタスク管理アプリを作る -フロントエンド編- [React+Apollo Client+Typescript]

まえがき

バックエンド編とフロントエンド編の2つに分けて、GraphQL を使ったタスク管理アプリを作っていきます。
このバックエンド編では、React / Apollo Client / typescript によるGraphQLを使ったTODOアプリの実装をご紹介していきます。

この記事は@ebknさんの記事の後編です。
まだ読んでないというそこのあなた、ぜひご一読ください!(そして願わくばこの記事に戻ってきてくださいな)

このアプリのコードは公開しています。

こんな感じのTODOアプリを作ります。

2019-12-1843442.gif

主な技術要素

Apolloとは

Apollo は、フロントエンド/バックエンド両方に対応したGraphQLフレームワークです。
TECHNOLOGY RADAR (イマドキの技術を半年に一回まとめて紹介しているガイド) の 2019年 4月版 でも ADOPT 、つまり実際の開発現場投入に適している、と紹介されています。

今回はその中のフロントエンド用のフレームワーク、Apollo client を使った開発を紹介します。
これを使うことで、非常に簡単にGraphQL APIを使うことができます。

今回はReactを使いますが、他にもAngular、vueでも似たようなものが存在します。

:warning: 注意:@apollo/react-hooksreact-apollo-hooks

Apollo clientでHooksを使いたい!となって検索すると、たいていこの2つが出てきます。
結論、 @apollo/react-hooksを使ってください。
react-apollo-hooks はまだ公式からhooksサポートが出される前に、有志の方が作ってくださっていたものです。
今は公式がサポートしているので、deprecatedとなっています。
1年くらい前の解説サイトだと、react-apollo-hooksをつかっているサイトもちらほらあるので、混同しないようにお気をつけください。

実現される開発体験

GraphQLの強みである型システムの恩恵を全面に受け、型安全なReactアプリケーションをシュッっと作る。
手動でTypescriptの型定義することを可能な限り減らすことで、ヒューマンエラーの予防にもなる。

ディレクトリ構成

なるべく単純化するために、コンポーネントもそんなに分けていません。
実開発ではもう少し分けたほうがいいと思います。

$ tree -I node_modules
.
├── README.md
├── codegen.yml
├── package-lock.json
├── package.json
├── query.graphql
├── src
│   ├── components
│   │   ├── CompletedIcon.tsx
│   │   ├── CreateTaskModal.tsx
│   │   ├── Tasks.tsx
│   │   └── UpdateTaskModal.tsx
│   ├── generated
│   │   └── graphql.ts
│   ├── hooks
│   │   └── formHooks.ts
│   ├── html
│   │   └── index.html
│   ├── index.tsx
│   ├── lib
│   │   └── sleep.ts
│   ├── styles
│   │   └── main.css
│   └── types
│       └── index.d.ts
├── tsconfig.json
└── webpack.config.js

開発の流れ

  • 1: 必要パッケージのインストール / ビルドツール等の設定
  • 2: GraphQLのスキーマファイルから、GraphQLを使うためのHooks、型定義を自動生成する
  • 3: 自動生成されたコードを使ってReactコンポーネントを作っていく

では早速やっていきましょう!

1: 必要パッケージのインストール / ビルドツール等の設定

必要パッケージをインストールする(後述するpackage.jsonをコピペして npm i でも可)

npm i -D @babel/core @babel/preset-env @babel/preset-react @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @types/react @types/react-datepicker @types/react-dom @types/react-infinite-scroller @types/react-router @types/react-router-dom" @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-eslint babel-loader babel-polyfill css-loader dotenv-webpack eslint eslint-config-prettier eslint-loader eslint-plugin-graphql eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks html-loader html-webpack-plugin prettier style-loader ts-loader typescript webpack webpack-cli webpack-dev-server && npm i -S @apollo/react-hooks apollo-boost core-js date-fns graphql graphql-tag react react-app-polyfill react-datepicker react-dom react-infinite-scroller react-router react-router-dom semantic-ui-react

たくさんインストールします...ざっくり説明すると以下です。

  • webpack / babel / prettier / eslint / webpackのloader群
    • TypescriptのコードをJSにトランスパイルしたり、css moduleを使ったり、lint/auto-formatしたりするやつ
  • webpack-dev-server
    • hot reload
  • react / react-router等
    • react本体とルーターなど
  • react-infinite-scroller
    • 無限スクロールを簡単にするコンポーネント
  • semantic-ui-react
    • いい感じのフロントを作れるフレームワークの一つ、semantic-uiのreact版
  • apollo-boost / @apollo/react-hooks
    • Apollo client

余談: tslintがdeprecatedになる話

typescriptのlinterとしてtslintをお使いの方は結構いるのではないでしょうか。
実はこのtslint、公式から 2019年に非推奨になる ことがアナウンスされています。

⚠️ TSLint will be deprecated some time in 2019. See this issue for more details: Roadmap: TSLint → ESLint. If you're interested in helping with the TSLint/ESLint migration, please check out our OSS Fellowship program.

このアナウンスにもあるよう、今後はeslintを使っていきましょう。

もうすでにtslintで動いているプロジェクトがある場合は、「どのように移行すべきか」に関して記事を書いてくださっている方がいるので、そちら等を参照ください。
脱TSLintして、ESLint TypeScript Plugin に移行する

筆者もtslintからeslintに移行しましたが、移行するにあって大した手間や不具合は感じなかったです。


codegen.yml (graphql code generatorの設定ファイル)

codegen.yml
schema:
  # GraphQL APIサーバーのエンドポイント
  # この配列に@restや@localを使うクエリファイルを列挙することで、それらに関してもhooksを生成してくれる
  - http://localhost:3000/graphql
# GraphQLのクエリを書いたファイル(詳しくは後述)
documents: ["query.graphql"]
generates:
  # generatorで作成したいファイル名
  ./src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      # hooksを生成するための設定
      withHOC: false
      withComponent: false
      withHooks: true
      # gqlgenのcustom scalarをstringとして扱う
      scalars:
        Time: string
    hooks:
      # ファイルが生成されたあとに、eslintのauto-fixを自動で走らせる
      afterOneFileWrite:
        - npx eslint --fix

その他諸々の設定ファイル

package.json
package.json
{
  "name": "graphql-app-advent-calendar-2019",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.tsx",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "serve-dev": "npx webpack-dev-server --config webpack.config.js --inline --hot --port=8081 --content-base dist --open-page ."
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.6",
    "@babel/preset-react": "^7.7.4",
    "@graphql-codegen/cli": "^1.9.1",
    "@graphql-codegen/typescript": "^1.9.1",
    "@graphql-codegen/typescript-operations": "^1.9.1",
    "@graphql-codegen/typescript-react-apollo": "^1.9.1",
    "@types/react": "^16.9.16",
    "@types/react-datepicker": "^2.9.5",
    "@types/react-dom": "^16.9.4",
    "@types/react-infinite-scroller": "^1.2.1",
    "@types/react-router": "^5.1.3",
    "@types/react-router-dom": "^5.1.3",
    "@typescript-eslint/eslint-plugin": "^2.11.0",
    "@typescript-eslint/parser": "^2.11.0",
    "babel-eslint": "^10.0.3",
    "babel-loader": "^8.0.6",
    "babel-polyfill": "^6.26.0",
    "css-loader": "^3.3.2",
    "dotenv-webpack": "^1.7.0",
    "eslint": "^6.7.2",
    "eslint-config-prettier": "^6.7.0",
    "eslint-loader": "^3.0.3",
    "eslint-plugin-graphql": "^3.1.0",
    "eslint-plugin-prettier": "^3.1.2",
    "eslint-plugin-react": "^7.17.0",
    "eslint-plugin-react-hooks": "^2.3.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "prettier": "^1.19.1",
    "style-loader": "^1.0.1",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.3",
    "webpack": "^4.41.3",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  },
  "dependencies": {
    "@apollo/react-hooks": "^3.1.3",
    "apollo-boost": "^0.4.7",
    "core-js": "^3.5.0",
    "date-fns": "^2.8.1",
    "graphql": "^14.5.8",
    "graphql-tag": "^2.10.1",
    "react": "^16.12.0",
    "react-app-polyfill": "^1.0.5",
    "react-datepicker": "^2.10.1",
    "react-dom": "^16.12.0",
    "react-infinite-scroller": "^1.2.4",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2",
    "semantic-ui-react": "^0.88.2"
  }
}


.eslintrc.js
eslintrc.js
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "react-hooks",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  plugins: ["@typescript-eslint", "react-hooks"],
  overrides: [
    {
        files: ["**/*.tsx"],
        rules: {
            "react/prop-types": "off"
        }
    }
  ],
  parser: "@typescript-eslint/parser",
  env: { browser: true, node: true, es6: true },
  parserOptions: {
    sourceType: "module"
  },
  rules: {
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/no-explicit-any": 0,
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "react/display-name": 0,
  }
};


tsconfig.json
tsconfig.json
{
  "compilerOptions": {
    /* Basic Options */
    "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
    "module": "es6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
    "lib": [
      "es2018",
      "esnext",
      "dom"
    ] /* Specify library files to be included in the compilation. */,
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./frontend/dist" /* Redirect output structure to the directory. */,
    // "rootDir": "./frontend/src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
    // "composite": true,                     /* Enable project compilation */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
    "strictNullChecks": true /* Enable strict null checks. */,
    "strictFunctionTypes": true /* Enable strict checking of function types. */,
    "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
    "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
    "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
    "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,

    /* Additional Checks */
    "noUnusedLocals": true /* Report errors on unused locals. */,
    "noUnusedParameters": true /* Report errors on unused parameters. */,
    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
    // "baseUrl": "./frontend" /* Base directory to resolve non-absolute module names. */,
    // "paths": {} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    "typeRoots": [
      "node_modules/@types",
    ] /* List of folders to include type definitions from. */,
    "types": [
      "node"
    ] /* Type declaration files to be included in compilation. */,
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
  // "include": ["frontend/src/**/*"],
  // "exclude": ["node_modules", "frontend/dist"]
}


webpack.config.js
webpack.config.js
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.tsx",
  output: {
    path: path.join(__dirname, "/dist"),
    publicPath: "/",
    filename: "[hash].js"
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "src/html/index.html"
    })
  ],
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx"],
    modules: [path.join(__dirname, "src"), path.join(__dirname, "node_modules")]
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        enforce: "pre",
        exclude: /node_modules/,
        use: ["eslint-loader"]
      },
      {
        test: /\.(ts|tsx)?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: [
                ["@babel/preset-react"],
                [
                  "@babel/preset-env",
                  { useBuiltIns: "usage", targets: ">0.25%", corejs: 3 }
                ]
              ]
            }
          },
          {
            loader: "ts-loader",
            options: {
              configFile: "tsconfig.json",
              experimentalWatchApi: true
            }
          }
        ]
      },
      {
        test: /\.css$/,
        loaders: ["style-loader", "css-loader?modules"]
      }
    ]
  }
};


.node-version (nodeのバージョンが8系とかだとGraphql code generatorでエラーが出ました)
12.13.1


2: GraphQLのスキーマファイルから、GraphQLを使うためのHooks、型定義を自動生成する

次に、GraphQL Code Generatorを使ってHooks、型定義を自動生成していきましょう。
これが一連の開発のなかで一番GraphQL!!!神!!!となる瞬間です。

GraphQLのクエリを書いていく

この記事の前編@ebknさんが作ってくれたスキーマを元に、実際に使うクエリを書いていきましょう。
ざっとこんな感じです。

query.graphql
fragment taskFields on Task {
  id
  title
  notes
  completed
  due
}

query fetchTasks(
  $completed: Boolean
  $order: TaskOrderFields!
  $first: Int
  $after: String
) {
  tasks(
    input: { completed: $completed }
    orderBy: $order
    page: { first: $first, after: $after }
  ) {
    pageInfo {
      endCursor
      hasNextPage
    }
    edges {
      cursor
      node {
        ...taskFields
      }
    }
  }
}

mutation createTask(
  $title: String!
  $notes: String
  $completed: Boolean
  $due: Time
) {
  createTask(
    input: { title: $title, notes: $notes, completed: $completed, due: $due }
  ) {
    ...taskFields
  }
}

mutation updateTask(
  $taskID: ID!
  $title: String
  $notes: String
  $completed: Boolean
  $due: Time
) {
  updateTask(
    input: {
      taskID: $taskID
      title: $title
      notes: $notes
      completed: $completed
      due: $due
    }
  ) {
    ...taskFields
  }
}

GraphQL Code Generatorでコードを生成

以下のコマンドでコードを生成します

// APIサーバーを起動
$ cd backend && make start && cd ../frontend

$ npx graphql-codegen
  ✔ Parse configuration
  ✔ Generate outputs

生成されたコードがこちらです!と言いたいところなのですが、300行を超えるファイルなので、一部だけご紹介します。
このアプリのコードは公開しているので、気になる方は見てみてください。

graphql.ts
// ~~~省略~~~
export type Task = Node & {
  __typename?: "Task";
  id: Scalars["ID"];
  title: Scalars["String"];
  notes: Scalars["String"];
  completed: Scalars["Boolean"];
  due?: Maybe<Scalars["Time"]>;
};

// ~~~省略~~~

/**
 * __useFetchTasksQuery__
 *
 * To run a query within a React component, call `useFetchTasksQuery` and pass it any options that fit your needs.
 * When your component renders, `useFetchTasksQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useFetchTasksQuery({
 *   variables: {
 *      completed: // value for 'completed'
 *      order: // value for 'order'
 *      first: // value for 'first'
 *      after: // value for 'after'
 *   },
 * });
 */
export function useFetchTasksQuery(
  baseOptions?: ApolloReactHooks.QueryHookOptions<
    FetchTasksQuery,
    FetchTasksQueryVariables
  >
) {
  return ApolloReactHooks.useQuery<FetchTasksQuery, FetchTasksQueryVariables>(
    FetchTasksDocument,
    baseOptions
  );
}

// ~~~省略~~~

最高ですね、Taskの型定義や、fetchTasksのhooksが型付きで生成されています。

ちなみに、 query.graphql を書くときに存在しないフィールドを書いていたり、必須フィールドを飛ばしていたりすると、コード生成のタイミングで以下のように怒ってくれます。

$ npx graphql-codegen
  ✔ Parse configuration
  ❯ Generate outputs
    ❯ Generate ./src/generated/graphql.ts
      ✔ Load GraphQL schemas
      ✔ Load GraphQL documents
      ✖ Generate
        →         at query.graphql:15:3


 Found 1 error

  ✖ ./src/generated/graphql.ts
    AggregateError: 
        GraphQLDocumentError: Field "tasks" argument "input" of type "TasksInput!" is required, but it was not provided.
            at query.graphql:15:3
        at Object.checkValidationErrors (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-toolkit/commo
n/index.cjs.js:295:15)
        at Object.codegen (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/core/index.cjs.js:1
01:16)
        at processTicksAndRejections (internal/process/task_queues.js:93:5)
        at async process (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:770:56)
        at async Promise.all (index 0)
        at async /Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:777:37
        at async Task.task (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:570:17)
    AggregateError: 
        GraphQLDocumentError: Field "tasks" argument "input" of type "TasksInput!" is required, but it was not provided.
            at query.graphql:15:3
        at Object.checkValidationErrors (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-toolkit/commo
n/index.cjs.js:295:15)
        at Object.codegen (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/core/index.cjs.js:1
01:16)
        at processTicksAndRejections (internal/process/task_queues.js:93:5)
        at async process (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:770:56)
        at async Promise.all (index 0)
        at async /Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:777:37
        at async Task.task (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:570:17)


Something went wrong

良い...これでググっとヒューマンエラーが減らせそうです。
いよいよ次の章からコンポーネントを組み立てていきます。

3: 自動生成されたコードを使ってReactコンポーネントを作っていく

3-0: 今回説明しないファイルに関して

この記事ではApollo Clientがメインのため、スタイルやhtmlに関しては説明しません。
リポジトリには上げていますので、必要に応じてご確認ください。

3-1: Apollo Clientの初期化

まずはApollo Clientの初期化です。ついでにReact Routerも初期化します。尚今回使うパスの数は1つです。笑

// src/index.tsx

import React from "react";
import ReactDom from "react-dom";
import { Router } from "react-router";

import ApolloClient from "apollo-boost";
import { ApolloProvider } from "@apollo/react-hooks";

import { createBrowserHistory } from "history";
const history = createBrowserHistory();

// ここでApollo Clientの初期化
const client = new ApolloClient({
  uri: "http://localhost:3000/graphql"
});

export default function App() {
  return (
    <Router history={history}>
      <ApolloProvider client={client}>
        <div>something</div>
      </ApolloProvider>
    </Router>
  );
}

ReactDom.render(<App />, document.getElementById("app"));

GraphQLサーバーのエンドポイントを指定するだけです。
非常にシンプルですね。
もう少し色々書くと、Rest APIをGraphQLのクエリの中で呼び出せるapollo-link-rest用のRest APIエンドポイントもまとめて登録したり、websocketエンドポイントとつなぐこともできます。
もちろん、Authorization ヘッダを指定したり、401が帰ってきたときにjwtトークンを再発行するAPIを叩いてリトライ...なんてことも可能です。
公式のページ①, 公式のページ②等が参考になると思います。

3-2: taskの一覧ページを作る

最初にコンポーネントの全体を貼ります。以下に要所要所で説明していきます。

src/components/Tasks.tsx
// src/components/Tasks.tsx

import React, { useCallback, useState, useEffect, useMemo } from "react";

import {
  Header,
  Icon,
  List,
  Dimmer,
  Loader,
  Dropdown,
  DropdownProps
} from "semantic-ui-react";
import InfiniteScroll from "react-infinite-scroller";
import CreateTaskModal from "./CreateTaskModal";
import UpdateTaskModal from "./UpdateTaskModal";
import CompletedIcon from "./CompletedIcon";

import { formatRelative } from "date-fns";
import ja from "date-fns/locale/ja";
import {
  useFetchTasksQuery,
  TaskOrderFields,
  Task
} from "../generated/graphql";
import styles from "../styles/main.css";

type TaskFilterType = "all" | "completed" | "notCompleted";
const Tasks = () => {
  const [selectedTask, setSelectedTask] = useState<Task>();
  const [fetchMoreLoading, setFetchMoreLoading] = useState(false);
  const [taskFilterType, setTaskFilterType] = useState<TaskFilterType>("all");
  const [orderType, setOrderType] = useState<TaskOrderFields>(
    TaskOrderFields.Latest
  );

  const handleTaskFilterTypeChange = useCallback(
    (_: React.SyntheticEvent<HTMLElement, Event>, data: DropdownProps) => {
      setTaskFilterType(data.value as TaskFilterType);
    },
    []
  );
  const completedInput = useMemo(() => {
    switch (taskFilterType) {
      case "all":
        return null;
      case "completed":
        return true;
      case "notCompleted":
        return false;
    }
  }, [taskFilterType]);

  const handleOrderTypeChange = useCallback(
    (_: React.SyntheticEvent<HTMLElement, Event>, data: DropdownProps) => {
      setOrderType(data.value as TaskOrderFields);
    },
    []
  );

  const { data, error, fetchMore, refetch } = useFetchTasksQuery({
    variables: { order: TaskOrderFields.Latest, first: 5 },
    fetchPolicy: "cache-and-network"
  });

  useEffect(() => {
    refetch({ order: orderType, completed: completedInput, first: 5 });
  }, [completedInput, orderType, refetch]);

  const refetchAfterAdd = useCallback(() => {
    refetch({ order: orderType, completed: completedInput, first: 5 });
  }, [completedInput, orderType, refetch]);

  const handleLoadMore = useCallback(async () => {
    if (data && !fetchMoreLoading) {
      setFetchMoreLoading(true);
      await fetchMore({
        variables: {
          after: data.tasks.pageInfo.endCursor,
          order: orderType,
          completed: completedInput,
          first: 5
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          if (!fetchMoreResult) {
            return previousResult;
          }

          const newEdges = fetchMoreResult.tasks.edges;
          const pageInfo = fetchMoreResult.tasks.pageInfo;

          return {
            tasks: {
              ...previousResult.tasks,
              pageInfo,
              edges: [...previousResult.tasks.edges, ...newEdges]
            }
          };
        }
      });
      setFetchMoreLoading(false);
    }
  }, [completedInput, data, fetchMore, fetchMoreLoading, orderType]);

  const handleListItemClick = useCallback(
    (subscriber: Task) => () => {
      setSelectedTask(subscriber);
    },
    []
  );

  const handleModalClose = useCallback(() => {
    setSelectedTask(undefined);
  }, []);

  if (!data) {
    return (
      <Dimmer active={true}>
        <Loader>ロード中...</Loader>
      </Dimmer>
    );
  }

  if (error) {
    return <div>エラー</div>;
  }

  return (
    <div className={styles.main_content_box}>
      <Header color="teal" icon={true} textAlign="center">
        <Icon name="tasks" />
        <Header.Content>TODOs</Header.Content>
      </Header>
      <Dropdown
        options={[
          { value: "all", text: "すべて" },
          { value: "notCompleted", text: "未完了" },
          { value: "completed", text: "完了済み" }
        ]}
        value={taskFilterType}
        onChange={handleTaskFilterTypeChange}
        fluid={true}
        selection={true}
      />
      <div className={styles.order_dropdown}>
        <Dropdown
          options={[
            { value: TaskOrderFields.Due, text: "期限順" },
            { value: TaskOrderFields.Latest, text: "作成順" }
          ]}
          icon="sort amount up"
          value={orderType}
          onChange={handleOrderTypeChange}
        />
      </div>
      <InfiniteScroll
        loadMore={handleLoadMore}
        hasMore={data.tasks.pageInfo.hasNextPage}
        loader={
          <p style={{ textAlign: "center" }} key={0}>
            <Icon loading={true} name="spinner" />
          </p>
        }
      >
        <List selection={true} divided={true}>
          {data.tasks.edges.map(task =>
            task ? (
              <List.Item key={task.node.id}>
                <CompletedIcon task={task.node} />
                <List.Content onClick={handleListItemClick(task.node)}>
                  <List.Header>{task.node.title}</List.Header>
                  {task.node.due ? (
                    <List.Description>
                      <Icon name="time" />
                      {formatRelative(new Date(task.node.due), new Date(), {
                        locale: ja
                      })}{" "}
                      まで
                    </List.Description>
                  ) : null}
                </List.Content>
              </List.Item>
            ) : null
          )}
        </List>
      </InfiniteScroll>
      <CreateTaskModal refetch={refetchAfterAdd} />
      {selectedTask !== undefined ? (
        <UpdateTaskModal
          task={selectedTask}
          handleModalClose={handleModalClose}
        />
      ) : null}
    </div>
  );
};
export default Tasks;


いきなり長いですね〜、少しずつ紐解いていきましょう。

3-2-1: taskのシンプルな取得

まずはTasksを取得する部分です。

  const { data, error, fetchMore, refetch } = useFetchTasksQuery({
    variables: { order: TaskOrderFields.Latest, first: 5 },
    fetchPolicy: "cache-and-network"
  });

useFetchTasksQuery が前のステップで自動生成してくれたHooksですね。
この引数として、queryの変数やfetchPolicy(キャッシュを優先するか、postするかを決める)を指定しています。
queryの変数というのは、 query.graphql で言うところのこれ↓です。
今回はpaginationのための変数です。
graphQLの仕様として、!がついてないフィールドは nullable ということで、省略しても大丈夫です。

# ~~ 省略 ~~

query fetchTasks(
  $completed: Boolean            <--- これ!
  $order: TaskOrderFields!       <--- これ!
  $first: Int                    <--- これ!
  $after: String                 <--- これ!
) {
  tasks(orderBy: $order, page: { first: $first, after: $after }) {

# ~~ 省略 ~~

このHooksはコンポーネントが初期化するタイミングや、variablesの中身が変わったタイミングでfetchします。
fetchした結果やエラーの有無、今回は取っていませんがloadingなどが返り値としてゲットできます。(fetchMore, refetchに関しては後述)
ほしいtasksのデータはdataの中です。

次に、ロード状態の表示や、エラーの表示を記述します。
本来はloadingをhooksからもらうのが一般的ですが、諸々の都合で今回はdataが空の間はロード中とみなします。

// ~~ 省略 ~~

if (!data) {
    return (
      <Dimmer active={true}>
        <Loader>ロード中...</Loader>
      </Dimmer>
    );
  }

  if (error) {
    return <div>エラー</div>;
  }

// ~~ 省略 ~~

それができたら次は、取得したtasksをリストとして表示しましょう。
配列として入っているので、シンプルにmapすれば完了です。
CompletedIconという謎のコンポーネントや、List.Contentに渡しているハンドラに関しては後述します。

// ~~ 省略 ~~
        <List selection={true} divided={true}>
          {data.tasks.edges.map(task =>
            task ? (
              <List.Item key={task.node.id}>
                <CompletedIcon task={task.node} />
                <List.Content onClick={handleListItemClick(task.node)}>
                  <List.Header>{task.node.title}</List.Header>
                  {task.node.due ? (
                    <List.Description>
                      <Icon name="time" />
                      {/* date-fnsのformatRelative関数は 「あとOO日」や「OO日前に投稿」のような相対的な時間をいい感じに表示してくれる。localオプションによるローカライズも可能。 */}
                      {formatRelative(new Date(task.node.due), new Date(), {
                        locale: ja
                      })}{" "}
                      まで
                    </List.Description>
                  ) : null}
                </List.Content>
              </List.Item>
            ) : null
          )}
        </List>

// ~~ 省略 ~~

ここまでで、シンプルな一覧の取得・表示はできました!

3-2-2: 無限スクロールでタスクを表示する

先程のHooksが返してくれた値の中に、fetchMore という関数があります。
これはPaginationのための関数で、これを呼び出すことで更にタスクを取得してきてくれます。
実際に使っている部分はここ↓です。再取得時の変数(variables)と、取得した際にどのように既存データとマージするかを定義した関数(updateQuery)を渡します。

// ~~ 省略 ~~
await fetchMore({
        variables: {
          after: data.tasks.pageInfo.endCursor,
          order: orderType,
          completed: completedInput,
          first: 5
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          // previousResultがもともとのデータ、fetchMoreResultは再取得結果
          if (!fetchMoreResult) {
            return previousResult;
          }

          const newEdges = fetchMoreResult.tasks.edges;
          const pageInfo = fetchMoreResult.tasks.pageInfo;

          // 新しいデータを組み立てて返す
          return {
            tasks: {
              ...previousResult.tasks,
              pageInfo,
              edges: [...previousResult.tasks.edges, ...newEdges]
            }
          };
        }
// ~~ 省略 ~~

一旦これを定義すれば、あとはこの関数を再取得したいタイミングで呼び出すだけです。
今回はreact-infinite-scrollerを使っています。

// ~~ 省略 ~~
      <InfiniteScroll
        // loadMoreが再取得用の関数
        loadMore={handleLoadMore}
        hasMore={data.tasks.pageInfo.hasNextPage}
        loader={
          <p style={{ textAlign: "center" }} key={0}>
            <Icon loading={true} name="spinner" />
          </p>
        }
      >
// ~~ 省略 ~~

無限スクロールとかそれっぽくていいですね〜
次はフィルタリングと並び替えをやっていきます!

3-2-3: タスクのフィルタリングと並び替え

これも考え方は同じで、フィルタするための変数と並び替え用の変数をgraphQLに渡してfetchすればOKです。
ここでは、Hooksが返してくれるrefetchを使っていきます。その名の通り、読んだタイミングで再取得する関数です。
useEffectを使って、変数が変わったときにrefetchを呼んでいます。

// ~~ 省略 ~~
  useEffect(() => {
    refetch({ order: orderType, completed: completedInput, first: 5 });
  }, [completedInput, orderType, refetch]);
// ~~ 省略 ~~

いい感じに一覧を実装することができました!続いてタスクの作製用コンポーネントを作りましょう。

3-3: タスクの作成

CreateTaskModal.tsx
import React, { useCallback, useState } from "react";

import {
  Form,
  Modal,
  Button,
  Icon,
  Message,
  Checkbox
} from "semantic-ui-react";

import { useCreateTaskMutation } from "../generated/graphql";

import { useTaskFields } from "../hooks/formHooks";
import sleep from "../lib/sleep";
import styles from "../styles/main.css";
import "react-datepicker/dist/react-datepicker-cssmodules.css";
import DatePicker, { registerLocale } from "react-datepicker";
import ja from "date-fns/locale/ja";
registerLocale("ja", ja);

interface Props {
  refetch: () => void;
}

const CreateTaskModal = ({ refetch }: Props) => {
  const {
    titleProps,
    notesProps,
    completedProps,
    dueProps,
    clearValue
  } = useTaskFields();

  const [success, setSuccess] = useState(false);
  const [open, setOpen] = useState(false);

  const handleMutationCompleted = useCallback(async () => {
    setSuccess(true);
    refetch();
    await sleep(1500);
    clearValue();
    setOpen(false);
    setSuccess(false);
  }, [clearValue, refetch]);

  const [createTask, { loading, error }] = useCreateTaskMutation({
    variables: {
      title: titleProps.value,
      notes: notesProps.value,
      completed: completedProps.checked,
      due: dueProps.selected?.toISOString()
    },
    onCompleted: handleMutationCompleted
  });

  const handleButtonClick = useCallback(() => {
    createTask();
  }, [createTask]);

  const handleOpen = useCallback(() => {
    setOpen(true);
  }, []);
  const handleClose = useCallback(() => {
    setOpen(false);
  }, []);

  return (
    <Modal
      open={open}
      closeIcon={true}
      onClose={handleClose}
      onOpen={handleOpen}
      trigger={
        <div className={styles.add_button}>
          <Button
            icon={true}
            size="tiny"
            basic={true}
            circular={true}
            positive={true}
          >
            <Icon name="plus" />
          </Button>
        </div>
      }
    >
      <Modal.Header>タスクを追加</Modal.Header>
      <Modal.Content>
        <Form loading={loading} success={success} error={!!error}>
          <Message error={true}>追加中にエラーが発生しました</Message>
          <Message success={true}>タスクを追加しました</Message>
          <Form.Field required={true}>
            <label>タスク名</label>
            <Form.Input
              placeholder="ピーマンを買いに行く"
              type="text"
              required={true}
              {...titleProps}
            />
          </Form.Field>
          <Form.Field>
            <label>メモ</label>
            <Form.Input
              placeholder="駅前のOKストアがマジで安い"
              type="text"
              {...notesProps}
            />
          </Form.Field>
          <Form.Field>
            <label>完了</label>
            <Checkbox {...completedProps} />
          </Form.Field>
          <Form.Field>
            <label>期限</label>
            <DatePicker {...dueProps} locale="ja" dateFormat="yyyy/MM/dd" />
          </Form.Field>
        </Form>
      </Modal.Content>
      <Modal.Actions>
        <Button
          icon={true}
          onClick={handleButtonClick}
          positive={true}
          disabled={titleProps.value === ""}
        >
          <Icon name="plus" /> 追加する
        </Button>
      </Modal.Actions>
    </Modal>
  );
};
export default CreateTaskModal;


作成するときは、 useCreateTaskMutation を使います。
useFetchTasksQueryの同じように、variableにqueryの変数を渡します。
今回はonCompetedに関数を指定し、mutation終了後(作成完了後)にする処理を決めています。
handleMutationCompletedの中ではrefetch、formの値のクリアなどを行っています。

// ~~ 省略 ~~
  const handleMutationCompleted = useCallback(async () => {
    setSuccess(true);
    refetch();
    await sleep(1500);
    clearValue();
    setOpen(false);
    setSuccess(false);
  }, [clearValue, refetch]);

// ~~ 省略 ~~

  const [createTask, { loading, error }] = useCreateTaskMutation({
    variables: {
      title: titleProps.value,
      notes: notesProps.value,
      completed: completedProps.checked,
      due: dueProps.selected?.toISOString()
    },
    onCompleted: handleMutationCompleted
  });
// ~~ 省略 ~~

なぜrefetchをするのか? / mutation実行時のキャッシュの更新に関して

Apollo Clientは、更新のmutationを走らせたときは自動で新しい値にキャッシュを更新してくれます。(一覧に表示されている更新したタスクが新しい値に書き換わる)
ですが、新規作成・削除した場合は自動で更新されません。

もちろん画面をreloadすればそのタイミングでfetchが走るので、新しい値に書き換わります。ですが、UX的にはmutationを走らせたときにキャッシュが更新されたほうが良いでしょう。
それを実現するために、mutationのHooksはupdate関数を引数として受け取ります。
その関数の中で手動でキャッシュを書き換えます。イメージとしては、先程実装したfetchMoreと同じような感じです。

以下のupdate関数は正しく動かないので、コードの雰囲気を感じるだけにしてください。

    update: (cache, { data }) => {
      if (!data) return;

      // 新しく作られたタスク
      const createdTask = data.createTask;

    // readQuery関数で既存のキャッシュを取ってくる
      const tasksQuery = cache.readQuery<
        FetchTasksQuery,
        FetchTasksQueryVariables
      >({
        query: FetchTasksDocument,
        variables: { ...fetchTaskParam }
      });

      if (!tasksQuery) return;

    // writeQuery関数で既存のキャッシュに新しいデータをマージして書き込む
      cache.writeQuery<FetchTasksQuery, FetchTasksQueryVariables>({
        query: FetchTasksDocument,
        variables: { ...fetchTaskParam },
        data: {
          ...tasksQuery,
          tasks: {
            ...tasksQuery.tasks,
            edges: [
              ...tasksQuery.tasks.edges,
              {
                node: createTask
              }
            ]
          }
        }
      });
    }

一見、単純なように見えますが、これがなかなか難しい問題を抱えています。

  1. connectionの形式でキャッシュに入れないといけないが、cursorとかはサーバーから貰っていない
  2. ソートや絞り込みがかかった状態で、どこに新しいタスクを追加すべきか問題がある(ただ末尾につけるだけでいいのか?)

1に関しては必要な情報をサーバー側に返してもらう、というのが解決策の一つです。このサイトなどで紹介されています。
2は同じようなことを議論しているフォーラムがありますが、特に結論は出ていないようです。。。
ソートとかをフロント側でやるのも違う。。。

シンプルな一覧表示においてはupdate関数を使うのがベストですが、このような微妙な場合は多少のオーバーヘッドを犠牲にrefetchするのもありかな、ということで今回はシンプルにrefetchしています。

3-4: タスクの更新

UpdateTaskModal.tsx
import React, { useCallback, useState } from "react";

import {
  Form,
  Modal,
  Button,
  Icon,
  Message,
  Checkbox
} from "semantic-ui-react";
import { useUpdateTaskMutation, Task } from "../generated/graphql";

import { useTaskFields } from "../hooks/formHooks";
import sleep from "../lib/sleep";
import "react-datepicker/dist/react-datepicker-cssmodules.css";
import DatePicker, { registerLocale } from "react-datepicker";
import ja from "date-fns/locale/ja";
registerLocale("ja", ja);

interface Props {
  task: Task;
  handleModalClose: () => void;
}

const CreateTaskModal = ({ task, handleModalClose }: Props) => {
  const {
    titleProps,
    notesProps,
    completedProps,
    dueProps,
    clearValue
  } = useTaskFields(task);

  const [success, setSuccess] = useState(false);

  const handleMutationCompleted = useCallback(async () => {
    setSuccess(true);
    await sleep(1500);
    clearValue();
    handleModalClose();
  }, [clearValue, handleModalClose]);

  const [updateTask, { loading, error }] = useUpdateTaskMutation({
    variables: {
      taskID: task.id,
      title: titleProps.value,
      notes: notesProps.value,
      completed: completedProps.checked,
      due: dueProps.selected?.toISOString()
    },
    onCompleted: handleMutationCompleted
  });

  const handleButtonClick = useCallback(() => {
    updateTask();
  }, [updateTask]);


  return (
    <Modal open={!!task} closeIcon={true} onClose={handleModalClose}>
      <Modal.Header>タスクを編集</Modal.Header>
      <Modal.Content>
        <Form loading={loading} success={success} error={!!error}>
          <Message error={true}>保存中にエラーが発生しました</Message>
          <Message success={true}>タスクを編集しました</Message>
          <Form.Field required={true}>
            <label>タスク名</label>
            <Form.Input
              placeholder="ピーマンを買いに行く"
              type="text"
              required={true}
              {...titleProps}
            />
          </Form.Field>
          <Form.Field>
            <label>メモ</label>
            <Form.Input
              placeholder="駅前のOKストアがマジで安い"
              type="text"
              {...notesProps}
            />
          </Form.Field>
          <Form.Field>
            <label>完了</label>
            <Checkbox {...completedProps} />
          </Form.Field>
          <Form.Field>
            <label>期限</label>
            <DatePicker {...dueProps} locale="ja" dateFormat="yyyy/MM/dd" />
          </Form.Field>
        </Form>
      </Modal.Content>
      <Modal.Actions>
        <Button
          icon={true}
          onClick={handleButtonClick}
          positive={true}
          disabled={titleProps.value === ""}
        >
          <Icon name="plus" /> 保存する
        </Button>
      </Modal.Actions>
    </Modal>
  );
};
export default CreateTaskModal;


CompletedIcon.tsx
import React from "react";

import { Icon, Message } from "semantic-ui-react";
import { useUpdateTaskMutation, Task } from "../generated/graphql";

interface Props {
  task: Task;
}

const CreateTaskModal = ({ task }: Props) => {
  const [updateTask, { error }] = useUpdateTaskMutation({
    variables: {
      taskID: task.id,
      completed: !task.completed
    }
  });

  if (error) {
    return <Message error={true}>更新に失敗しました</Message>;
  }

  return task.completed ? (
    <Icon name="check circle" color="green" size="big" onClick={updateTask} />
  ) : (
    <Icon name="check circle" size="big" onClick={updateTask} />
  );
};
export default CreateTaskModal;


最後に、タスクの更新です。
今回は、一覧画面上のチェックアイコンをクリックすると完了・未完了のtoggleができる機能と、モーダルを出して諸項目をまとめて更新するものを作っています。

更新は、 useUpdateTaskMutation を使います。このHooksの使い方はcreateとだいたい同じです。

// UpdateTaskModal.tsx

  const [updateTask, { loading, error }] = useUpdateTaskMutation({
    variables: {
      taskID: task.id,
      title: titleProps.value,
      notes: notesProps.value,
      completed: completedProps.checked,
      due: dueProps.selected?.toISOString()
    },
    onCompleted: handleMutationCompleted
  });

// CompletedIcon.tsx
  const [updateTask, { error }] = useUpdateTaskMutation({
    variables: {
      taskID: task.id,
      completed: !task.completed
    }
  });

更新の場合はキャッシュが自動で書き換わるので、update等を気にする必要はありません!これで一通り完成です!


ここまでで、React/Apollo Client/Typescriptを使ったフロントエンド実装ができました。
型定義を自分でする必要なく、Hooksで取得できる値に型が付いていることでエディタの補完もはかどり、書き心地は抜群です。
まだ発展途上の技術ではありますが、どんどんキャッチアップして、素敵なGraphQLライフを送っていきましょう。

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

React-VisでReact-Friendlyなデータビジュアライズ

はじめに

この記事はReact#2 Advent Calendar 2019 18日目の記事です。

タイトル通り、Reactでのデータ可視化に関する内容になっています。
ダッシュボードを初めとしたデータビジュアライズの絡む開発をReactでするなら、React-Visってライブラリもなかなか良いよ!ということで書きました。

個人的な背景としては、Reactベースでのプロダクト開発において、Reactのライフサイクルやコンポーネント設計に合わせて作られた可視化ライブラリはないのかな?、と思い探していたところ見つけたのがReact-Visなので、似たような思いをお持ちの方がいたら参考になるかもしれません?(元々は似た理由でrechartsを使っていました)

React-Visとは?

Uber社のOpen Source Projectの1つで、githubに公開されています。Star 6.6k(2019/12/18時点)でなかなかと思うのですが、日本語記事が探しても見つからなかったので、使っている方がいたら教えて欲しいですね。
github: https://github.com/uber/react-vis
公式HP: https://uber.github.io/react-vis/

なぜReact-Vis?

データを可視化するライブラリならD3やChartJS, ThreeJSなどの有名どころがあって、それらの表現力はReact-Vis以上の部分も多く、事足りてはいます。それならわざわざReact特化のライブラリを使う必要などないケースも多いでしょうが、個人的には

1.React Componentとして記述し構成できる点
2.パフォーマンス

に魅力を感じています。

1つ目は、グラフの構成要素(ラベルや軸の範囲やアニメーションなど)にComponentと同様でStateを仕込めば、Stateの更新によってグラフの描画もインタラクティブに更新できます。React Componentと同じ感覚で設計して記述できるのがありがたいですね。
2つ目は、感覚的な話ですが、SPAで要素が1,000や10,000~とかのデータポイント多めのグラフを複数描画していく際のパフォーマンスに違いを感じます。ただこの辺はバックエンドでの処理も重要ですし、比較検証テストをちゃんとした訳でもないので、違いは追々書きたいと思います。

また、Reactベースでプロダクト開発をしている身としては、React-Visが掲げる下記の原則にも魅力を感じました。

(意訳)
[React-friendly]
React-VisはReact Componentと同様に機能する設計となっており、properties, children, callbacksをもって構成できる

[High-level and customizable]
React-Visはシンプルなコードとデフォルトの設定でも複雑なチャートを作成することができるが、個々のパーツをあなたが好きな様にカスタマイズすることもできる

[Industry-strong]
React-VisはUberの様々な内部ツールをサポートする目的で開発されている

全て自分がライブラリに求めていたものですが、3番目の[Industory strong]が自分にとっては結構重要でした。
Uberのプロダクトが特徴として持つ、地理空間情報や機械学習というビッグデータの可視化と隣合わせの状況で、それらに対するパフォーマンスを重視して開発されている(であろう)ライブラリというのは魅力的です。
(個人事情ですが、少なからず類似性のあるプロダクトを扱っているため)

逆にデメリットというか、不安な点としては、まだTypescriptに対応していないことですかね。。有志でreact-vis.d.tsを提供してくれてる方がいますが、@typesはなしなので、時々自分で型を付けています。

どんな感じで書けるの?

前置きが長くなりましたが、最小構成で代表的なグラフを書いてみます。

LineSeries(折れ線グラフ)

import React from "react";
import { XYPlot, LineSeries } from "react-vis";

interface SamplePropsTypes {
  width: number;
  height: number;
}

interface DataTypes {
  x: number;
  y: number;
}

const SampleLine = (props: SamplePropsTypes) => {
  const data: DataTypes[] = [
    { x: 0, y: 18 },
    { x: 1, y: 19 },
    { x: 2, y: 20 },
    { x: 3, y: 21 },
    { x: 4, y: 22 },
    { x: 5, y: 23 },
    { x: 6, y: 24 },
    { x: 7, y: 25 },
  ];

  return (
    <div>
      React Advent2 18th
      <XYPlot width={props.width} height={props.height}>
        <LineSeries data={data} />
      </XYPlot>
    </div>
  );
};

image.png

VerticalBarSeries(棒グラフ 縦ver)

// 上記コードに追加・変更
import { VerticalBarSeries } from "react-vis";

return (
  <div>
    React Advent2 18th
   <XYPlot width={props.width} height={props.height}>
     <VerticalBarSeries data={data} />
   </XYPlot>
  </div>
)

image.png

HorizontalBarSeries(棒グラフ 横ver)

// 上記コードに追加・変更
import { HorizontalBarSeries } from "react-vis";

const dataHorizontal: DataTypes[] = [
  { y: 0, x: 18 },
  { y: 1, x: 19 },
  { y: 2, x: 20 },
  { y: 3, x: 21 },
  { y: 4, x: 22 },
  { y: 5, x: 23 },
  { y: 6, x: 24 },
  { y: 7, x: 25 },
];

return (
  <div>
    React Advent2 18th
    <XYPlot width={props.width} height={props.height}>
      <HorizontalBarSeries data={dataHorizontal} />
    </XYPlot>
  </div>
);

image.png

Horizontalな棒グラフは軸が入れ替わるので、入力するデータのx, yも入れ替わるのが少しややこしいですね。

各パーツがReact Componentとしてexportされているので、リファレンスラインの挿入なんかもLineSeriesを使って組み込める部分など、直感的でよいなーと思います。

リファレンスラインの挿入

  return (
    <div>
      React Advent2 18th
      <XYPlot width={props.width} height={props.height}>
        <VerticalBarSeries data={data} />
        <LineSeries data={data} />
      </XYPlot>
    </div>
  );

image.png

ちょっと描画をリッチにするとこんな感じです。

import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import {
  HorizontalBarSeries,
  XYPlot,
  XAxis,
  YAxis,
  Crosshair,
} from "react-vis";
import { descSort, ascSort } from "./utils";
import { sample1, sample2, sampleLabel } from "./sampleData";

const useStyles = makeStyles(theme => ({
  crosshair: {
    color: "white",
    backgroundColor: "black",
    width: 20,
    opacity: 0.7,
    paddingLeft: 10,
  },
}));

interface SamplePropsTypes {
  width: number;
  height: number;
}

interface DataTypes {
  x: number;
  y: number;
}

const SampleVis = (props: SamplePropsTypes) => {
  const classes = useStyles();
  const sampleLabel = ["R", "e", "a", "c", "t", "-", "A", "C"];

  const [crosshairValues, setCrosshairValues] = useState<any>({});
  const [data, setData] = useState<DataTypes[]>([]);
  const [isLabel, setLabel] = useState<boolean>(false);
  const [isSort, setSort] = useState<boolean>(false);
  const [labelList, setLabelList] = useState<string[]>([]);
  const [top, setTop] = useState<number>(0);
  const [yLabel, setYLabel] = useState<number | null>(null);

  const getData = (direction: string) => {
    if (direction === "vertical") {
      setData(sample1);
    } else if (direction === "horizontal") {
      setData(sample2);
    }
  };

  useEffect(() => {
    getData("horizontal");
  }, []);

  useEffect(() => {
    if (isLabel) {
      setLabelList(sampleLabel);
    } else {
      setLabelList([]);
    }
  }, [isLabel]);

  useEffect(() => {
    const tmpLabel = labelList.slice().reverse();
    if (isSort) {
      data.sort((a: any, b: any) => ascSort(a.x, b.x));
      data.map((val: any, index: number) => (val.y = index));
      setLabelList(tmpLabel);
    } else {
      data.sort((a: any, b: any) => descSort(a.x, b.x));
      data.map((val: any, index: number) => (val.y = index));
      setLabelList(tmpLabel);
    }
  }, [isSort]);

  const onMouseLeave = () => {
    setCrosshairValues({});
    setYLabel(null);
    setTop(0);
  };

  const onNearestX = (_value: any, { event, innerX, innerY, index }: any) => {
    console.log(`${innerY} | ${innerX} | ${index}`);
    setYLabel(index);
    setTop(event.offsetY);
    setCrosshairValues(data[index]);
  };

  return (
    <div>
      <Button onClick={() => setLabel(!isLabel)}>Change Label</Button>
      <Button onClick={() => setSort(!isSort)}>Change Sort</Button>
      {data.length > 0 ? (
        <XYPlot
          width={props.width}
          height={props.height}
          onMouseLeave={onMouseLeave}
        >
          <XAxis title="React Advent2" />
          <YAxis
            title="18th"
            tickFormat={v => {
              if (v > labelList.length) {
                return null;
              }
              return labelList[v];
            }}
          />
          <HorizontalBarSeries
            data={data}
            color="skyblue"
            onNearestXY={onNearestX}
          />
          {yLabel != null ? (
            <Crosshair values={[crosshairValues]}>
              <div
                className={classes.crosshair}
                style={{ position: "absolute", top: top - 30 }}
              >
                <p>{labelList[yLabel]}</p>
                <p>{data[yLabel].x}</p>
              </div>
            </Crosshair>
          ) : (
            <div />
          )}
        </XYPlot>
      ) : (
        ""
      )}
    </div>
  );
};

image.png

image.png

一気にパーツを増やしてしまいましたが、React Componentに慣れている方にはなかなか書きやすそうではないでしょうか?
これらの基本的なグラフ以外にも、様々なグラフ(散布図、面積図、ツリーマップ、ネットワーク、、)が用意されているので、描画に困ることはなさそうです。

今回は省略していますが、フィルターなどのインタラクティブな操作ロジックを組む部分は、Hooksなどのおかげでより書きやすいのではなかろうかと思っています。

おわりに

React-Visによるデータビジュアライズの簡単な紹介をさせて頂きました。
ライブラリの原則通り各パーツがReact Componentとしてexportされているので、JSX内のXyPlotに色々なコンポーネントを差し込んでいくことで、任意のパーツを組み込んだグラフが作れるのはとてもよいです。また、1つ1つのパーツに余計なものがついておらず、分離されており自由度が高いのもよいですね。逆にコードの記述量が増えがちというのはあるかも?そこはうまく汎化させていきたいところです。
ただ、ドキュメントでカバーされていない部分もたまにあり、カスタマイズする際にソースを読みにいかなければよく分からないこともあるのが玉に瑕ですが、React Componentとして設計されているので、把握しやすいといえば把握しやすいです。
Reactでデータの可視化を扱う際は、是非使ってみてください。

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

React-Bootstrapを使えるようにする

初めにreactのプロジェクトを作る

npx create-react-app my-react-project



プロジェクトのroot直下に移動して

cd my-react-project



インストールして

npm install react-bootstrap bootstrap



index.htmlに以下を記載して

<link
  rel="stylesheet"
  href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
  integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
  crossorigin="anonymous"
/>



使いたい要素をインポートして使う
App.js

import React, { Component } from 'react';
import Button from 'react-bootstrap/Button';

class App extends Component {
    render() {
        return (
            <div>
                <Button> ボタンだよ </Button>
            </div>
        );
    }
}
export default App;

↓実行結果
スクリーンショット 2019-12-17 23.55.57.png

macで`の打ち方初めて知った。[ option + Shift + _ ]

参考サイト
本家

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

jQuery/React/Vue.js/Svelte/StimulusのTodoアプリをつくる

※この記事はMisoca+弥生 AdventCalendar2019の18日目のエントリーです。

そうだ!フロントエンドを勉強しよう!

昨今のフロントエンドは難しいですよね。私も何もわかりません。

特にフレームワーク・ライブラリが多すぎます。

jQueryが良いって聞いてたのに、ReactやらVue.js使って当然みたいな雰囲気ですし、最近だとSvelteやら、Basecampが作ったStimulusというのも登場してるらしいですよ。

とはいえ、嘆いていても始まりません。

まずは簡単なアプリをつくってそれぞれの実装を勉強してみましょう!!

Todoアプリをつくってみよう!

フロントエンドのFWを学びたければ、息をするより先にTodoアプリを作れって誰かが言ってました。

Todoアプリは動的な要素の追加や削除、さらにコレクションの描画が必要となるので入門にはぴったりですね!!

よーし、がんばるぞ〜!

できたもの

そういうわけで、jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました!!

まずは、jQueryで動くTodoアプリです!!

https://jumble-todo.netlify.com/

image.png

しっかりタスクの追加もできますね!

image.png

いかがでしたか?

このようにjQueryを駆使することで簡単にTodoアプリを作ることができました。
jQueryはお手軽に使えて便利ですね!


さて、私は

jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました

とお伝えしていますので、残りのTodoアプリもご紹介したいと思います。


...おや?

なんだか、このjQueryのTodoアプリのページ、下の方にまだ何かスクロールできますね...

image.png

(不穏な音)

おもむろにこのコードをコピーして、ChromeDevToolsのコンソールで実行してみましょう。

image.png

すると...

image.png

Vue.jsのTodoアプリになりました

image.png

Vue DevToolsでも、ちゃんとVue.jsで動作していることが確認できます。

そして、この画面も全体を表示すると...

image.png

はい。もうここまで来たらこの後の流れはあなたの予想している通りです。

このコードをコピーして同じようにDevToolsのコンソールで実行してみると...

image.png

ReactのTodoアプリになりました。

image.png

React Developer ToolsのComponentsタブでも確認できますね。

そしてこのページのコードも実行してみると..

image.png

SvelteのTodoアプリになりました。

(※DevToolsとかを動かせてないので、Svelteっぽさが表現できてないのはスイマセン)

さらにコードを実行してみます。

image.png

StimulusjsのTodoアプリになりました。

image.png

data-actionなどの属性で動作してるのがそれっぽいですね。

そして最後に、StimulusjsのTodoアプリのページのコードを実行すると...

image.png

jQueryのTodoアプリに戻ってきます。

このページのコードを実行すると再度Vue.jsのTodoアプリとなり、以降はループします。

また、最後に実行したコードは、ページ自体を初期表示した際に実行されるコードと完全に一致するようになっています。


いかがでしたか?

jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを(縛りとして、単一の index.html のみで完結し、かつコードが循環することを条件に)作ってみた

というエントリでした!

いやー、Todoアプリは入門にぴったりですね。勉強になりました。

コードについて

はい、すいませんここからが本編です。

半分ネタみたいな内容にお付き合いいただきありがとうございました。

実際の動作ページやコードは以下で確認できます。

以前RubyKaigiでTRICKという超絶技巧プログラミングを見て感動して、同じレベルのものは無理でも、何か自分でも作ってみたいな〜と思ってやってみた、というのがこのエントリの本質です。

今回作成したのは、いわゆるQuineと呼ばれるものの一種です。

クワイン(英: Quine)は、コンピュータプログラムの一種で、自身のソースコードと完全に同じ文字列を出力するプログラムである。娯楽として、プログラマが任意のプログラミング言語での最短クワインを書くことがある。プログラムを出力するプログラムだと見れば、クワインのプログラミングはメタプログラミングの一種である。

クワイン (プログラミング) - Wikipedia

自身のソースコードと完全に同じ文字列を出力する という記述があったので、一応コードを実行するたびにHTMLのみだけではなく、console.logでもソースコードを出力しています。

動作の仕組み

実際に動いているコードは若干のMinifyをしています。
圧縮前のコードはこちらです↓
https://github.com/mugi-uno/jumble-todo/blob/master/index.full.html

長々書いてありますが、結局のところ次のような構造です。

const num = 0; // 実行するFW

const html = ["jQuery用のHTML", "Vue用のHTML", ...];
const scripts = ["jQuery用のJS", "Vue用のJS", ...];

const nextScriptTemplate = "次に実行するJSのテンプレート";

// 〜このあたりの処理で「自分自身と同じ構造のコード」をnextScriptTemplateをもとに構築〜

const script = scripts[num].replace("_NEXTHTML_", nextHtml);

// 完成したscriptを実行
window.eval(script);

「自分自身と同じ構造のスクリプトを文字列上で構築して、それをevalで実行する」を繰り返しているのみで、実はさほど難しいことはやっていません。
なお、htmlscriptsの配列の内容を順に実行する仕組みで、自由に増やしたり減らしたりできます。(AngularやElmのコードを実装してくれてもいいんですよ!)

実はどこでも動く

見た目をある程度整えるためにデモページを用意しましたが、スクリプトはデモページじゃなくても動きます。(すでに読み込まれているスクリプトやheadタグの内容によっては動作しない可能性もあります)

では、実際に試してみましょう。

(※本エントリ内では紹介のために私自身が管理している別ページを利用しています。もし同様に試す場合は迷惑のかからないページで自己責任にて実行してください。)

たとえば、私はToyama.rbというコミュニティを主催しており、公式ページは次のURLです。

https://toyamarb.github.io/

こちらのページでDevToolsを開いてコードを実行してみます。

image.png

ソリャー!!

image.png

はい、動きましたね。bodyタグのみを書き換えるので、スタイルシートはもとのページのhead内に定義されているものを引き継いでいます。

ハマったところ

せっかくなので今回のコードを作るにあたってハマった点をいくつか紹介します。

React動かない問題

「別にJSX使わなくてもReact使ったと言っても良いのでは..?」と一瞬思いましたが、負けた気がしたのでテンプレート部は絶対にJSXで書くことにしました。

幸い、standalone版のbabelが存在するので、そちらを利用すればいけるかな〜と思ってました。
https://reactjs.org/docs/add-react-to-a-website.html#quickly-try-jsx

が、今回のコードに適用した場合、サンプルのままでは動作しませんでした。

結果として、

「動的に挿入するstandalone版のbabelを使った場合に、同じく動的に挿入されるscript(type=text/jsx)タグの内容が動きません!!」

という、恐らくこの世で誰も困ってないし解決しても二度と役に立たない問題に激突しました。同じ質問されても「いますぐその方法をやめてwebpackでビルドしてください」と答えると思います。

結果的には、自力で Babel.transformScriptTags() を呼ぶだけで良かったのですが、やってることが日常と違いすぎて、辿り着くまでに結構ハマりました。

エスケープ地獄

「スクリプトを文字列化してevalする」という前提があるので、何らかのエスケープはほぼ必須です。

最悪のケースでは、「evalで実行可能な文字列から出力されるscriptタグ内のJSXの中に表示する文字列の中のevalで実行可能な文字列の中の \ のエスケープが必要でした。自分でも何を言ってるのかわかりません。

さらに、実行対象のFWによって必要なエスケープ回数も異なるため、頭がクラッシュします。

最終的には、スクリプトを一旦Base64化した上で、eval実行されるスクリプト内でそれをDecodeする方法を取ることで回避しました。

妥協した点

完璧に作れたわけではなく、いくつか妥協しています。

  • Vue.jsのSFCをブラウザ上でコンパイルして実行したかった
  • Svelteは諦めてビルドしたものを直接埋め込んでる
  • コードがでかい

Quine...奥が深いぜ...!

まとめ

というわけで、JSを駆使してQuineを書いてみた、というエントリでした。

仕事でコードを書くのも楽しいですが、たまにはこういうトリッキーなことをしてみるのも新鮮で面白いので、興味が湧いた方はぜひ遊んでみてください!

Misoca+弥生 Advent Calendar 2019の次回19日目は、@issiによる業務効率化の話とのことです。

私のエントリからガラリとかわり、現実で役に立ちそうな内容で楽しみですね!

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

(縛りプレイ)jQuery/React/Vue.js/Svelte/StimulusのTodoアプリをつくる

※この記事はMisoca+弥生 AdventCalendar2019の18日目のエントリーです。

そうだ!フロントエンドを勉強しよう!

昨今のフロントエンドは難しいですよね。私も何もわかりません。

特にフレームワーク・ライブラリが多すぎます。

jQueryが良いって聞いてたのに、ReactやらVue.js使って当然みたいな雰囲気ですし、最近だとSvelteやら、Basecampが作ったStimulusというのも登場してるらしいですよ。

とはいえ、嘆いていても始まりません。

まずは簡単なアプリをつくってそれぞれの実装を勉強してみましょう!!

Todoアプリをつくってみよう!

フロントエンドのFWを学びたければ、息をするより先にTodoアプリを作れって誰かが言ってました。

Todoアプリは動的な要素の追加や削除、さらにコレクションの描画が必要となるので入門にはぴったりですね!!

よーし、がんばるぞ〜!

できたもの

そういうわけで、jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました!!

まずは、jQueryで動くTodoアプリです!!

https://jumble-todo.netlify.com/

image.png

しっかりタスクの追加もできますね!

image.png

いかがでしたか?

このようにjQueryを駆使することで簡単にTodoアプリを作ることができました。
jQueryはお手軽に使えて便利ですね!


さて、私は

jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました

とお伝えしていますので、残りのTodoアプリもご紹介したいと思います。


...おや?

なんだか、このjQueryのTodoアプリのページ、下の方にまだ何かスクロールできますね...

image.png

(不穏な音)

おもむろにこのコードをコピーして、ChromeDevToolsのコンソールで実行してみましょう。

image.png

すると...

image.png

Vue.jsのTodoアプリになりました

image.png

Vue DevToolsでも、ちゃんとVue.jsで動作していることが確認できます。

そして、この画面も全体を表示すると...

image.png

はい。もうここまで来たらこの後の流れはあなたの予想している通りです。

このコードをコピーして同じようにDevToolsのコンソールで実行してみると...

image.png

ReactのTodoアプリになりました。

image.png

React Developer ToolsのComponentsタブでも確認できますね。

そしてこのページのコードも実行してみると..

image.png

SvelteのTodoアプリになりました。

(※DevToolsとかを動かせてないので、Svelteっぽさが表現できてないのはスイマセン)

さらにコードを実行してみます。

image.png

StimulusjsのTodoアプリになりました。

image.png

data-actionなどの属性で動作してるのがそれっぽいですね。

そして最後に、StimulusjsのTodoアプリのページのコードを実行すると...

image.png

jQueryのTodoアプリに戻ってきます。

このページのコードを実行すると再度Vue.jsのTodoアプリとなり、以降はループします。

また、最後に実行したコードは、ページ自体を初期表示した際に実行されるコードと完全に一致するようになっています。


いかがでしたか?

jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを(縛りとして、単一の index.html のみで完結し、かつコードが循環することを条件に)作ってみた

というエントリでした!

いやー、Todoアプリは入門にぴったりですね。勉強になりました。

コードについて

はい、すいませんここからが本編です。

半分ネタみたいな内容にお付き合いいただきありがとうございました。

実際の動作ページやコードは以下で確認できます。

以前RubyKaigiでTRICKという超絶技巧プログラミングを見て感動して、同じレベルのものは無理でも、何か自分でも作ってみたいな〜と思ってやってみた、というのがこのエントリの本質です。

今回作成したのは、いわゆるQuineと呼ばれるものの一種です。

クワイン(英: Quine)は、コンピュータプログラムの一種で、自身のソースコードと完全に同じ文字列を出力するプログラムである。娯楽として、プログラマが任意のプログラミング言語での最短クワインを書くことがある。プログラムを出力するプログラムだと見れば、クワインのプログラミングはメタプログラミングの一種である。

クワイン (プログラミング) - Wikipedia

自身のソースコードと完全に同じ文字列を出力する という記述があったので、一応コードを実行するたびにHTMLのみだけではなく、console.logでもソースコードを出力しています。

動作の仕組み

実際に動いているコードは若干のMinifyをしています。
圧縮前のコードはこちらです↓
https://github.com/mugi-uno/jumble-todo/blob/master/index.full.html

長々書いてありますが、結局のところ次のような構造です。

const num = 0; // 実行するFW

const html = ["jQuery用のHTML", "Vue用のHTML", ...];
const scripts = ["jQuery用のJS", "Vue用のJS", ...];

const nextScriptTemplate = "次に実行するJSのテンプレート";

// 〜このあたりの処理で「自分自身と同じ構造のコード」をnextScriptTemplateをもとに構築〜

const script = scripts[num].replace("_NEXTHTML_", nextHtml);

// 完成したscriptを実行
window.eval(script);

「自分自身と同じ構造のスクリプトを文字列上で構築して、それをevalで実行する」を繰り返しているのみで、実はさほど難しいことはやっていません。
なお、htmlscriptsの配列の内容を順に実行する仕組みで、自由に増やしたり減らしたりできます。(AngularやElmのコードを実装してくれてもいいんですよ!)

実はどこでも動く

見た目をある程度整えるためにデモページを用意しましたが、スクリプトはデモページじゃなくても動きます。(すでに読み込まれているスクリプトやheadタグの内容によっては動作しない可能性もあります)

では、実際に試してみましょう。

(※本エントリ内では紹介のために私自身が管理している別ページを利用しています。もし同様に試す場合は迷惑のかからないページで自己責任にて実行してください。)

たとえば、私はToyama.rbというコミュニティを主催しており、公式ページは次のURLです。

https://toyamarb.github.io/

こちらのページでDevToolsを開いてコードを実行してみます。

image.png

ソリャー!!

image.png

はい、動きましたね。bodyタグのみを書き換えるので、スタイルシートはもとのページのhead内に定義されているものを引き継いでいます。

ハマったところ

せっかくなので今回のコードを作るにあたってハマった点をいくつか紹介します。

React動かない問題

「別にJSX使わなくてもReact使ったと言っても良いのでは..?」と一瞬思いましたが、負けた気がしたのでテンプレート部は絶対にJSXで書くことにしました。

幸い、standalone版のbabelが存在するので、そちらを利用すればいけるかな〜と思ってました。
https://reactjs.org/docs/add-react-to-a-website.html#quickly-try-jsx

が、今回のコードに適用した場合、サンプルのままでは動作しませんでした。

結果として、

「動的に挿入するstandalone版のbabelを使った場合に、同じく動的に挿入されるscript(type=text/jsx)タグの内容が動きません!!」

という、恐らくこの世で誰も困ってないし解決しても二度と役に立たない問題に激突しました。同じ質問されても「いますぐその方法をやめてwebpackでビルドしてください」と答えると思います。

結果的には、自力で Babel.transformScriptTags() を呼ぶだけで良かったのですが、やってることが日常と違いすぎて、辿り着くまでに結構ハマりました。

エスケープ地獄

「スクリプトを文字列化してevalする」という前提があるので、何らかのエスケープはほぼ必須です。

最悪のケースでは、「evalで実行可能な文字列から出力されるscriptタグ内のJSXの中に表示する文字列の中のevalで実行可能な文字列の中の \ のエスケープが必要でした。自分でも何を言ってるのかわかりません。

さらに、実行対象のFWによって必要なエスケープ回数も異なるため、頭がクラッシュします。

最終的には、スクリプトを一旦Base64化した上で、eval実行されるスクリプト内でそれをDecodeする方法を取ることで回避しました。

妥協した点

完璧に作れたわけではなく、いくつか妥協しています。

  • Vue.jsのSFCをブラウザ上でコンパイルして実行したかった
  • Svelteは諦めてビルドしたものを直接埋め込んでる
  • コードがでかい

Quine...奥が深いぜ...!

まとめ

というわけで、JSを駆使してQuineを書いてみた、というエントリでした。

仕事でコードを書くのも楽しいですが、たまにはこういうトリッキーなことをしてみるのも新鮮で面白いので、興味が湧いた方はぜひ遊んでみてください!

Misoca+弥生 Advent Calendar 2019の次回19日目は、@issiによる業務効率化の話とのことです。

私のエントリからガラリとかわり、現実で役に立ちそうな内容で楽しみですね!

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