20200112のReactに関する記事は14件です。

React Hooks + TypescriptでサンプルWebアプリ

久しぶりにReact触りました。以前Reduxを使ったサンプルアプリを作りましたが、もっと手軽に作りたいと思いますので、今回は同様のアプリをReact Hooksで作成したいと思います。

以前のサンプルはこちら

サンプルコードを書く前にReact Hooksの簡単なご紹介を。
React HooksはReact 16.8から追加された新しい機能です。

Reactの特徴は再利用可能なコンポーネントが作成可能であることですが、ステートフルなコンポーネントはコンポーネント間での再利用が難しく、ステートの複雑なライフサイクルにより理解が困難なものとなっていました。
Hookの導入により1つのコンポーネントを複数の小さな関数に分割可能にし、よりシンプルなコンポーネントを作ることが可能となります。

Hookの説明については公式を参照してください。

Hooksの説明

Hooksにはいくつかのフックが用意されています。

  • useState:関数コンポーネントの中でローカルな state を使うために呼び出す。
  • useEffect:関数コンポーネント内で副作用を実行することを可能にする。(componentDidMount, componentDidUpdate および componentWillUnmount と同様の目的で使う。)
  • useContext:React のコンテクストをコンポーネントのネストなしに利用できる
  • useReducer:複雑なコンポーネントのローカル state をリデューサ (reducer) を用いて管理できる

など

すべてのフックについては公式 Hooks API リファレンスを参照してください。

コードを書く

では、早速コードを書きます。

create-react-appで最初に作成されたテンプレートのApp.tsxを修正します。(Reduxのサンプルと違いindex.tsxの修正は不要です。)

src/App.tsx
import React from 'react';
import './App.css';
import { TopPage } from './components/TopPage'

const App: React.FC = () => {
  return (
    <>
      <TopPage />
    </>
  );
}

export default App;

useStateを使って書く

TopPageコンポーネントをuseStateフックを使って実装します。
(TextInput、RadioInput、SubmitButton、ShowStateは以前のサンプルを流用しています。)

src/components/TopPage.tsx
import React, { useState } from 'react'
import { TextInput } from './TextInput'
import { RadioInput } from './RadioInput';
import { SubmitButton } from './SubmitButton';
import { ShowState } from './ShowState';

export const TopPage: React.FC = () => {
    const [inputValue, setInputValue] = useState('')
    const [selectedValue, setSelectedValue] = useState('')
    const [clickCount, setClickCount] = useState(0)
    const handleInputValue = (value: string) => {
        setInputValue(value);
        console.log(value)
    }

    const handleSelectedValue = (value: string) => {
        setSelectedValue(value)
    }

    const submit = () => {
        setClickCount(clickCount + 1)
    }
    return (
        <>
            <TextInput title='入力' inputValue={inputValue} onChangeValue={handleInputValue} />
            <RadioInput title='ラジオ' selectedValue={selectedValue} onChangeValue={handleSelectedValue} />
            <SubmitButton title='Click me' onClick={submit} />
            <ShowState inputValue={inputValue} selectedValue={selectedValue} clickCount={clickCount} />
        </>
    )
}

Webアプリを起動すると以下のような画面が表示されます。

image.png

非常に簡単でシンプルなコードが書けました。
React Hooksの登場で、従来鉄板だと思われていたReact + Reduxの使い所も変わって来そうですね。

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

React Hooks(useState | useReducer) + TypescriptでサンプルWebアプリ

久しぶりにReact触りました。以前Reduxを使ったサンプルアプリを作りましたが、もっと手軽に作りたいと思いますので、今回は同様のアプリをReact Hooksで作成したいと思います。

以前のサンプルはこちら

サンプルコードを書く前にReact Hooksの簡単なご紹介を。
React HooksはReact 16.8から追加された新しい機能です。

Reactの特徴は再利用可能なコンポーネントが作成可能であることですが、ステートフルなコンポーネントはコンポーネント間での再利用が難しく、ステートの複雑なライフサイクルにより理解が困難なものとなっていました。
Hookの導入により1つのコンポーネントを複数の小さな関数に分割可能にし、よりシンプルなコンポーネントを作ることが可能となります。

Hookの説明については公式を参照してください。

Hooksの説明

Hooksにはいくつかのフックが用意されています。

  • useState:関数コンポーネントの中でローカルな state を使うために呼び出す。
  • useEffect:関数コンポーネント内で副作用を実行することを可能にする。(componentDidMount, componentDidUpdate および componentWillUnmount と同様の目的で使う。)
  • useContext:React のコンテクストをコンポーネントのネストなしに利用できる
  • useReducer:複雑なコンポーネントのローカル state をリデューサ (reducer) を用いて管理できる

など

すべてのフックについては公式 Hooks API リファレンスを参照してください。

コードを書く

では、早速コードを書きます。

create-react-appで最初に作成されたテンプレートのApp.tsxを修正します。(Reduxのサンプルと違いindex.tsxの修正は不要です。)

src/App.tsx
import React from 'react';
import './App.css';
import { TopPage } from './components/TopPage'

const App: React.FC = () => {
  return (
    <>
      <TopPage />
    </>
  );
}

export default App;

useStateを使って書く

TopPageコンポーネントをuseStateフックを使って実装します。
(TextInput、RadioInput、SubmitButton、ShowStateは以前のサンプルを流用しています。)

src/components/TopPage.tsx
import React, { useState } from 'react'
import { TextInput } from './TextInput'
import { RadioInput } from './RadioInput';
import { SubmitButton } from './SubmitButton';
import { ShowState } from './ShowState';

export const TopPage: React.FC = () => {
    const [inputValue, setInputValue] = useState('')
    const [selectedValue, setSelectedValue] = useState('')
    const [clickCount, setClickCount] = useState(0)
    const handleInputValue = (value: string) => {
        setInputValue(value);
        console.log(value)
    }

    const handleSelectedValue = (value: string) => {
        setSelectedValue(value)
    }

    const submit = () => {
        setClickCount(clickCount + 1)
    }
    return (
        <>
            <TextInput title='入力' inputValue={inputValue} onChangeValue={handleInputValue} />
            <RadioInput title='ラジオ' selectedValue={selectedValue} onChangeValue={handleSelectedValue} />
            <SubmitButton title='Click me' onClick={submit} />
            <ShowState inputValue={inputValue} selectedValue={selectedValue} clickCount={clickCount} />
        </>
    )
}

Webアプリを起動すると以下のような画面が表示されます。

image.png

useReducerを使って書く

次に同じ内容をuseReducerを使って書き直してみます。
useReducerはuseStateの代替品です。公式によると「通常、useReducer が useState より好ましいのは、複数の値にまたがる複雑な state ロジックがある場合や、前の state に基づいて次の state を決める必要がある場合です」とのこと。

では、早速書き換えてみます。

TopPage.tsx
import React, { useReducer } from 'react'
import { TextInput } from './TextInput'
import { RadioInput } from './RadioInput';
import { SubmitButton } from './SubmitButton';
import { ShowState } from './ShowState';

interface TopPageState {
    inputValue: string,
    selectedValue: string,
    clickCount: number,
}
interface TopPageAction {
    type: ActionType,
    payload: TopPageState,
}
enum ActionType {
    ACTION_INPUT_TEXT = 'ACTION_INPUT_TEXT',
    ACTION_SELECT_RADIOBUTTON = 'ACTION_SELECT_RADIOBUTTON',
    ACTION_CLICK_BUTTON = 'ACTION_CLICK_BUTTON',
}

const initialState = { inputValue: '', selectedValue: '', clickCount: 0 }

const reducer: React.Reducer<TopPageState, TopPageAction> = (state: TopPageState, action: TopPageAction) => {
    switch (action.type) {
        case ActionType.ACTION_INPUT_TEXT:
            return {
                ...state,
                inputValue: action.payload.inputValue
            }
        case ActionType.ACTION_SELECT_RADIOBUTTON:
            return {
                ...state,
                selectedValue: action.payload.selectedValue
            }
        case ActionType.ACTION_CLICK_BUTTON:
            return {
                ...state,
                clickCount: action.payload.clickCount
            }
        default:
            throw new Error()
    }
}

export const TopPage: React.FC = () => {
    const [state, dispatch] = useReducer(reducer, initialState)

    const handleInputValue = (value: string) => {
        dispatch({ type: ActionType.ACTION_INPUT_TEXT, payload: { ...state, inputValue: value } });
        console.log(value)
    }

    const handleSelectedValue = (value: string) => {
        dispatch({ type: ActionType.ACTION_SELECT_RADIOBUTTON, payload: { ...state, selectedValue: value } });
    }

    const submit = () => {
        dispatch({ type: ActionType.ACTION_CLICK_BUTTON, payload: { ...state, clickCount: state.clickCount + 1 } });

    }
    return (
        <>
            <TextInput title='入力' inputValue={state.inputValue} onChangeValue={handleInputValue} />
            <RadioInput title='ラジオ' selectedValue={state.selectedValue} onChangeValue={handleSelectedValue} />
            <SubmitButton title='Click me' onClick={submit} />
            <ShowState inputValue={state.inputValue} selectedValue={state.selectedValue} clickCount={state.clickCount} />
        </>
    )
}

さっくりとReduxライクに書き換えできました。

React Hooksを利用することで非常に簡単でシンプルなコードが書けましたね。
React Hooksの登場で、従来鉄板だと思われていたReact + Reduxの使い所も変わって来そうですね。

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

TypeScriptを使って嬉しかったこと

新人「先輩、TypeScriptのコーディングできました!」

先輩社員「どれどれ」
先輩社員「いやそこら中コンパイルエラーだらけ...なのは型システムが働いてる証拠だ!」
先輩社員「そうだろ?」

先輩社員「型は...ガードレールだ」
先輩社員「進むべきじゃない場所へ進もうとしたら、ちゃんとブロックしてくれる...」
(ぺこぱ風)

ってことで、静的型付言語って良いですよね。
今回はTypeScriptを使ってみて嬉しかったことを書いてみます。

登場人物

ワイ・・・ワイ(36歳)
社長・・・社長
ハスケル子・・・インターンの中学2年生

今日から新しいプロジェクト開始

社長「おーい、やめ太郎、ハスケル子ちゃん」
社長「新しいお仕事を獲得してきたで」
社長「技術記事投稿サイトを作る案件や」

ワイ「おお〜、さすが社長はんや」

社長「おおきにやで」
社長「ほんで、そのお仕事の話なんやけど」
社長「クライアントはんから一つ要望があんねん」
社長「Reactを使って作ってくれ、って言われてんねや」
社長「やめ太郎、ハスケル子ちゃん、Reactはイケるか?」

ワイ「やった事なくはないですわ」
ハスケル子「私も大丈夫です」

TypeScriptはどうする?

社長「TypeScriptはどうしよか?」
社長「そこはクライアントはんからは何も言われてへんねやけど」

ワイ「TSはよう分からんから、今回はやめときまひょ」
ワイ「型とかよう読まれへんし」

ハスケル子「私は絶対TypeScript有りでやりたいです」
ハスケル子「型がないとコード読むの面倒なので...」

型がないと面倒、とは

ワイ「いや、型があるほうが型情報を読まなあかんから面倒やん」

ハスケル子「いえ、型が書いてあるほうがコードを把握しやすいです」

ワイ「ええ...何で...?」

例えば:マイページ

ハスケル子「例えば、技術投稿サイトのマイページみたいなのを作るとしますね」

スクリーンショット 2020-01-12 20.44.38.png

ハスケル子「↑こんな感じのやつです」

ワイ「ほうほう」

ハスケル子「で、ユーザ情報を表示するためのUserInfoっていうコンポーネントを作るとします」
ハスケル子「TypeScriptなしで書くとすると、コードは↓こんな感じです」

/components/UserInfo.jsx
const UserInfo = ({ user }) => (
  <div>
    <section>
      <h2>ユーザ情報</h2>
      <p>アカウント名:{user.account}</p>
      <p>名前:{user.name}</p>
    </section>
    <section>
      <h2>記事一覧</h2>
      <ol>
        {
          user.articles.map(article => (
            <li>
              <a href={article.url}>{article.title}</a>
            </li>
          ))
        }
      </ol>
    </section>
  </div>
)

ワイ「なるほどな」

ハスケル子「やめ太郎さんなら、このコンポーネントのコードを見て、」
ハスケル子「どんな風に使うべきコンポーネントなのか読み取れますか?」

ワイ「そら簡単やで」
ワイ「余裕で読み取れますがな
ワイ「まず...」

/components/UserInfo.jsx
const UserInfo = ({ user }) => (

ワイ「↑こう書いてあるから」
ワイ「このUserInfoコンポーネントは」
ワイ「親コンポーネントからuserっていうpropsを受け取ってる...」
ワイ「っちゅうことが分かるわ」

ワイ「他にも...」

/components/UserInfo.jsx
<h2>ユーザ情報</h2>
<p>アカウント名:{user.account}</p>
<p>名前:{user.name}</p>

ワイ「↑こんなコードがあるから」
ワイ「親から受け取ったuserっていうpropsは、accountnameというプロパティを持ったオブジェクトである...」
ワイ「ってこともわかるで」

ワイ「あとは...」

<h2>記事一覧</h2>
<ol>
  {
    user.articles.map(article => (
      <li>
        <a href={article.url}>{article.title}</a>
      </li>
    ))
  }
</ol>

ワイ「↑こんな部分があるな...」
ワイ「せやから、userarticlesっちゅうプロパティも持っとんな」
ワイ「mapメソッドを使っているということは、そのuser.articlesは配列やということが分かるで」
ワイ「ほんで、その配列の中身はオブジェクトやな」
ワイ「urltitleというプロパティを持ったオブジェクトっちゅうことや」

ハスケル子「はい、そんな感じですよね」
ハスケル子「まとめると、どんな風に使うべきコンポーネントだと言えますか?」

ワイ「ええ...?」
ワイ「まあ、まとめるならば」

const user = {
  account: 'Yametaro',
  name: 'やめ太郎',
  articles: [
    {
      title: "記事名1",
      url: "/article1.html"
    },
    {
      title: "記事名2",
      url: "/article2.html"
    }
  ]
};

ワイ「propsとして↑こんなオブジェクトを渡して使うべきやな」

ハスケル子「そうですね」

TypeScriptの場合はどうか

ハスケル子「次に、TypeScriptを使った場合のコードを見てみます」

/components/UserInfo.tsx
type Props = {
  user: User
}

type User = {
  account: string,
  name: string,
  articles: Article[]
}

type Article = {
  title: string,
  url: string
}

const UserInfo: React.FC<Props> = ({ user }) => (
  <div>
    <section>
      <h2>ユーザ情報</h2>
      <p>アカウント名:{user.account}</p>
      <p>名前:{user.name}</p>
    </section>
    <section>
      <h2>記事一覧</h2>
      <ol>
        {
          user.articles.map(article => (
            <li>
              <a href={article.url}>{article.title}</a>
            </li>
          ))
        }
      </ol>
    </section>
  </div>
)

ハスケル子「↑こんな感じです」

ワイ「ほら、やっぱ型が書いてあるぶん長いやん...」

ハスケル子「そりゃあコード量は少し増えますけど」
ハスケル子「可読性の面でメリットもありますよ」
ハスケル子「例えば...」

/components/UserInfo.tsx
type Props = {
  user: User
}

ハスケル子「↑ここを見ると」
ハスケル子「このコンポーネントはuserというpropsを受け取る」
ハスケル子「そしてそのuserUser型の値である」
ハスケル子「...ということが分かります」

ワイ「なるほどな、受け取るprops一覧がここで分かるわけやな」
ワイ「今回の例はuser一個だけってことか」
ワイ「でも、なんやのUser型って」
ワイ「number型とかstring型なら知っとるけど」

ハスケル子「それは↓この部分に書いてあります」

/components/UserInfo.tsx
type User = {
  account: string,
  name: string,
  articles: Article[]
}

ハスケル子「User型の値は、accountという文字列と」
ハスケル子「nameという文字列...」
ハスケル子「そしてarticlesという配列を持っているよ、ってことがわかります」

ワイ「ほうほう、つまり...」
ワイ「User型っていうのはハスケル子ちゃんの自作の型ってこと?」

ハスケル子「そうです」
ハスケル子「自分で新しい型を定義して名前を付けることができるんです」

ワイ「ほー、なんか...」
ワイ「Userってのはこんなやつですよ、みたいな」
ワイ「世界観的なものが分かりやすいな」

ハスケル子「そうですね」

ワイ「ほんで、Userが持ってるaccountnamestring型なのは分かったけど」
ワイ「Article[]ってのはどういう型なん?」

ハスケル子「Article[]っていうのは」
ハスケル子「Article型の値が入ってる配列だよ、って意味です」

ワイ「Article型ってのは、また自作の型やな?」

ハスケル子「はい」

/components/UserInfo.tsx
type Article = {
  title: string,
  url: string
}

ハスケル子「↑この部分でArticle型を定義しています」

ワイ「なるほど」
ワイ「Article型の値は、titleurlというプロパティを持ってますよ...」
ワイ「ほんで、titleurlも文字列ですよ...」
ワイ「っちゅうことやな」

ハスケル子「そうです」

型が書いてあると、何が嬉しいか

ハスケル子「TypeScriptなしの場合は」
ハスケル子「コンポーネントの中身を全部読んで...」

「なるほど、このコンポーネントにはuserというオブジェクトを渡して使うんだな」
「そのuserは、accountnamearticlesというプロパティを持ってるんだな」
articlesmapメソッドを持っているようだから、配列だな・・・!」

ハスケル子「...っていうことがようやく分かったじゃないですか」

ワイ「せやな」

ハスケル子「でもTypeScriptありの場合だと」

/components/UserInfo.tsx
type Props = {
  user: User
}

type User = {
  account: string,
  name: string,
  articles: Article[]
}

type Article = {
  title: string,
  url: string
}

ハスケル子「↑この型定義の部分を見ただけで...」

const user = {
  account: 'Yametaro',
  name: 'やめ太郎',
  articles: [
    {
      title: "記事名1",
      url: "/article1.html"
    },
    {
      title: "記事名2",
      url: "/article2.html"
    }
  ]
};

ハスケル子「↑こんなpropsを受け取るコンポーネントだな!」
ハスケル子「ってことが分かるじゃないですか」
ハスケル子「コンポーネントの実装部分を読まなくても!」

ワイ「おお...」
ワイ「やっぱリーダブルやなぁ...」

社長「(何がやっぱやねん...)」

ワイ「仕事やと、一つの案件のコーディングを複数人で担当することも多いから」
ワイ「人の作ったコンポーネントを使う機会とかも多いしな」
ワイ「せやから、読み取りやすいのはええよなぁ」

ワイ「あと思ったんやけど」
ワイ「型ってなんか、コメントみたいな効果もあるんやね」
ワイ「このプロパティには文字列を入れてくださいな、みたいな」
ワイ「コード内に仕様書が書いてある感じがええな」

ハスケル子「そうですよね」
ハスケル子「しかも、ちゃんと守らなきゃ前に進めない...」
ハスケル子「強制力を持ったコメントですよね」
ハスケル子「型定義の通りにpropsを渡さないとエラーが出て、コンパイルが通らない...」
ハスケル子「だから、マウスでぽちぽちページを見て回る前にミスに気付けるんです」

ワイ「ああ、普通はコードを実行してみるまで」
ワイ「エラーが出るかどうか分からへんもんなぁ...」
ワイ「それはええかも...」

他にも嬉しいことが

ハスケル子「そういえば、やめ太郎さんって」
ハスケル子「nameっていう単語をよくnamaeってスペルミスするじゃないですか」

ワイ「まぁ、比較的するな」

ハスケル子「そういうタイポ対策にもTypeScriptが役立ったりしますよ」
ハスケル子「例えば...」

/components/UserInfo.tsx
<p>名前:{user.namae}</p>

ハスケル子「↑こんな風にタイポしちゃったとしたら...」

Property 'namae' does not exist on type 'User'. Did you mean 'name'?
(プロパティ「namae」はタイプ「User」に存在しません。 「name」という意味ですか?

ハスケル子「なんて教えてくれるんです」

ワイ「おお、まじか...」
ワイ「まさにワイのための機能やん...」

ハスケル子「そもそもタイポしなくなるようなメリットもありますよ」

スクリーンショット 2020-01-12 22.14.27.png

ハスケル子「↑こんな風に、user.まで打つと入力候補が出てくるので」
ハスケル子「その中から選択すればいいんです」

ワイ「おお...」
ワイ「もう、使わん理由ないやん...」

ワイ「ハスケル子先生...!!」
ワイ「TypeScriptがしたいです......」

社長「ほな、決まりやな!」

そんなこんなで終業時刻

ワイ「ハスケル子ちゃん、今日も色々教えてくれてありがとうやで...」

ハスケル子「教えるの楽しいから全然いいですよ」

ワイ「ハスケル子ちゃんは凄いなぁ...まだ中学生なのに色々知ってて...」
ワイ「チート過ぎて、見てると自分が情けなくなってくるわ...」
ワイ「負け過ぎてるんやもん...」

ハスケル子「私だって同じですよ」
ハスケル子「いつも友達に嫉妬してます...」
ハスケル子「でも...誰かに負けてても、誰かより劣っていても」
ハスケル子「今の自分から精一杯生きるしかないんです」
ハスケル子「自分なりの精一杯をやりましょう」

ワイ「ハスケル子ちゃん...!」

ハスケル子「...もしくは、死ぬとか...」

ワイ「いや死を提案すな!

〜おしまい〜

続編もよろしくやで

4歳娘「パパ、実行時エラーの出ないフロントエンド言語ってなーんだ?」

おまけ:簡単にReact + TypeScriptを始めるには

React + TypeScriptを触ってみたい人は、

npx create-next-app --example with-typescript my-app-name
cd my-app-name
npm run dev

↑この3つのコマンドを打ってから
http://localhost:3000/にアクセスするだけやで!
(もちろんNode.jsは先にインストールしといてや!)

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

reduxでTypeError: _useSelector is undefinedと言われた時

ある日

:sun_with_face::sun_with_face::sun_with_face:

お勉強用react製web appをいじっていたら TypeError: _useSelector is undefined というエラーが! :anger:

20200112_220045.png

しかし普通にbuildは通る :thinking: そしてググっても出てこない(気がする) :sob:

↓ばーじょん↓

  • react@16.12.0
  • react-redux@7.1.3
  • redux@4.0.5
  • reselect@4.0.0

こんなコード書いてた

:poop::poop::poop:

// src/containers/App.tsx
const appSelector = createSelector(
  (state: { init: AppState }) => state.init,
  init => init
)

export const AppContainer: React.FC = () => {
  const dispatch = useDispatch()
  const { loading, loaded, error } = useSelector(appSelector)
  const loadDispatcher = (): void => {
    dispatch(initOp())
  }
  return (
    <AppView
      loading={loading}
      loaded={loaded}
      error={error}
      loadDispatcher={loadDispatcher}
    />
  )
}

// src/reducers/index.ts
export default combineReducers({
  app: appReducer,
  todos: todoReducer,
  visibilityFilter: visibilityFilterReducer,
})

// src/reducers/app.ts
export const appReducer = reducerWithInitialState({
  loaded: false,
  loading: false,
  error: null,
} as AppState)
  .cases(...

と、こうやって書けば一目瞭然で(実際はファイルが別れているのでそこそこ時間を取られた)

  • appSelector()ではinitからAppStateを取得しようとしている
  • combineReducers()ではappappReducerとして結合されている

このため、上記エラーが出たようだった。

実際にはuseSelector()の引数にダミーの(() => true的な)selectorを使うなどしてエラー原因の特定を行った。

以上よりappSelectorを以下のように修正することでエラーは解消した。

// reselect使っている理由は特に無いです(使ってみたかった)
// この程度ならワンライナーで
// const appSelector = ({ app }: { app: AppState }): AppState => app
// の方が分かりやすいですね
const appSelector = createSelector(
  (state: { app: AppState }) => state.app,
  app => app
)

所感

:notebook_with_decorative_cover::notebook_with_decorative_cover::notebook_with_decorative_cover:

reducerが無いと_useSelector()が未定義状態になるのかーというのが一番の収穫:hugging:

当初InitStateとして定義していたが名前的に微妙だな思いAppStateに変えたのだが、この修正が一部漏れていた:innocent:

selector周りでtypescriptの型チェックが微妙な感じになってしまうのは予想外で、もう少し丁寧に書くべきだったと反省:muscle:

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

React Native (Expo) を使ってはじめてのモバイルアプリ制作 きっかけから制作、公開まで

昨年末12月28日にはじめて制作したアプリを App Store に公開しました。
icon.jpg
"tagTimeLog Lite"

タグを使って時間を記録するアプリです。かなり単純なアプリです。
モバイルアプリを開発したきっかけから制作、公開までをまとめました。

きっかけ

複数の仕事が重なり、それぞれのプロジェクトごとに実際にどれだけの時間を消費しているのか計測したいと思い、既存のアプリを探していくつか試して見たのですが、自分が欲しいと思うようなシンプルで手軽なアプリが見つかりませんでした。
アプリを探すのにも疲れたので「仕方ない、自分で作るか」ということで一念発起してアプリ開発を思い立ちました。
仕事で使っている WordPress でも導入された React の勉強もしなければならなかったので、 React を使ってアプリを作れば一石二鳥ということで無駄になることはないと自分を納得させて開発を開始しました。

仕様

自分が欲しいと思ったアプリの仕様。

  • 操作がわかりやすい
  • シンプル
  • 少ないタップ数でログが取れる
  • 動作が早い
  • ログイン不要
  • エキスポート機能
  • メモを取れるようにする
  • ログの時間を修正できるようにする

最初からこのようにリスト化するようなことはせず、何となくこんな感じでとぼんやり考えながら作っていきました。
制作途中であれも欲しい、これもあったらいいなと思いつくことがありましたが、元々の「シンプルで手軽」という基本コンセプトととりあえず早く公開することを優先して機能を絞り込みました。
そもそも技術がないので複雑なことはできませんし。

集計自体はスプレッドシートで行う予定でしたので、CSVファイルへのエキスポートは必須でした。

ブラウザベースかモバイルネイティブか

React を使用することを前提としていましたが、React 自体を全く理解しておらず、React Native(モバイル用フレームワーク) 以前の話だったので、 React を使ってブラウザベースで制作することにしました。

JavaScript の再学習

これまで jQuery を何となく使ってきた程度の JavaScript の知識だったので、React を学習するには、 JavaScript の知識が全く足りず、まずは JavaScript の学習から始めました。
React においては ES6以降 を使っての制作が基本なので ES6 を中心に JavaScript を学習しなおしました。

JavaScript の学習に使用したWebサイトや書籍

React の学習

JavaScript をある程度学習できたので React の学習です。
個人的には、React の重要なポイントは state のように思いました。
あとは JavaScript (ES6) の知識で書ける感じです。
なので、もし、React の学習に躓いている方がおられたら JavaScript の知識について少し見直してみても良いかもしれません。
少し難しく感じたのは、ルーティングのところでの props や値の受け渡しです。
コンポーネントの構成をどうすればよいのか迷いました。
あと、 JSX は慣れるしかないと思います。

React の学習に使用したWebサイトや書籍

React の勉強には公式サイトが最も役に立ちました。
本などは少し古かったりするので、先ずは公式サイトで勉強し、わからないところを本で補うようにしました。

React を使ってブラウザベースのアプリを制作

Create React App を使って作成しました。
勉強しながらの制作だったので、あらゆる所でつまずきましたが、公式サイトや書籍、Web 上の情報を参考にしながら一定ペースを保って完成までたどり着きました。
アプリ制作のおかげで React の概要については、おおよそ理解できたように思います。

使用したライブラリー、データベース

  • ライブラリー: react-router-dom
  • データベース: IndexedDB

ライブラリーは 、react-router-dom を入れました。
データベースは、ブラウザ用のローカルストレージ IndexedDB を使いました。

react-router-dom の勉強には「速習 React 速習シリーズ」、 IndexedDB の勉強には MDN が非常に参考になりました。というか、IndexedDB には、 MDNしかないと思います。

ブラウザベースからモバイルへ

最初に希望していた機能は、ほぼこのブラウザベースのアプリで満たしており、完成後しばらくアプリを使用していました。
しかし、使用していて少し不安になることがありました。
それは、データベースの IndexedDB です。

IndexedDB は個人的には使いやすく非常によいデータベースだと思うのですが、ブラウザ依存のデータベースなので、例えば何かの理由でブラウザをアンインストールしてしまうとデータベースも消えてしまいます。
私が Web サイト制作を仕事にしていることもありブラウザの設定を変えるなど、荒い(?)使い方をする場合もあるのでブラウザ依存のデータベースを使用していることが少し不安でした。

そこで、React の思想「Learn Once, Write Anywhere」を思い出し、専用のアプリであればアンインストールするようなこともないため、「仕方ない、React Native を使ってモバイルアプリを作るか。」ということでモバイルアプリの制作を開始しました。

React Native (Expo) を使ってネイティブモバイルアプリを制作

React で作成したアプリを React Native で作り直しです。
React Native であっても一部ネイティブ言語を触る必要があります。
ネイティブ言語は触りたくない(勉強したくない)ので、 Expo を利用することにしました。
Expo は、実装できる機能の範囲が狭まるようですが、最初から最後まで JavaScript だけでアプリ公開できるようになる React Native 向け支援ツールだそうです。

React と React Native の違いとしては、根本的なところは React とほぼ一緒だと思いますが、見た目の構築とルーティング(画面遷移)の設定は、まったく違うように思いました。
全体的に組みなおす必要がありました。
モバイルと比較して Web のほうが自由度が高いので、もし、モバイルと Web で、見た目を同じにしようと考えているのであれば、モバイルから作成したほうが良いように思いました。

React Native (Expo) の学習に使用したWebサイトや書籍

React Native については、Web 、書籍ともに日本語化されているものが少なく、基本的には英語で書かれている公式サイトを見て勉強しました。
英語がわからないので理解するのにかなり時間がかかってしまいましたが、Google 翻訳など利用しながら、とりあえず制作できるところまでは理解できました。

使用したライブラリー、データベース

package.json の中身です。

  • expo
  • expo-ads-admob
  • expo-sharing
  • expo-sqlite
  • papaparse
  • react
  • react-dom
  • react-native
  • react-native-elements
  • react-native-gesture-handler
  • react-native-keyboard-aware-scroll-view
  • react-native-reanimated
  • react-native-svg
  • react-native-swipe-list-view
  • react-native-web
  • react-navigation
  • react-navigation-stack
  • react-navigation-tabs
  • sentry-expo

ライブラリー、データベースで気になったところ

Expo

JavaScript だけでアプリの制作から申請まですべてできるのは大変ありがたいです。
ただ、ちょっと気になるのはアプリのファイルサイズが大きくなるということです。
Expo を使用すると仕方がないことらしいのですが、確かにより複雑なことができる他の Swift などで作られたであろうと思われるアプリと比較すると、ちょっと大きいなという感じがしました。
今後ブラッシュアップして小さくできるか確認していきたいと思います。

react-navigation

React から React Native に移行して最も戸惑ったのがルーティング(ページ遷移)です。
React の "react-router-dom" とまったく違うように感じました。

react-native-swipe-list-view

当初、"react-native-swipeout" を使っていたのですが、expo をアップデートしたところエラーが出るようになったので、"react-native-swipe-list-view" に変更しました。

ライブラリーは、大変便利でありがたいのですが、アップデートに対応されていないと代替ライブリーがない場合は、アプリ自体が更新できないことになってしまうので、依存度はなるべく低くした方が良いと思いました。

expo-ads-admob

ローカル環境でのテスト広告や publish 後の正式広告は、すぐに表示されるのですが、iOS App Store 公開、申請後はすぐに表示されず、2、3日後に表示されるようになりました。
他の方のブログなどを見ていると普通にそれぐらい待たされることがあるようです。

sqlite

データベースは、 sqlite を使いました。当初は、 realm を使いたいと思っていましたが、Expo が対応していなかったので仕方なく sqlite にしました。
sqlite を使ってみたところ特に問題なく使えてますので、とりあえず良かったです。

App Store (iOS) への申請と公開

担当者によって厳しさが違う?

App Store (iOS) への一度目の申請は、一発 OK で何の修正命令もなく翌日には公開となりました。
Apple の審査は、厳しいと思っていたので意外でした。
公開翌日に見た目のところを少し調整してアップデートし、審査申請に出したところエラー表示され修正命令が来ました。
修正内容は「アプリの名前がガイドライン違反してるので名前変えろ」(英語わからないのでおそらくこんな感じ)でした。ガイドラインでは「アプリの名前に金額を表すようなワードは入れるな」ということらしいです。
現在のアプリ名は "tagTimeLog Lite" なのですが、最初の申請時は "tagTimeLog Free" という名前で申請していました。
この "Free" が金額を表しているようです。
私の勉強不足だったので反省なのですが、いくらなんでも最初の申請時に気づかなかったのかなと思いました。
Apple の審査が早くなったとどこかで読みましたが、担当者によって能力に差があり、一部で審査が甘くなっているのかもしれません。

名前を変更し、再審査を依頼、2、3日後に無事通過して公開となりました。

思いつきから制作、公開までの期間

思いつきから公開まで、ほぼ一年かかりました。
通常の受注仕事しながらなので、途中一ヶ月くらいまったく触れなかたということもあるので、ざっくりですが、実質4、5ヶ月くらいでしょうか。

特に学習面で React については、公式含めて教材となるような、日本語で記載されている Web サイトがたくさんあるのですが、React Native では日本語がガクッと減って、ほぼ英語で記載されている Web サイトでした。英語が苦手な私は React Native の学習段階でかなり時間を取られた印象です。

アップデートの予定

今後は下記内容を予定してます。

  • Android 版作成
  • 他の形式による時間表示
  • ダークモード (iOS)
  • 有料版作成 ほか

以上

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

JavaScriptのUIライブラリ ReactのHook使用してToDoアプリを作成してみました

はじめに

この記事ではJavaScriptのライブラリであるReactとReactの機能であるhookを使用して簡単なToDoアプリの実装を行います。
以前に書いた記事をベースに書き換えるので、そちらも参考にしてみてください。
JavaScriptのUIライブラリ ReactでToDoアプリを作成してみました
Reactのドキュメントチュートリアル(三目並べ)を一通り行った後の練習になるように書きたいと思います。
環境構築に関しては、create-react-appを使用して作成しています。
環境構築に関しては以前書いた記事があります。
ソースコード

目次

  1. コンポーネントの確認
  2. 各コンポーネントの解説
  3. まとめ

1. コンポーネントの確認

すぐに動かせる環境を置いておきます。


See the Pen
ReactToDo with Hook
by oq-Yuki-po (@oq-yuki-po)
on CodePen.



ReactはUIのパーツをコンポーネントという独立した一つの部品とみなして構成していきます。
今回の例では下記の様に分割しました。
以前は、これらのコンポーネントを全てクラスで定義して行きましたが今回は関数コンポーネントで定義して行きます。

ReactToDoApp.png

ToDoアプリケーションを構成するコンポーネントは全部で4つあります。

  • ToDo
    ToDoアプリケーションの全体を表します

  • TaskAdd
    新しいタスクの追加を行います

  • TaskList
    追加されたタスクをリストにして表示します

  • TaskItem
    一つのタスクを表します

2. 各コンポーネントの解説

2-1. ToDo.js

2-1-1. ソース全体

ToDo.js
import React, { useState, useReducer } from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [
        ...state,
        action.NewTask
      ];
    case 'delete':
      let TaskIndex = 0;
      for (var i = 0; i < state.length; i++) {
        if (state[i].key.toString() === action.TaskId.toString()) {
          TaskIndex = i;
        }
      }
      return state.filter((_, index) => index !== TaskIndex);
    default:
      return state;
  }
}

function ToDo() {
  const [TaskId] = useState(0)
  const [ToDoList, dispatch] = useReducer(reducer, [])
  return (
    <main className='todo-component'>
      <TaskAdd id={TaskId} dispatch={dispatch} TaskList={ToDoList}/>
      <TaskList TaskList={ToDoList} />
    </main>
  );
}

export default ToDo; 

2-1-2. モジュールのインポート

import
import React, { useState, useReducer } from "react";
import TaskAdd from './TaskAdd';
import TaskList from './TaskList';

useStateuseReducerがhookと呼ばれるものです。
これを使用することで、クラスコンポーネントで行なっていた状態管理が関数コンポーネントでも行えます。

2-1-3. ToDoコンポーネント

ToDoコンポーネント
function ToDo() {

  const [TaskId] = useState(0)
  const [ToDoList, dispatch] = useReducer(reducer, [])

  return (
    <main className='todo-component'>
      <TaskAdd id={TaskId} dispatch={dispatch} TaskList={ToDoList}/>
      <TaskList TaskList={ToDoList} />
    </main>
  );
}

クラスで定義していた際は、constructorを定義していたと思います。
以前の記事では、下記の様に定義していました。

ToDoコンポーネントのconstructor
constructor(props) {
  super(props);
  this.state = {
    TaskList: [],
    TaskId: 0
  };
  this.deleteTask = this.deleteTask.bind(this);
  this.addTask = this.addTask.bind(this);
}

関数でコンポーネントを定義する際は以下の様になります。

ToDoコンポーネント(クラス版)
const [TaskId] = useState(0)
const [ToDoList, dispatch] = useReducer(reducer, [])

stateを定義する際にuseStateを使用します。
const [状態管理したい変数, 状態を変更する関数] = useState(状態管理したい変数の初期値)
TaskIdは子コンポーネントに渡して、そちらで管理するので変更する関数を定義していません。
(定義してる場所が、そもそもどうなの?みたいなツッコミはご勘弁を。後々にAPIとの連携を見越した実装だと解釈お願いします。。。)
const [状態管理したい変数, 状態を変更する関数] = useState(初期値)
配列や複雑なロジックをstateに持たせる時は、TaskListの様にuseReducerを使用します。
const [状態管理したい変数, reducerで定義した関数(dispatch)] = useReducer(reducer, 状態管理したい変数の初期値);
公式の引用

通常、useReducer が useState より好ましいのは、複数の値にまたがる複雑な state ロジックがある場合や、
前の state に基づいて次の state を決める必要がある場合です。また、useReducer を使えば
コールバックの代わりに dispatch を下位コンポーネントに渡せるようになるため、
複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。

引用にも書いてある通り、<TaskAdd id={TaskId} dispatch={dispatch} TaskList={ToDoList}/>
dispatchをTaskAddに渡しています。

2-1-4. reducerの定義

reducerの定義
function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [
        ...state,
        action.NewTask
      ];
    case 'delete':
      let TaskIndex = 0;
      for (var i = 0; i < state.length; i++) {
        if (state[i].key.toString() === action.TaskId.toString()) {
          TaskIndex = i;
        }
      }
      return state.filter((_, index) => index !== TaskIndex);
    default:
      return state;
  }
}

reducerの定義を行います。
stateは状態管理したい変数、今回はTaskListが入っています。
actiondispatchで、この関数を実行するときに指定したパラメータが格納されています。
action.typeで、どの操作なのかを判定するのに使用しています。
adddeleteはそれぞれ、TaskListに新規にタスクを追加、指定したタスクを削除の処理を行なっています。

2-2. TaskAdd.js

2-2-1. ソース全体

TaskAdd.js
import React, { useState } from "react";
import Task from './Task'

function TaskAdd(props) {

    const [NewTask, setTask] = useState('')
    const [TaskId, setTaskId] = useState(props.id)
    const [ErrorMessage, setErrorMessage] = useState('')

    function handleClick() {
        if (NewTask === '') {
            setErrorMessage('入力が空です。')
            return 0
        }
        for (var i = 0; i < props.TaskList.length; i++) {
            if (props.TaskList[i].props.name === NewTask) {
                setErrorMessage('タスク名が重複しています。')
                return 0
            }
        }
        props.dispatch({
            type: 'add',
            NewTask: <Task key={TaskId} id={TaskId} name={NewTask} dispatch={props.dispatch} />
        })
        setTaskId(TaskId + 1)
        setTask('')
        setErrorMessage('')
    }

    return (
        <section className='task-creator'>
            <h2>Task Add</h2>
            <input className='task-item-text' type="text" placeholder="Task" value={NewTask}
                onChange={(e) => setTask(e.target.value)} />
            <button className='task-add-btn' type="button" onClick={handleClick}>
                Add
                </button>
            <p className='error-msg'>{ErrorMessage}</p>
        </section>
    )
}

export default TaskAdd; 

2-2-2. stateの定義

stateの定義
const [NewTask, setTask] = useState('')
const [TaskId, setTaskId] = useState(props.id)
const [ErrorMessage, setErrorMessage] = useState('')

Task.jsと同じ様に定義しています。
const [状態管理したい変数, 状態を変更する関数] = useState(状態管理したい変数の初期値)

2-2-3. handleClick

TaskAdd.jsのhandleClick
function handleClick() {
    if (NewTask === '') {
        setErrorMessage('入力が空です。')
        return 0
    }
    for (var i = 0; i < props.TaskList.length; i++) {
        if (props.TaskList[i].props.name === NewTask) {
            setErrorMessage('タスク名が重複しています。')
            return 0
        }
    }
    props.dispatch({
        type: 'add',
        NewTask: <Task key={TaskId} id={TaskId} name={NewTask} dispatch={props.dispatch} />
    })
    setTaskId(TaskId + 1)
    setTask('')
    setErrorMessage('')
}

定義したstateにアクセスする際はthis.state.NewTaskの様に行なっていましたが
hookでは単純にNewTaskでアクセスできます。(this.stateの呪縛から開放される!!)

stateの更新の際はthis.setState({ ErrorMessage: '入力が空です。' })ではなく
state定義時の関数をそのまま使用できます。
つまりsetErrorMessage('入力が空です。')と書けます。

タスクの追加時にはTask.jsからpropsとして受け取ってあるdispatchを使用しています。

props.dispatch({
    type: 'add',
    NewTask: <Task key={TaskId} id={TaskId} name={NewTask} dispatch={props.dispatch} />
})

typeNewTaskはactionで取れる様に追加しています。

2-2-4. render

TaskAdd.jsのrender
return (
    <section className='task-creator'>
        <h2>Task Add</h2>
        <input className='task-item-text' type="text" placeholder="Task" value={NewTask}
            onChange={(e) => setTask(e.target.value)} />
        <button className='task-add-btn' type="button" onClick={handleClick}>
            Add
            </button>
        <p className='error-msg'>{ErrorMessage}</p>
    </section>
)

this.stateで取得することが無くなったので、少しスッキリしたと思います。

2-3. Task.js

2-3-1. ソース全体

Task.js
import React, { useState } from "react";

function Task(props) {

    const [isDone, setStatus] = useState(props.isDone)
    const task_id = 'task-id-' + props.id.toString()
    const css_label = 'task-item-label'
    const css_isDone = `${css_label} isDone`
    const css_Wip = `${css_label} WorkInProgress`

    return (
        <li className='task-item-row'>
            <input id={task_id} className='task-item-checkbox' type='checkbox'
                onChange={() => (isDone === true) ? setStatus(false) : setStatus(true)}>
            </input>
            <label htmlFor={task_id} className={(isDone === true) ? css_isDone : css_Wip}>
                {props.name}
            </label>
            <i className="material-icons icon" onClick={() => props.dispatch({type:'delete', TaskId:props.id})}>
                delete
            </i>
        </li>
    );
}
export default Task; 

2-3-2. stateの定義

stateと定数の定義
const [isDone, setStatus] = useState(props.isDone)
const task_id = 'task-id-' + props.id.toString()
const css_label = 'task-item-label'
const css_isDone = `${css_label} isDone`
const css_Wip = `${css_label} WorkInProgress`

stateとclassNameの定数を定義しているのみです。

2-3-3. render

Task.js
return (
    <li className='task-item-row'>
        <input id={task_id} className='task-item-checkbox' type='checkbox'
            onChange={() => (isDone === true) ? setStatus(false) : setStatus(true)}>
        </input>
        <label htmlFor={task_id} className={(isDone === true) ? css_isDone : css_Wip}>
            {props.name}
        </label>
        <i className="material-icons icon" onClick={() => props.dispatch({type:'delete', TaskId:props.id})}>
            delete
        </i>
    </li>
);

onChange={() => (isDone === true) ? setStatus(false) : setStatus(true)}で単純なif文を省略しています。
onClick={() => props.dispatch({type:'delete', TaskId:props.id})}でpropsで受け取ったdispatchを使用してタスクの削除を行なっています。

2-4. TaskList.js

TaskList.js
import React from "react";

function TaskList(props) {
    return (
        <section className='task-list'>
            <h2>Task List</h2>
            <ul>
                {props.TaskList}
            </ul>
        </section>
    );
}

export default TaskList; 

特にクラスと関数で違いは無いでしょうか、強いて言えばクラスで書くより短いくらいでしょうか??

3. まとめ

Reactのhookを使用して、クラスコンポーネントを関数コンポーネントのみで書き換えてみました。
スッキリ書き換えられるのはメリットに感じました。
ドキュメントを読む限りでは、完全に互換性がある訳では無いそうなので更に勉強が必要そうです。。。
最後まで見てくださり、ありがとうございます。
質問や、指摘は大歓迎ですので、よろしくお願いします。

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

create-react-appでpackage.jsonが作成されない

package.jsonが作成されない...

Reacteでアプリ作成しようと色なサイトみながらやっているのですが、環境構築初っ端から起動しない。

#バージョン
yarn 1.21.1
node 12.14.1
npm 6.13.4
create-react-app 3.3.0
※ここが3.3.0の方は注意

原因

結論から言うとどうやらcreate-react-app 3.3.0このバージョンに限って、.gitignoreファイルが存在する状態でやるとバグか何かで動作しないらしい。

解決

Node指定→githubでリポジトリ作成→git clone ~~ → create-react-app ../アプリ名
だとNode指定のタイミングで.gitignoreこいつができてしまうのでこの手順でやるならNode指定はせずに、
githubでリポジトリ作成→git clone ~~ → create-react-app ../アプリ名
の手順でやるとpackage.jsonが目だたく作成できた!!

まとめ

無知のことを一からやるときは苦労がつきものだが、諦めなければ必ず解決までたどり着ける!!
と再度認識できた!!!!笑

P.S.
まとめはReact関係ありません。笑

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

Reactのreduxを用いたログイン処理周りの実装【初学者のReact✗Railsアプリ開発第4回】

やったこと

フロントエンド: React, バックエンド: Rails(APIモード)のアプリ開発における基本的なログイン処理周りの実装
test200111.gif

Rails

まずは、ログイン中のユーザーをjsonとして返すために、userコントローラーを作成します。

$ docker-compose run api rails g controller users
users_controller
module Api
  module V1
    class UsersController < ApplicationController
      before_action :authenticate_api_v1_user!

      def currentuser
        @user = current_api_v1_user
        render json: { status: 'SUCCESS', message: 'Loaded the user', data: @user}
      end

    end
  end
end
  • current_userを使うと、簡単にログイン中のユーザーを返してくれる。ログインしてなかったら、エラーを返す。ルートのパスが/api/v1/user/なので、current_api_v1_userになる。
omniauth_callbacks_controller
          def render_data_or_redirect(message, data, user_data = {})
            ##if Rails.env.production?  コメントアウト!
              if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
                render_data(message, user_data.merge(data))


              # 通常、elsif内の処理が実行されるはず。
              elsif auth_origin_url
                redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true))


              else
                fallback_render data[:error] || 'An error occurred'
              end
            else
              # @resource.credentials = auth_hash["credentials"]

              ##render json: @resource, status: :ok コメントアウト!
            end
          end

React

要約

  • Routingでログインが必要なコンポーネントは、Authコンポーネントを経由させて出力させる。
  • Authコンポネートで、未ログイン・ログイン済みを確認して、未ログインならリダイレクト、ログイン済みなら、パスのコンポーネントを表示させた上で、reduxのglobal stateにログイン中ユーザーの情報をセットする。

redux

なんで必要?
-> state管理(状態管理)を楽にするため。reduxが無いと、親から子、子から親などコンポーネント間のデータの受け渡しが必要であるが、reduxを使うと、一つの情報源に全てのコンポーネントが直接アクセスできる。

今回はなぜ使った?
-> ログイン中ユーザーの情報はどのコンポーネントでも使いうるため、"Global State"として、全体で管理したかったから。

App.js

App.js
import React, { Component } from 'react';
import './App.css';
import Home from './containers/Home';
import Term from './containers/Term';
import Info from './containers/Info';
import Auth from './containers/Auth';
import Login from './containers/Login';


import { BrowserRouter, Route, Switch } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <div className="App">
        <BrowserRouter>
          <Switch>
            <Route path="/login" component={Login} />
            <Route path="/info" component={Info} />
            <Route path="/term" component={Term} />
            <Auth>
              <Route exact path="/" component={Home} />
            </Auth>
          </Switch>
        </BrowserRouter>
      </div >
    );
  }
}

export default App;

Login.js

Login.js
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';


const styles = theme => ({
});

class Login extends React.Component {
  loginTwitter() {
    window.location.href = process.env.REACT_APP_API127_URL + '/api/v1/auth/twitter?auth_origin_url=' + process.env.REACT_APP_BASE_URL;
  }

  render() {
    const { classes } = this.props;
    return (
      <div className={classes.login}>
        <p>未ログイン</p>
        <Button variant="contained" color="secondary" onClick={this.loginTwitter}>
          Twitterで登録・ログイン
        </Button>
      </div>
    )
  }
}

Login.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles, { withTheme: true })(Login);

Home.js

Home.js
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const styles = theme => ({
  home: {
    backgroundColor: "red",
    width: "50%"
  },
});

class Home extends React.Component {
  render() {
    const { classes } = this.props;
    return (
      <div className={classes.home}>
        <p>ログイン済み</p>
      </div>
    )
  }
}

Home.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles, { withTheme: true })(Home);

Auth.js

Auth.js
import React from 'react';
import PropTypes from 'prop-types';

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import * as actions from '../actions';
import _ from 'lodash';
import { Redirect, Route } from 'react-router-dom'
import "normalize.css";

import queryString from 'query-string';
import axios from 'axios';

const styles = theme => ({
});

class Auth extends React.Component {
  constructor(props) {
    super()
    this.state = {
      isLoading: true,
    }
  }

  componentDidMount() {
    let tokens = queryString.parse(_.get(this, "props.location.search"))
    if (!_.isEmpty(tokens.auth_token)) {
      localStorage.setItem('auth_token', tokens.auth_token)
      localStorage.setItem('client_id', tokens.client_id)
      localStorage.setItem('uid', tokens.uid)
      window.location.href = process.env.REACT_APP_BASE_URL
    } else {
      this.setState({
        isLoading: true,
      })

      const auth_token = localStorage.auth_token
      const client_id = localStorage.client_id
      const uid = localStorage.uid
      axios.get(process.env.REACT_APP_API_URL + '/api/v1/user/currentuser', {
        headers: {
          'access-token': auth_token,
          'client': client_id,
          'uid': uid
        }
      })
        .then((response) => {
          this.setState({
            isLoading: false,
            isLoggedin: true,
          });
          this.props.actions.setCurrentUserSuccess(response.data.data)
        })
        .catch(() => {
          this.setState({
            isLoading: false,
            isLoggedin: false,
          });
        });
    }
  }

  render() {
    const { CurrentUserReducer } = this.props;
    const isLoggedin = this.state.isLoggedin;
    const isLoading = this.state.isLoading;
    const { classes } = this.props;

    console.log(isLoading)

    if (isLoading) {
      return (
        <div>loading</div>
      )
    } else {
      if (isLoggedin) {
        return (
          <Route children={this.props.children} />
        )
      } else {
        console.log(isLoading)
        return (
          <Redirect to={'/login'} />
        )
      }
    }
  }
}

Auth.propTypes = {
  classes: PropTypes.object.isRequired,
};

const mapState = (state, ownProps) => ({
  CurrentUserReducer: state.CurrentUserReducer,
});
function mapDispatch(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

export default connect(mapState, mapDispatch)(Auth);
  • このコンポーネントのコードこそが、今回の記事のいちばん重要なポイント。
  • componentDidmount()は、render後に実行される。初めのif文では、ログイン処理を行ってrails側から戻ってきているかどうかを判別。
  • 先程、説明したように、rails側でログイン処理が終わると、devise_token_authでの認証に必要なtokenがクエリとしてURLに乗せられて、/に戻ってきます。if文ではqueryStringというモジュールで、URLの文字列を分析し、?auth_token="..."が存在しているかどうかを判定し、存在していたら、認証に必要なトークンなどをlocalstorageに保存しています。
  • else内では、ログインしているかどうかを判定するために、rails側で先程実装したように、/api/v1/user/currentuserにアクセスして、ログインをしていたら、actionを発行して、reduxを使用して、global stateとして、CurrentUserReducerにログイン中のユーザー情報を保存するようにしています。
  • localのstateとしては、isLoggedin(ログインしているかどうか)とisLoading(currentuserを確認中かどうか)を管理していて、これらを使って、Loadingを表示するか、そのままログイン済みユーザーのみ表示させたいコンポーネントへのアクセスを許可するか、/loginにリダイレクトするかを決めています。
  • Reactのライフサイクルを理解しきっていないため、タイミングの調節が結構苦労しました。

redux

実装の概要

  • index.jsでcreateStoreする、Providerで配る。
  • reducers/rootReducer.jsで複数のreducerを管理している。
  • actions/index.js内で、アクションの内容を記述している。(今回は、setCurrentUserSuccessしか使っていませんが...)
  • reducers/CurrentUserReducer.js内でdispatchされたactionに対するstateの変更の記述をしている。

起こっていること

  1. Auth.js内のthis.props.actions.setCurrentUserSuccess(response.data.data)でactions/index.jsに対して、データが渡されて、actionのオブジェクトがcreateされる。
  2. createされたactionのオブジェクトはreducers/CurrentUserReducer.jsに渡されて、stateの変更が行われる。

index.js

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { MuiThemeProvider } from '@material-ui/core/styles';
import { theme } from './materialui/theme'
import { BrowserRouter as Router } from 'react-router-dom';

import createBrowserHistory from 'history/createBrowserHistory';
import { Provider } from 'react-redux';
import rootReducer from './reducers/rootReducer';
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import { createStore, applyMiddleware, compose } from 'redux';


const history = createBrowserHistory();

const store = createStore(
  rootReducer,
  applyMiddleware(thunk, logger)
);

ReactDOM.render(
  <Provider store={store}>
    <MuiThemeProvider theme={theme} >
      <Router>
        <App />
      </Router>
    </MuiThemeProvider>
  </Provider >
  , document.getElementById('root'));

actions/index.js

index.js
import axios from 'axios'

export const setCurrentUser = () => {
  return (dispatch) => {
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid

    return axios.get(process.env.REACT_APP_API_URL + '/api/v1/user/currentuser', {
      headers: {
        'access-token': auth_token,
        'client': client_id,
        'uid': uid
      }
    })
      .then(response => dispatch(setCurrentUserSuccess(response.data.data)))
      .catch(error => dispatch(setCurrentUserFailure(error)))
  };
}

export const setCurrentUserRequest = () => ({
  type: 'SET_CURRENTUSER_REQUEST',
})


export const setCurrentUserSuccess = (json) => ({
  type: 'SET_CURRENTUSER_SUCCESS',
  items: json,
})

export const setCurrentUserFailure = (error) => ({
  type: 'SET_CURRENTUSER_FAILURE',
  items: error,
})

reducers/CurrentUserReducer.js

CurrentUserReducers.js
const initialState = {
  isLoggedin: false,
  isLoading: false,
  items: []
};

const CurrentUserReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'SET_CURRENTUSER_REQUEST':
      return {
        ...state,
        isLoggedin: false,
        isLoading: true,
        items: [],
      };
    case 'SET_CURRENTUSER_SUCCESS':
      if (!action.items) {
        return {
          ...state,
          isLoggedin: false,
          isLoading: false,
          items: action.items,
        };
      } else {
        return {
          ...state,
          isLoggedin: true,
          isLoading: false,
          items: action.items,
        };
      }

    case 'SET_CURRENTUSER_FAILURE':
      return {
        ...state,
        isLoggedin: false,
        isLoading: false,
        error: action.error,
      };
    default:
      return state;
  }
};

export default CurrentUserReducer;

reducers/rootReducer.js

rootReducer.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import CurrentUserReducer from './CurrentUserReducer'

const rootReducer = combineReducers({
  CurrentUserReducer,
  router: routerReducer,
})

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

gatsbyにnpmでpluginを追加するとプロジェクトが壊れる話

2020年1月12日の時点で、gatsbyのプロジェクトにnpmでpluginを追加すると、プロジェクトが壊れてしまうようです。

具体的には下記のエラーが出てしまいます。

Error: Cannot find module 'react' - after installing Plugin #18048

再現手順

gatsby new hello
cd hello
gatsby develop

ここまでは大丈夫ですが、そのあと例えばGoogleのフォントを使いたいなと思って下記のものを実行します。

npm install gatsby-plugin-google-fonts --save

gatsby-plugin-google-fonts

すると、reactモジュールがないと言われ初めてしまいます。

$ gatsby develop
...
There was a problem loading the local develop command. Gatsby may not be installed. Perhaps you need to run "npm install"? Cannot find module 'react'
Require stack:
- /Users/dkato/webspace/hello/node_modules/ink/build/instance.js
- /Users/dkato/webspace/hello/node_modules/ink/build/render.js
- /Users/dkato/webspace/hello/node_modules/ink/build/index.js
- /Users/dkato/webspace/hello/node_modules/gatsby/node_modules/gatsby-cli/lib/reporter/loggers/ink/index.js
- /Users/dkato/webspace/hello/node_modules/gatsby/node_modules/gatsby-cli/lib/reporter/index.js
- /Users/dkato/webspace/hello/node_modules/gatsby/dist/schema/types/type-defs.js
- /Users/dkato/webspace/hello/node_modules/gatsby/dist/redux/reducers/inference-metadata.js
- /Users/dkato/webspace/hello/node_modules/gatsby/dist/redux/reducers/index.js
- /Users/dkato/webspace/hello/node_modules/gatsby/dist/redux/index.js
- /Users/dkato/webspace/hello/node_modules/gatsby/dist/utils/webpack.config.js
- /Users/dkato/webspace/hello/node_modules/gatsby/dist/commands/develop.js
- /usr/local/lib/node_modules/gatsby-cli/lib/create-cli.js
- /usr/local/lib/node_modules/gatsby-cli/lib/index.js



  Error: Cannot find module 'react'
  Require stack:
  - /Users/dkato/webspace/hello/node_modules/ink/build/instance.js
  - /Users/dkato/webspace/hello/node_modules/ink/build/render.js
  - /Users/dkato/webspace/hello/node_modules/ink/build/index.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/node_modules/gatsby-cli/lib/reporter/loggers/ink  /index.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/node_modules/gatsby-cli/lib/reporter/index.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/dist/schema/types/type-defs.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/dist/redux/reducers/inference-metadata.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/dist/redux/reducers/index.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/dist/redux/index.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/dist/utils/webpack.config.js
  - /Users/dkato/webspace/hello/node_modules/gatsby/dist/commands/develop.js
  - /usr/local/lib/node_modules/gatsby-cli/lib/create-cli.js
  - /usr/local/lib/node_modules/gatsby-cli/lib/index.js

暫定的な対策

パッケージマネージャーとしてnpmではなくyarnを使えばいいらしいです。

yarn add gatsby-plugin-google-fonts

すでにnpmでinstallした場合は、下記の方法でpackageを再インストールすれば治りました。

$ rm -Rf node_modules package-lock.json
$ yarn install

https://github.com/gatsbyjs/gatsby/issues/18048#issuecomment-562786372

リポジトリのIssuesをいくつか見た限りでは、どうやらnpmとyarnで依存関係の解決方法が異なることが原因らしいです。そのうち治るのかな?

環境など

$ node --version
v13.6.0
$ npm --version
6.13.4
$ yarn --version
1.21.1
$ gatsby --version
Gatsby CLI version: 2.8.26
Gatsby version: 2.18.12

macOS 10.15.2

余談

もともとReactを少し触ったことがあったのですが、去年のアドベントカレンダーにハイテンションな記事があったのでGatsbyJSを触り始めました。眺めるだけでも楽しい記事です。

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

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

GatsbyJSでタグ一覧を作成するときにgroup(field: frontmatter___tags) が表示されない

解決案というより、代替案なのですが公式ページに習ってGatsbyJSで作成したブログにタグ機能を追加しようとしたとき、

{
  allMarkdownRemark {
    group(field: frontmatter___tags) {
      tag: fieldValue
      totalCount
    }
  }
}

本来上記のようにタグ情報を引っ張ってくるのですが、私の場合なぜか

field: frontmatter___tags

こちらが表示されませんでした。なので代替案として以下のように取得することができました。

{
  allContentfulPost {
    group(field: tags) {
      fieldValue
      totalCount
    }
  }
}

このように記述すると以下のようにデータを取得することができます。

{
  "data": {
    "allContentfulPost": {
      "group": [
        {
          "fieldValue": "Rails",
          "totalCount": 1,
          "nodes: {
            (あとはそのタグがついた記事情報が入る)
          }
        },
        {
          "fieldValue": "GatsbyJS",
          "totalCount": 1,
          "nodes: {
            (あとはそのタグがついた記事情報が入る)
          }
        },
        {
          "fieldValue": "Contentful",
          "totalCount": 4,
          "nodes: {
            (あとはそのタグがついた記事情報が入る)
          }
        }
      ]
    }
  }
}

しかしこのままだとタグを押したときに全てのタグの記事詳細を持ってきてしまうため、
クリックしたタグと同じタグを持つ記事を絞り込むためにfilterメソッドなどを通して記事を絞り込む必要があります。

const posts = data.allContentfulPost.group.filter((post) => {
                return post.fieldValue === tag
              })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GatsbyJSで作成したブログのタグ一覧を作成するときにgroup(field: frontmatter___tags) が表示されない

解決案というより、代替案なのですが公式ページに習ってGatsbyJSで作成したブログにタグ機能を追加しようとしたとき、

{
  allMarkdownRemark {
    group(field: frontmatter___tags) {
      tag: fieldValue
      totalCount
    }
  }
}

本来上記のようにタグ情報を引っ張ってくるのですが、私の場合なぜか

field: frontmatter___tags

こちらが表示されませんでした。なので代替案として以下のように取得することができました。

{
  allContentfulPost {
    group(field: tags) {
      fieldValue
      totalCount
    }
  }
}

このように記述すると以下のようにデータを取得することができます。

{
  "data": {
    "allContentfulPost": {
      "group": [
        {
          "fieldValue": "Rails",
          "totalCount": 1,
          "nodes: {
            (あとはそのタグがついた記事情報が入る)
          }
        },
        {
          "fieldValue": "GatsbyJS",
          "totalCount": 1,
          "nodes: {
            (あとはそのタグがついた記事情報が入る)
          }
        },
        {
          "fieldValue": "Contentful",
          "totalCount": 4,
          "nodes: {
            (あとはそのタグがついた記事情報が入る)
          }
        }
      ]
    }
  }
}

しかしこのままだとタグを押したときに全てのタグの記事詳細を持ってきてしまうため、
クリックしたタグと同じタグを持つ記事を絞り込むためにfilterメソッドなどを通して記事を絞り込む必要があります。

const posts = data.allContentfulPost.group.filter((post) => {
                return post.fieldValue === tag
              })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React使いのためのElm入門(React Hooks編)

お先にこちらの記事も併せてお読みください。

Reactを普段書いている人がElmにあまり抵抗感を持たずに始めれるように、全く同じ挙動をするReactとElmのコードを並べてコードの説明をしていきたいと思います。あまり初歩的すぎる例だと実際に使うイメージが付かないだろうと言うことで、React Hooksを使い副作用を含むコードを2つ用意しました。注意点としてReactを使う成果物を私自身が久しぶりで、React Hooksの使用が初めてであると言うことと、Elmのコードに近くなるように、普段Reactを使っている方からすると不自然に見えるかもしれないコードであることをご了承ください(あらかじめコードはTwitterで展開し様々なコード例をいただいているので、興味がある方は私のTwitterを遡ってみるとおもしろいかもしれません)。

乱数カウンタ

副作用を簡単に確かめたり説明するときは、よく手軽に副作用を発生させれる乱数カウンタを例として作ります。乱数の扱いさえ分かれば、HTTP通信やDBの操作などはインターフェースが異なるAPIというだけで基本は同じだと考えています。

Image from Gyazo

React

useStateを用いて、初期値0から始まり今のカウンタの数値を保存する状態countを用意しました。乱数は1-10の値が入りuseRefuseEffectを用いてレンダリングが走るごとに更新されるようにしています。onClick関数では、乱数をcountに足し合わせて更新を掛けています。

import React, {useState, useEffect, useRef} from 'react';

const getRandomInt = (max) => 1 + Math.floor(Math.random() * Math.floor(max));

function App() {
    const [count, setCount] = useState(0);
    const ref = useRef(0);

    useEffect(() => {
        ref.current = getRandomInt(10);
    });

    return (<div>
        <p>Random countup: {count}</p>
        <button onClick={
            () => setCount(count + ref.current)
        }>random countup</button>
    </div>);
}

export default App;

Elm

Modelと言うのは習慣的に使われるアプリケーションの状態を扱う型の別名です(TypeScriptで言うところの、type Model = number;と同じです)。今回はReactの例のcountになります。全く同一にするのであれば、type Model = { count: Int }と言うレコード(オブジェクト)の形にも出来ましたが、コードを単純にするためにこうなりました。

次にview関数を見ると、Reactのコード例で計算をしてしまっていましたが、RandomCountupと言うメッセージ(指令)を飛ばすだけです。

update関数では、RandomCountupに関する処理の分岐があります。update関数の戻り値は、タプルで(Model, Cmd Msg)という形になります。Modelで計算され値によって次のレンダリングがされるかどうかが変わります。つまりsetCount関数と同様の効果があります。しかし、RandomCountupのタプルの最初の値を見ると、modelのままで値が加算されていません。これは、副作用によって得られる値はElmの関数内では、直接取得できないためです。代わりにRandom.generate GotRandomInt (Random.int 1 10)(実際コードでは、<|がありますが、これは括弧の省略だと思ってください)と言うCmd Msgの値が返されています。GotRandomIntは、Elmランタイム(JavaScript)から乱数を受け取るためのMsgです。Random.generateは、MsgとRandom.Generatorを受け取って副作用を発生させる方法です。ReactのuseEffectに当たる部分です。ElmにおいてCmdと次の例で見せるSubと言う方法でしか、副作用を起こす方法はありません。

GotRandomIntの分岐を見てみましょう。GotRandom nのnがElmランタイムから受け取った乱数の値です。タプルの最初では、model + nとmodelに乱数を足し合わせて更新しています。タプルの二つ目の値つまり、Cmdは副作用をこれ以上起こす必要はないため、Cmd.noneとなります。

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Random


main =
    Browser.element { init = init, update = update, view = view, subscriptions = \_ -> Sub.none }


type alias Model =
    Int


init : () -> ( Model, Cmd Msg )
init _ =
    ( 0, Cmd.none )


type Msg
    = RandomCountup
    | GotRandomInt Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RandomCountup ->
            ( model, Random.generate GotRandomInt <| Random.int 0 10 )

        GotRandomInt n ->
            ( model + n, Cmd.none )


view : Model -> Html Msg
view model =
    div []
        [ p [] [ text <| "Random countup: " ++ String.fromInt model ]
        , button [ onClick RandomCountup ] [ text "random countup" ]
        ]

おまけ

Twitterのフォロワーの方が、Elmに完全に寄せたReactのコードを書いてくれました。記事では私自身の言葉で説明できる形を保ちたかったため、そのまま私のコードを載せました。pxfncさんありがとうございました!

タイマー

今回お見せするのは、タイマーです。タイマーは定期的に処理が走るタイプの副作用です。実務で言えば、重い処理をバックグラウンド処理として走らせ、その結果を定期的に確認するなどの処理が書けるようになります。

Image from Gyazo

React

timeは、0で初期化されmsの単位で管理される数値です。加えて、タイマーを止めたときに今のtimeの値を保存する数値stoppedTimeとタイマーを動かした時間を保存するlastStartedAt値、この3つの状態でタイマーは表現されます。タイマーの描画や状態の更新は、requestAnimationFrame関数を利用して行われます。その他の細かいロジックに関しては本質的ではないため、省略します。

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

export default function App() {
  const [stoppedTime, setStoppedTime] = useState(0);
  const [time, setTime] = useState(0);
  const [lastStartedAt, setLastStartedAt] = useState(null);

  const calcTime = () => {
    return lastStartedAt === null
      ? stoppedTime
      : stoppedTime + new Date().getTime() - lastStartedAt;
  };

  const onClick = () => {
    if (lastStartedAt === null) {
      setLastStartedAt(new Date().getTime());
      return;
    }
    setStoppedTime(calcTime());
    setLastStartedAt(null);
    return;
  };

  useEffect(() => {
    const requestId = requestAnimationFrame(() => setTime(calcTime()));
    return () => cancelAnimationFrame(requestId);
  });

  return (
    <div>
      <p>timer (sec): {Math.floor(time / 1000)}</p>
      <p>timer (ms): {time}</p>
      <p>
        <button onClick={onClick}>
          {lastStartedAt === null ? "start" : "stop"}
        </button>
      </p>
    </div>
  );
}

このReactコードはフォロワーのn_1215さんに書いていただきました。ありがとうございます!

Elm

こちらのコードはReactとほぼ同じ構造のため、差分だけを説明します。

ModelのうちlastStartedAtMaybeはあるかないか(nullが含まれるかどうか)の型は、Maybe aと言う型で表されます。aには任意の型が入るため今回は、Intとなります。この値は通常のIntと演算はできないため、必ずパターンマッチで分岐(値があるJust aか、無いか Nothing)するか無い場合のデフォルト値を設定する必要があり、コンパイル時にチェックされます。TypeScriptのnullとのユニオンの型と同じ感覚で使えば良いと思います。

requestAnimationFrameのように定期購読が必要なコードは全て、subscription関数にまとめられます。今回はそのままrequestAnimationFrameのイベントをハンドリングするonAnimationFrameと言う関数があるため、そちらを使用しました。Sub Msg型はCmdと使い方のノリは変わりありません。定期的に実行されたときにupdateで動くおなじみのMsgを設定してあげるだけになります。今回は、Tick Time.Posixと言うUnix時間を表す型を受け取るMsgが働いてくれます。

他にはrequestAnimationFrame以外で現在時刻を受け取りたいときは、副作用なので乱数のときと同じようにCmd Msgを発行してあげる必要があります。例えば、Task.perform SetLastStartedAt Time.nowの部分がその部分に当たります。Task.performはRandom.generateと同様に加工可能な現在時刻を加工し終わった後にCmd Msgに変換してくれる関数になります。(リファレンスやそれに関して記事をいくつか書いているため気になる人は調べてみてください)。

module Main exposing (main)

import Browser
import Browser.Events exposing (onAnimationFrame)
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Task
import Time


main =
    Browser.element { init = init, update = update, view = view, subscriptions = subscriptions }


type alias Model =
    { time : Int
    , stoppedTime : Int
    , lastStartedAtMaybe : Maybe Int
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( {  time = 0, stoppedTime = 0,lastStartedAtMaybe = Nothing }, Cmd.none )


type Msg
    = Tick Time.Posix
    | StartStop
    | SetLastStartedAt Time.Posix
    | SetStoppedTime Int Time.Posix


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    let
        calcTime now lastStartedAt =
            model.stoppedTime + Time.posixToMillis now - lastStartedAt
    in
    case msg of
        Tick now ->
            ( { model
                | time =
                    case model.lastStartedAtMaybe of
                        Just lastStartedAt ->
                            calcTime now lastStartedAt

                        Nothing ->
                            model.stoppedTime
              }
            , Cmd.none
            )

        StartStop ->
            case model.lastStartedAtMaybe of
                Just lastStartedAt ->
                    ( model, Task.perform (SetStoppedTime lastStartedAt) Time.now )

                Nothing ->
                    ( model, Task.perform SetLastStartedAt Time.now )

        SetStoppedTime lastStartedAt now ->
            ( { model
                | stoppedTime =
                    calcTime now lastStartedAt
                , lastStartedAtMaybe = Nothing
              }
            , Cmd.none
            )

        SetLastStartedAt now ->
            ( { model | lastStartedAtMaybe = Just <| Time.posixToMillis now }, Cmd.none )


view : Model -> Html Msg
view model =
    div []
        [ p [] [ text <| "timer (sec): " ++ (String.fromInt <| model.time // 1000) ]
        , p [] [ text <| "timer (ms): " ++ String.fromInt model.time ]
        , button [ onClick StartStop ]
            [ text <|
                case model.lastStartedAtMaybe of
                    Just _ ->
                        "stop"

                    Nothing ->
                        "start"
            ]
        ]


subscriptions : Model -> Sub Msg
subscriptions _ =
    onAnimationFrame Tick

他の副作用を含むElmの例

上記の2つのコードが読めてしまえば、基本的にElmの副作用を含むコードを読むことができるようになります。Elm公式ではexamplesがいくつか公開されています。Random, Http, Timeがそれに該当するためよければご覧ください。

まとめ

この記事では単発での副作用と定期購読のための副作用を表現するコードをReactとElmでそれぞれ紹介しました。Reactを普段使う人がElmを使うイメージが掴むことができ、チャレンジする人が増えたら嬉しいなと思います。この記事を書くに当たって感じたことですが、記事の通りReactであろうがElmであろうが実現できることに変わりはありません(VueやAngularも言うまでもありませんね)。しかし、使うユーザの思考性は異なる部分があると感じました。Elmは私が書いたからこのようなコードになったわけではなく、Elmユーザが書いた場合、アルゴリズムを除いて間違いなく同じコードになります。これはCmdとSubの2つの方法でしか、副作用を表現できないためです。一方、Reactは僕とフォロワーでコードが十人十色でした。プログラミング自体を楽しみたいと言う方は、TypeScript+Reactの選択をすると良いかもしれません。一方、細かい書き分けの選択肢がない分Elmはプログラミング自体の幅を楽しみたい人にとっては苦痛かもしれませんが、逆にその幅が苦痛であったりアプリケーションの作成に集中したい方、プログラミング初心者の方にとってはElmを選択すると良いかもしれません。また、その特性だけにフォーカスした場合は円滑なプロダクトの進行はElmの方が良いと感じましたが、言語特性以外にもエコシステムやユーザ間の情報共有の円滑さを考えるとReactに分があると思います。しかしこれに関しては、ReactもElmも使えるユーザが増えると状況が変わってる話なので、是非Elmを触るユーザが増えていけば良いなと思いました。それでは良いReactとElmライフを!

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

ReactへのMaterial-UI、react-routerの導入【初学者のReact✗Railsアプリ開発第3回】

やったこと

Reactの初期画面の状態からMaterial-UIとreact-routerによるルーティングを実装する

開発環境

DockerでRuby on Rails + Reactを別々にアプリ作成する環境構築手順

実装手順

各種モジュールのインストール

Materai-UI、react-router、react-reduxなどアプリ開発で使うモジュールをインストールします。
(今回使わないものも含みます)

package.json
{
  "name": "app_name",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "@loadable/component": "^5.10.3",
    "@material-ui/core": "^4.6.1",
    "@material-ui/icons": "^4.5.1",
    "@material-ui/styles": "^4.6.0",
    "axios": "^0.19.0",
    "connected-react-router": "^6.6.0",
    "lodash": "^4.17.15",
    "material-table": "^1.54.2",
    "material-ui": "^0.20.2",
    "material-ui-flat-pagination": "^4.0.0",
    "normalize.css": "^8.0.1",
    "prop-types": "^15.7.2",
    "query-string": "^6.9.0",
    "react": "^16.12.0",
    "react-custom-scrollbars": "^4.2.1",
    "react-dom": "^16.12.0",
    "react-infinite-scroll-component": "^5.0.4",
    "react-redux": "^5.0.4",
    "react-router-dom": "^4.2.2",
    "react-router-redux": "^5.0.0-alpha.9",
    "react-scripts": "3.3.0",
    "react-share": "^3.0.1",
    "react-spring": "^8.0.27",
    "react-swipe-card-chsstm": "^0.1.5",
    "react-swipeable-views": "^0.13.3",
    "react-with-gesture": "^4.0.8",
    "recharts": "^2.0.0-beta.1",
    "redux": "^4.0.4",
    "redux-form": "^8.2.6",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0",
    "styled-components": "^4.4.1",
    "vec-la": "^1.5.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
$ npm install

Reactの仕組み

超ざっくりな理解としてはこんな感じですかね。
スクリーンショット 2020-01-11 10.18.47.png

  • サイトとしてはpublic/index.htmlが表示される
  • <div id=”root”></div>の部分がsrc/index.jsによりレンダリングされる
  • <App/>はsrc/App.jsから出力されたもの

react-router

Reactでルーティングを行うライブラリです。
ルーティングを一言でいうと、パスと出力したい内容を結ぶことです。
XXX.com/Homeにアクセスしたら、Home.jsの内容を表示する。
XXX.com/Infoにアクセスしたら、Info.jsの内容を表示する。みたいな...

Material-UI

Googleが言っている「マテリアルデザイン」を簡単にReactに実装できるようにしたパッケージ

今回作るもの

/

スクリーンショット 2020-01-12 6.19.25.png

/term

スクリーンショット 2020-01-12 6.19.15.png

/info

スクリーンショット 2020-01-12 6.19.08.png

  • /,/home,/infoでルーティングする
  • Material-UIを導入し、スタイリングを行う

src/index.js

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

import { MuiThemeProvider } from '@material-ui/core/styles';
import { theme } from './materialui/theme'
import { BrowserRouter as Router } from 'react-router-dom';

ReactDOM.render(
  <MuiThemeProvider theme={theme}>
    <Router>
      <App />
    </Router>
  </MuiThemeProvider>
  , document.getElementById('root'));
  • MUIThemeProviderタグで囲むことで、MaterialUIを適用している。theme={theme}の記述は、./materialui/theme.jsに記載したテーマカラーのカスタムを適用している
  • Routerタグで、Routingを実現している

materiaiui/theme.js

theme.js
import { createMuiTheme } from '@material-ui/core/styles'

export const theme = createMuiTheme({  // #1
  palette: {
    primary: {
      light: '#484848',
      main: '#212121',
      dark: '#000000',
      contrastText: '#ffffff',
    },
    secondary: {
      light: '#ffffff',
      main: '#ffffff',
      dark: '#ffffff',
      contrastText: '#000000',
    },
  },
})

src/App.js

App.js
import React, { Component } from 'react';
import './App.css';
import Home from './containers/Home';
import Term from './containers/Term';
import Info from './containers/Info';
import { Route, Switch } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <div className="App">
        <BrowserRouter>
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/info" component={Info} />
            <Route path="/term" component={Term} />
          </Switch>
        </BrowserRouter>
      </div >
    );
  }
}

export default App;
  • BrowserRouterタグとSwitchタグとRouterタグを使ってルーティングを実装。この方の解説めっちゃ分かりやすい。 -> https://qiita.com/kosuke0820/items/77addd20db9e6f7809fe
  • Routeタグで、pathごとに、それぞれのコンポーネントに切り替えてくれる。
  • exactつけると全体一致、つけないとpathで先頭一致。

src/containers/Home.js

Home.js
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const styles = theme => ({
  home: {
    backgroundColor: "red",
    width: "50%"
  },
});

class Home extends React.Component {
  render() {
    const { classes } = this.props;
    return (
      <div className={classes.home}>
        <p>未ログイン</p>
        <Button variant="contained" color="secondary">
          Twitterで登録・ログイン
        </Button>
      </div>
    )
  }
}

Home.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles, { withTheme: true })(Home);
  • withStyles(styles, { withTheme: true })(Home); で 上部に記載のstylesを適用している。withStylesの書き方は古いみたい。react Hookへの移行を促しているから??? https://qiita.com/gumiTECH/items/9e0f3172b8f85e93cbbe
  • stylesのhomeに記載のスタイルが<div className={classes.home}>タグに適用されている。
  • styleの書き方がcssの書き方と微妙に異なる。ハイフンを使わずに大文字になっていたり。
  • Home.propTypes = { classes: PropTypes.object.isRequired, }; でちゃんとpropsの型チェックをしている。propsはコンポネートをカスタマイズできるようにするパラメータ。

src/containers/Info.js

Info.js
import React from 'react';

class Info extends React.Component {
  render() {
    return (
      <h1>Info</h1>
    )
  }
}

export default Info;

src/containers/Term.js

Term.js
import React from 'react';

class Term extends React.Component {
  render() {
    return (
      <h1>Term</h1>
    )
  }
}

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

REACT-MAP-GLのexampleを紹介します

概要

今、マップアプリをReactで作ろうとしています。
Uberが提供しているREACT-MAP-GLというライブラリを使おうと、色々試しています。
REACT-MAP-GLには色々なexampleが入ってますので、少し紹介します。

表示方法

全てのexapleは以下の手順で使うことができます。

MapboxからTokenを取得

MapboxのサイトからTokenを取得します。
Mapbox(リンク)

app.jsを編集

exampleフォルダの下に様々なexapleのフォルダがあります。
使いたいexampleフォルダを開きsrc/app.jsを開きます

以下の一文を編集します。

const MAPBOX_TOKEN = ''; // Set your mapbox token here

この「' '」にMapboxのTokenを入れます。

npm install
npm run start-local

これで表示されます。

exampleの紹介

controls

このexampleではポップアップを表示させることができます。
町のポップアップとWikipediaのリンクが確認できるかと思います。

Image from Gyazo

geojson-animation

json情報を元にmap上に描画を行います。
Image from Gyazo

draggable-markers

こちらはドラックできるマーカーを描画することができます。
Image from Gyazo

heatmap

こちらはヒートマップを描画します。
デフォルトでは地震の回数をもとにヒートマップを描いています。
Image from Gyazo

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