20191222のReactに関する記事は16件です。

PlantUMLのマインドマップを用いてRedux-SagaとRedux Storeについて考えた話

これは React Advent Calendar 2019 の22日目の記事です。

はじめに

先日、「TwitterやSlackのRedux Storeを覗く」という記事が話題になっていました。そこでも述べられているように、Reduxを用いたアプリケーションではStoreの設計が重要になってきます。筆者も、最近友人とReact(Redux+ReduxSaga)+FirebaseのWebアプリを開発する中で Storeの構成についてや、Redux-Sagaを呼び出すタイミングの整理をしたシーンがありました。その際、PlantUMLのマインドマップを用いたところ、スッキリと情報を整理することができました。この記事では、どういった経緯でマインドマップを利用することになったのか、またどのように活用したのかということを書いていきます。

特定のSagaを呼び出す適切なタイミングを考える必要があった

ReduxとRedux-Sagaを用いて開発を進める中で課題に感じていたことが「様々にあるSagaはどのタイミングで飛ばすべきなのか、どのタイミングで飛ばさなければいけないのか」ということです。またそれに関連して「画面をレンダリングする時に必要なstateが存在しないという状況を無くしたい」ということも感じていました。

何がいけなかったのか

上述した課題に直面した原因は「stateの依存関係およびそのstateを取得するSagaを飛ばすタイミングを考慮していなかったこと」です。この状況の例えとして、Twitterのようなフォローユーザー一覧画面やタイムライン画面を持つアプリを想定して考えてみます。

スクリーンショット 2019-12-22 17.46.32.png

上の図の中では、ホーム画面、フォローユーザー一覧画面、タイムライン画面の三つの画面があります。そして、それぞれの画面がそれぞれの画面の描写に必要なstateを持っています。ここでは、フォローユーザー一覧画面をレンダリングするタイミングでSagaを飛ばしてstateC(フォローユーザー情報)をFirestoreに問い合わせています。一方で、タイムライン画面はstateD(タイムライン情報)を持っているのですが、これはstateC(フォローユーザー情報)に依存しているstateになります。フォローしているユーザーの情報が無ければタイムラインを表示できないからです。ここにstateの依存関係があるのですが、これを考慮していなかったためフォローユーザー一覧画面を経由してからでないとタイムラインが正常にレンダリングできなかったり、タイムライン画面をレンダリングするタイミングでもう一度フォローユーザー情報を取得するSagaを飛ばさなければいけないという冗長な作りになっていました。

こういった問題が浮き彫りになったため、情報を整理するためPlantUMLのマインドマップを用いて考えてみます。

マインドマップを用いて整理する

前述した状況をマインドマップを用いて表現したのが以下の図です。

スクリーンショット 2019-12-22 18.15.11.png

先ほどの流れで、まずは画面ベースでstateを考えてみます。このマインドマップでは各画面が持つstateを末端に表しています。ここで、Sagaについて考える上での観点が二つあります。一つ目は、state Aがホーム画面とフォローユーザー一覧画面で重複していること、二つ目はタイムライン画面においてstate D(タイムライン情報)state C(フォローユーザ情報)に依存していることです

まずは一つ目について考えます。state Aに関して、それぞれの画面をレンダリングする時に別々のSagaによって取得することは冗長です。なので、state Aを取得するSagaは1回だけにして、認証後の初期外部リクエスト(initial saga)によって該当データを取得することにします。

次に二つ目についてです。state D(タイムライン情報)state C(フォローユーザ情報)に依存しています。つまり、state Cが無ければstate Dも表示させることができません。そこで、プロフィール画面もしくはタイムライン画面個別に紐づいたSagaではなく、一つのSagaとしてstate Cを取得することにします。こちらも認証後の初期外部リクエスト(initial saga)で飛ばすのが良さそうです。

スクリーンショット 2019-12-22 18.41.29.png

ここまでPlantUMLのマインドマップを用いてstateとSagaについて考えてみました。ここで、stateとSagaについてまとめてみます。下図のように、二つの画面で重複していたstate Aと、依存関係にあったstate Cはまとめて認証後の初期外部リクエスト(initial saga)によってデータを取得するようにしました。

スクリーンショット 2019-12-22 23.13.18.png

また、Storeに関しては下図のような構成になっています。最初は画面ベースでstateを考えていましたが、state Aが複数の画面で重複しているため冗長な構成を避けたいこと、開発規模が小さいことから最終的には画面ベースではなくドメインベースの構成になっています。これらに加えて、開発中のアプリのビューは完全に決定してるわけではないので、後々のビューの変更にも強いドメインベースを採用しています。画面ごとに分担作業するなどの場合は冗長でも画面ベースで考えるというのはありかもしれません。ちなみに、Redux公式ドキュメントはドメインベースでのStore設計を推奨しています。また、Store設計に関しては「TwitterやSlackのRedux Storeを覗く」の記事が勉強になりました。

スクリーンショット 2019-12-22 19.00.36.png

おわりに

今回、マインドマップを用いてSagaの呼び出し順序やStoreの構成を考えてみました。ReduxやRedux-Sagaを使用し複雑なビジネスロジックになる場合、整理のためにマインドマップを用いてみてはどうでしょうか。また、Storeの設計を考える上で「TwitterやSlackのRedux Storeを覗く」の記事はとても参考になると思いました。

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

マインドマップを用いてRedux-sagaとstoreの構成を整理した話

これは React Advent Calendar 2019 の22日目の記事です。

はじめに

先日、「TwitterやSlackのRedux Storeを覗く」という記事が話題になっていました。そこでも述べられているように、Reduxを用いたアプリケーションではStoreの設計が重要になってきます。筆者も、最近友人とReact(Redux+ReduxSaga)+FirebaseのWebアプリを開発する中で Storeの構成についてや、Redux-Sagaを呼び出すタイミングの整理をしたシーンがありました。その際、PlantUMLのマインドマップを用いたところ、スッキリと情報を整理することができました。この記事では、どういった経緯でマインドマップを利用することになったのか、またどのように活用したのかということを書いていきます。

特定のSagaを呼び出す適切なタイミングを考える必要があった

ReduxとRedux-Sagaを用いて開発を進める中で課題に感じていたことが「様々にあるSagaはどのタイミングで飛ばすべきなのか、どのタイミングで飛ばさなければいけないのか」ということです。またそれに関連して「画面をレンダリングする時に必要なstateが存在しないという状況を無くしたい」ということも感じていました。

何がいけなかったのか

上述した課題に直面した原因は「stateの依存関係およびそのstateを取得するSagaを飛ばすタイミングを考慮していなかったこと」です。この状況の例えとして、Twitterのようなフォローユーザー一覧画面やタイムライン画面を持つアプリを想定して考えてみます。

スクリーンショット 2019-12-22 17.46.32.png

上の図の中では、ホーム画面、フォローユーザー一覧画面、タイムライン画面の三つの画面があります。そして、それぞれの画面がそれぞれの画面の描写に必要なstateを持っています。ここでは、フォローユーザー一覧画面をレンダリングするタイミングでSagaを飛ばしてstateC(フォローユーザー情報)をFirestoreに問い合わせています。一方で、タイムライン画面はstateD(タイムライン情報)を持っているのですが、これはstateC(フォローユーザー情報)に依存しているstateになります。フォローしているユーザーの情報が無ければタイムラインを表示できないからです。ここにstateの依存関係があるのですが、これを考慮していなかったためフォローユーザー一覧画面を経由してからでないとタイムラインが正常にレンダリングできなかったり、タイムライン画面をレンダリングするタイミングでもう一度フォローユーザー情報を取得するSagaを飛ばさなければいけないという冗長な作りになっていました。

こういった問題が浮き彫りになったため、情報を整理するためPlantUMLのマインドマップを用いて考えてみます。

マインドマップを用いて整理する

前述した状況をマインドマップを用いて表現したのが以下の図です。

スクリーンショット 2019-12-22 18.15.11.png

先ほどの流れで、まずは画面ベースでstateを考えてみます。このマインドマップでは各画面が持つstateを末端に表しています。ここで、Sagaについて考える上での観点が二つあります。一つ目は、state Aがホーム画面とフォローユーザー一覧画面で重複していること、二つ目はタイムライン画面においてstate D(タイムライン情報)state C(フォローユーザ情報)に依存していることです

まずは一つ目について考えます。state Aに関して、それぞれの画面をレンダリングする時に別々のSagaによって取得することは冗長です。なので、state Aを取得するSagaは1回だけにして、認証後の初期外部リクエスト(initial saga)によって該当データを取得することにします。

次に二つ目についてです。state D(タイムライン情報)state C(フォローユーザ情報)に依存しています。つまり、state Cが無ければstate Dも表示させることができません。そこで、プロフィール画面もしくはタイムライン画面個別に紐づいたSagaではなく、一つのSagaとしてstate Cを取得することにします。こちらも認証後の初期外部リクエスト(initial saga)で飛ばすのが良さそうです。

スクリーンショット 2019-12-22 18.41.29.png

ここまでPlantUMLのマインドマップを用いてstateとSagaについて考えてみました。ここで、stateとSagaについてまとめてみます。下図のように、二つの画面で重複していたstate Aと、依存関係にあったstate Cはまとめて認証後の初期外部リクエスト(initial saga)によってデータを取得するようにしました。

スクリーンショット 2019-12-22 23.13.18.png

また、Storeに関しては下図のような構成になっています。最初は画面ベースでstateを考えていましたが、state Aが複数の画面で重複しているため冗長な構成を避けたいこと、開発規模が小さいことから最終的には画面ベースではなくドメインベースの構成になっています。これらに加えて、開発中のアプリのビューは完全に決定してるわけではないので、後々のビューの変更にも強いドメインベースを採用しています。画面ごとに分担作業するなどの場合は冗長でも画面ベースで考えるというのはありかもしれません。ちなみに、Redux公式ドキュメントはドメインベースでのStore設計を推奨しています。また、Store設計に関しては「TwitterやSlackのRedux Storeを覗く」の記事が勉強になりました。

スクリーンショット 2019-12-22 19.00.36.png

おわりに

今回、マインドマップを用いてSagaの呼び出し順序やStoreの構成を考えてみました。ReduxやRedux-Sagaを使用し複雑なビジネスロジックになる場合、整理のためにマインドマップを用いてみてはどうでしょうか。また、Storeの設計を考える上で「TwitterやSlackのRedux Storeを覗く」の記事はとても参考になると思いました。

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

React Hooks ✖️ Reduxを使ってみて 自分なりに詰まったところをまとめてみた

自分の作っているサービスで初めてReact Hooks, reduxを使用したので自分が詰まったところをまとめてみました!
:warning: この記事はReact Hooks, reduxを使う上で、自分が詰まったところについて書いています。
それぞれ個々の説明に関しては公式のドキュメントや別記事をお読みください:warning:

参考にさせていただいた記事

Redux Hooks によるラクラク dispatch & states
React Hooksでredux / react-reduxでやってたことを色々やってみる
?React 16.8: 正式版となったReact Hooksを今さら総ざらいする

この記事の対象読者

  • 私のようなReact超初心者
  • React Hooks, redux それぞれは何となくわかったけど一緒に使う方法がわからない方

私の開発環境

  • macOS Mojave 10.14.6
  • Node.js 12.13.0
  • React 16.12.0
  • create-react-appでアプリの雛形を作成

今回作るもの

  • ボタンを押したら足したり引いたりできものを作ります

スクリーンショット 2019-12-22 21.54.52.pngスクリーンショット 2019-12-22 21.54.42.png

actionを作る

src/actions/action.jsにactionを定義します。
今回は足したり引いたりするだけなのでこの二つでいいでしょう。

action.js
export const incrementAction = () => ({
    type: 'INCREMENT',
});

export const decrementAction = () => ({
    type: 'DECREMENT',
});

reducerを作る

src/reducers/counterReducer.jsにreducersを定義します。
今回は、combineReducersを使いrootReducerでそれぞれのreducerをまとめる形にしました。

rootReducer.js
const rootReducer = combineReducers({
    counterReducer,
});

export default rootReducer
counterReducer.js
const initialState = {
    counter: 0,
};

export default function counterReducer(state = initialState, action) {
    switch (action.type) {
        case "INCREMENT":
            return {counter: state.counter + 1}
        case "DECREMENT":
            return {counter: state.counter - 1}
    }
    return {counter: state.counter}
}

この時注意しないといけない点が、returnする際に連想配列の形にすることです!
私はstate + 1state.counter + 1としていたため、うまく受け渡しができていませんでした:sob:

storeを作る

react-create-appで雛形を作った場合src/index.jsがあるのでそこに書いていきましょう!
ReactDOM.renderしているファイルでcreateStoreにrootReducerを渡します。
作成したstoreをProviderに渡して下のコンポーネントでも使えるようにします。

index.js
const store = createStore(rootReducer);

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,

    document.getElementById('root')
);

呼び出す

index.jsで呼び出しているコンポーネントで上で定義したものを呼び出していきます。
react-create-appで雛形を作った場合src/App.jsです!

App.js
export default function App() {
    const dispatch = useDispatch() 
    const counter = useSelector(state => state.counterReducer.counter);// 使用するreducerを引数に渡す。
    return (
        <div className="App">
            <p> count: {counter} </p>
            <button onClick={() => dispatch(incrementAction())}>たす</button>
            <button onClick={() => dispatch(decrementAction())}>ひく</button>
        </div>
    );
}

この時注意しないといけない点が、useSelectorに引数を渡す際ちゃんとreducerを指定して渡すことです!
reducerをrootでまとめているのでuseSelector(state => state.counter)の様にしてしまうと当たり前ですがundefindが返ってきてしまいます。

以上で完了です、ページにアクセスしてみましょう。

まとめ

今回はじめてReact Hooksを使ってみたんですがすごく便利ですね!
まだ出たばかりで資料もすくなかったため、僕の様な超初心者でもわかるように僕が詰まったりしたところを丁寧に書いてみました。

Reactを書くのは初めてなので、この書き方はおかしい!こっちの方がいい!みたいなご指摘がありましたらぜひコメント等いただければと思います。

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

Webpackで出力したbundleのハッシュ値を抽出して使う

個人的に開発しているサイトではWordPressのPHP上にReactを載せており、そこではbundleのハッシュ値をPHP側で参照できるようにしています。extract-hash-webpack-pluginを使うと任意のテキスト形式でハッシュ値を含んだファイルを出力できます。今回はPHPから参照したいので.phpの形式で出力してみます。以下がwebpack.config.jsの例です。

var path = require('path');

var ExtractHashWebpackPlugin = require('extract-hash-webpack-plugin').default;
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const DIST_PATH = "theme/dist";

module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "index.[hash].js",
        path: path.resolve(__dirname, DIST_PATH)
    },
    /* ... */
    plugins: [
        new ExtractHashWebpackPlugin({
            dest: DIST_PATH,
            filename: 'version.php',
            fn: hash => `<? $JS_HASH = '${hash}'; ?>`
        }),
        /* ... */
    ]
};

プラグインのfnオプションでハッシュ値を使った出力を定義できます。今回は変数$JS_HASHにハッシュ値を保存するPHPのコードを出力するようにしています。

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

「zeit/swr」のpagination exampleでScroll Position Restoreを試す

こちらはNext.js Advent Calendar 2019の22日目の記事です。

はじめに

例えばYoutubeをスマホで見ていたときに、↓のように一度ページ遷移をしてからブラウザバックしたときにスクロール位置が一番上まで戻ってしまって経験はないでしょうか。

cap1.gif

これはSPAが抱える課題の1つで、解決するためには「Scroll Position Restore」と呼ばれる「スクロール位置の保持」とその「リストア(復元)」が必要です。

また、それ以外にも「もっと読む」などで画面内に非同期で要素が追加された場合はその要素も合わせて復元しないと正しいスクロール位置には戻れません。これには複雑なStateの管理も必要になってきます。

このように「Scroll Position Restore」を実現するにはいくつか超えなくてはいけない壁があります。

zeit/swr の登場

2019年10月、 Next.jsやNowの開発元であるZeitから「zeit/swr」というライブラリが発表されました。
そして、この「zeit/swr」の機能の1つにある「Pagination」が「Scroll Position Restore」を実現するための機能となっています!

swr(Stale-While-Revalidate) とは主にキャッシュ周りの用語なのでこのライブラリもキャッシュ周りのハンドリングも含むdata fetchのReact Hooksライブラリとなっています。1

簡単にやってくれることを書くと、「最初にキャッシュした古いデータを見せておいて、その裏で最新のデータをfetchしてキャッシュと差し替える」といった感じかなと思います。2

早速、Paginationを動かしてみる

Next.jsもそうですが、リポジトリ内にexamplesというサンプル集があり、そこにPaginationのexampleもあったので今回はそれを動かしてみます。

swr/examples/pagination at master · zeit/swr

READMEに書いてあるとおり、Setupを終わらせます(このサンプル自体もNext.jsが使われていました)。

$ curl https://codeload.github.com/zeit/swr/tar.gz/master | tar -xz --strip=2 swr-master/examples/pagination
$ cd pagination
$ yarn
$ yarn dev

起動したサーバーにアクセスすると以下のページが出てきます。「load more」で読み込ん要素が、ページ遷移をしてブラウザバックをしても表示されていることがわかると思います!また、スクロール位置も保持されていそうですね。3

cap2.gif

実装はこんな感じです。

import fetch from '../libs/fetch'
import Link from 'next/link'

import useSWR, { useSWRPages } from 'swr'

export default () => {
  const {
    pages,
    isLoadingMore,
    isReachingEnd,
    loadMore
  } = useSWRPages(
    // page key
    'demo-page',

    // page component
    ({ offset, withSWR }) => {
      const { data: projects } = withSWR(
        // use the wrapper to wrap the *pagination API SWR*
        useSWR('/api/projects?offset=' + (offset || 0), fetch)
      )
      // you can still use other SWRs outside

      if (!projects) {
        return <p>loading</p>
      }

      return projects.map(project => 
        <p key={project.id}>{project.name}</p>
      )
    },

    // one page's SWR => offset of next page
    ({ data: projects }) => {
      return projects && projects.length
        ? projects[projects.length - 1].id + 1
        : null
    },

    // deps of the page component
    []
  )

  return <div>
    <h1>Pagination (offset from data)</h1>
    {pages}
    <button onClick={loadMore} disabled={isReachingEnd || isLoadingMore}>
      {isLoadingMore ? '. . .' : isReachingEnd ? 'no more data' : 'load more'}
    </button>
    <hr />
    <Link href="/page-index"><a>page index based pagination </a></Link><br/>
    <Link href="/about"><a>go to another page </a></Link>
  </div>
}

useSWRPages()の引数として、キャッシュキーとしても使われるページ名、実際にfetchしたデータを使って生成するComponent、次ページのoffsetなどを設定しています。

ここの説明はコード側にシーケンス図があったのでそちらを見るとより分かりやすいかなと思います。

少し触ってみたりissueなども見たところ、まだスクロール位置の復元がやや不完全であったり、キャッシュのストアを触ろうとしたときに不自由さはあるものの、シンプルなアプリケーションであれば比較的簡単に利用できそうな印象でした。

さいごに

zeit/swrはまだまだ発表されたばかりで利用実績も多くありませんが、頻繁に開発されており、何より開発元がzeitであるため今後ますますNext.jsとうまく協調して進化していくはずです。Nextをお使いの方はぜひその動向に注目してみてください!

以上、22日目の記事でした!


  1. このライブラリきっかけでswr自体を勉強し直しました。。こちらの記事が分かりやすかったです。 

  2. 詳しくはこちらの記事こちらの記事をおすすめします! 

  3. 分かりやすくするために1度に読む込むデータ数を3件から10件に変更しています。 

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

React-hook-formで簡単にバリデーションフォーム作る

Reactでフォームの実装をしたことのある、もしくはこれから実装する皆さん。
React-hook-formをご存知ですか?
フォームの実装がとても楽になる便利なライブラリです。

この記事ではReact-hook-formの基本的な簡単な使い方と
実装例をソースコードとともに解説しています。

React-hook-formとは?

高性能で柔軟かつ拡張可能な使いやすいフォームバリデーションライブラリ。(引用)

従来のformライブラリに比べて、以下の特徴があります。1
・記述量が少ない
・レンダリングが少ない
・マウントが高速
・hooksで記述がシンプル

そして何より。。
バリデーションの実装が楽になります。

使い方

それではReact-hook-formの簡単な使い方を見てみましょう。
以下は公式デモのソースコードです。

import React from 'react'
import useForm from 'react-hook-form'

export default function App() {
  const { register, handleSubmit, watch, errors } = useForm()
  const onSubmit = data => { console.log(data) }

  console.log(watch('example')) 

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="example" defaultValue="test" ref={register} />
      <input name="exampleRequired" ref={register({ required: true })} />
      {errors.exampleRequired && <span>This field is required</span>}
      <input type="submit" />
    </form>
  )
}

React-hook-formでは必要なメソッドやオブジェクトをuseFormから受け取って使用します。
以下の手順で実装します。

1. フィールドを登録する。
非制御コンポーネント (Uncontrolled Components) をフックに登録(register) し、フォームフィールドの値を検証と収集できるようにする(引用)

登録したいフィールドにname="uniqueName"ref={register}を加えます。

<input name="example" defaultValue="test" ref={register} />

2. バリデーションとエラー文言を設定する。
registerメソッドにバリデーションを渡し、
バリデーション時にエラーが発生するとerrorsオブジェクトに
先ほど加えたnameをkeyとしたエラーメッセージを割り当てられます。2

<input name="exampleRequired" ref={register({ required: true })} />
{errors.exampleRequired && <span>This field is required</span>

上記の例のrequiredはバリデーションの際に必須入力を求めます。
バリデーションは上記の他に最大文字数、最小文字数なども設定でき、
さらに正規表現やバリデーション関数を渡すこともできます!

実装例

では実際に以下のようなフォームを実装してみます。

form.gif

・各フォームごとに入力後バリデーションする
・バリデーションエラーの場合はエラーメッセージを表示する
・全てのフォームが正しく入力されている場合のみsubmitボタンを押せるようにする

hooksのみで実装してみると。。。(長いので読む必要なし)

import * as React from 'react';

interface FormData {
  title: string;
  author: string;
}

interface FormValidationResults {
  title: boolean;
  author: boolean;
}

interface ErrorMessage {
  title: string;
  author: string;
}

const SomeForms: React.FC = () => {
  const [values, setValues] = React.useState<FormData>({
    title: '',
    author: ''
  });

  const [validationResults, setValidationResults] = React.useState<
    FormValidationResults
  >({
    title: false,
    author: false
  });

  const [errorMessages, setErrorMessages] = React.useState<ErrorMessage>({
    title: '',
    author: ''
  });

  const handleChange = (name: keyof FormData) => (
    event: React.ChangeEvent<HTMLTextAreaElement>
  ) => {
    const newValues = { ...values, [name]: event.target.value };
    setValues(newValues);
    validate(newValues, name);
  };

  const validate = (values: FormData, name: keyof FormValidationResults) => {
    switch (name) {
      case 'title':
        titleValidation(values[name]);
        break;
      case 'author':
        authorValidation(values[name]);
        break;
    }
  };

  const titleValidation = (value: string): void => {
    if (value.length < 1 || value.length > 20) {
      setValidationResults({ ...validationResults, title: false });
      setErrorMessages({
        ...errorMessages,
        title: 'タイトル名は1文字以上、20文字以下でなければなりません。'
      });
    } else {
      setValidationResults({ ...validationResults, title: true });
      setErrorMessages({ ...errorMessages, title: '' });
    }
  };

  const authorValidation = (value: string): void => {
    if (value.length < 1 || value.length > 20) {
      setValidationResults({ ...validationResults, author: false });
      setErrorMessages({
        ...errorMessages,
        author: '作者名は1文字以上、20文字以下でなければなりません。'
      });
    } else {
      setValidationResults({ ...validationResults, author: true });
      setErrorMessages({ ...errorMessages, author: '' });
    }
  };

  return (
    <div>
      <h2>タイトル名</h2>
      <textarea
        name='title'
        value={values.title}
        onChange={handleChange('title')}
      />
      {errorMessages.title && <span>{errorMessages.title}</span>}
      <h2>作者名</h2>
      <textarea
        name='author'
        value={values.author}
        onChange={handleChange('author')}
      />
      {errorMessages.author && <span>{errorMessages.author}</span>}
      <button
        disabled={
          validationResults.title && validationResults.author ? false : true
        }
      >
        送信する
      </button>
    </div>
  );
};

export default SomeForms;


。。。長い。。改行があるとはいえ100行強あります。

useStateで以下を管理しています。。。長い。
・フィールドの値
・エラーメッセージ
・バリデーションがvalidかどうか

React-hook-formで実装

import * as React from 'react';
import useForm from 'react-hook-form';

interface FormData {
  title: string;
  author: string;
}

const OtherForms: React.FC<{}> = () => {
  const { register, handleSubmit, errors, formState } = useForm<FormData>({
    mode: 'onChange'
  });

  const onSubmit = (data: FormData): void => console.log(data);

  return (
    <div>
      <h2>タイトル名</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <textarea
          name='title'
          ref={register({ required: true, maxLength: 20 })}
        />
        {errors.title && '作者名は1文字以上、20文字以下でなければなりません。'}
        <h2>作者名</h2>
        <textarea
          name='author'
          ref={register({ required: true, maxLength: 20 })}
        />
        {errors.author && '作者名は1文字以上、20文字以下でなければなりません。'}
        <button disabled={!formState.isValid}>送信する</button>
      </form>
    </div>
  );
};

export default OtherForms;

なんと37行!(しかもフォームの結果をconsole.logで出力している)
デモの実装例にはなかった2つのメソッドorオブジェクトを追加して実装しています。

・formState
フォームの状態に関する情報が含まれているオブジェクト。
formState.isValidはフィールドにエラーがない状態かどうかをbooleanで表しています。

・handleSubmit
バリデーションに成功するとフォームのデータを渡してくれるメソッド。

補足

バリデーションのタイミングはオプションで指定することができます。
今回は各フォームの入力ごとにバリデーションしたいので、
useFormに{mode: 'onChange'}を渡しています。
パフォーマンスの観点ではレンダリングが増えるので推奨されてはいないようです。


  1. 詳しくはhttps://react-hook-form.com/jp  

  2. バリデーションのみの登録とバリデーションとエラーメッセージをセットで登録することもできます。https://react-hook-form.com/jp/api/#register 

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

【python】flaskを使ってローカルサーバーを動かしてみた

はじめに

Progateでしかpythonを触ったことのない筆者が、pythonを使ってローカルサーバーを立てて、任意のアドレス対して動作をさせるまでの備忘録。

開発環境

Windows 10 64bit
Python 3.7.3
pip 19.3.1
flask 1.1.1

環境構築

pythonをインストールしていない人はまずはインストールをします!
詳しい方法はこちらから

インストールした記憶がないけどあるかも...と思う方はコマンドプロンプトを開いて、python -Vで確認してみましょう!

$ python --version
Python 3.7.3

$ python -V
Python 3.7.3

インストール済みであれば以上のように表示されます!

インストールが完了したら、Pythonのパッケージ管理ツールであるpipをインストールしていきます!
インストール済みか確認したい方は

python -m pip -V

で確認できます!
インストールされていなかった人は詳しいインストール方法はこちら

以下のコマンドを叩いてインストールしていきます!

py -m pip install pypdf2

インストールができたら、今回使うpythonの軽量フレームワークであるflaskのインストールを行っていきます!
flask使ったことのある人はこの記事を読まないであろうと想定して、インストールの仕方から!

pip install Flask

確認したいときは

$ python
>> import flask
>> flask.__version__

(\$マークはコピペしないでね。てかこの\$って何なんでしょう?)
ここまで出来たらとりあえずの環境構築は終了です。
おつかれさまでした。

さっそく実装

今回は前々回の記事で作成したreact-appでボタンをクリックしたときに指定したデータを返すような実装をしていきます。
前々回の記事→【React】はじめてReact触ってみた!~create-react-app編~
よかったらいいねしてください()

それはさておき、コードを書いていきます。
前回作ったもののフォルダとの相関関係はこんな感じです!

Sample
  └ resource
  └ react-test
      └ node_modules
      └ public
      └ src
          └ App.js
      ・
      ・
      ・
  └ flask-test
      └ run.py

まずは、いろいろなサイトを参考にpython側のコードを作成しました。

run.py
import json

# Flask などの必要なライブラリをインポートする
from flask import Flask
from flask_cors import CORS
from flask import request, make_response, jsonify

# 自身の名称を app という名前でインスタンス化する
app = Flask(__name__)
# CORS (Cross-Origin Resource Sharing)
CORS(app)

# ここからウェブアプリケーション用のルーティングを記述
# index にアクセスしたときの処理
@app.route('/', methods=['GET'])
def show_user():
    response = {'user': {'name': 'index', 'age': 'hoge', 'job': 'web'}}
    return make_response(jsonify(response))

# /user にアクセスしたときの処理
@app.route('/user', methods=['GET'])
def show_user2():
    response = {'user': {'name': 'user', 'age': 'fuga', 'job': 'free'}}
    return make_response(jsonify(response))


if __name__ == '__main__':
    app.run()

↓ 参考にした記事の数々はこちら ↓
ウェブアプリケーションフレームワーク Flask を使ってみる
PythonのFlaskを使用してWebアプリ作成してみよう(1)こんにちは世界
Flaskのユーザーガイド

続いて、以前create-react-appを使って作成したアプリを改造していきます!
react-test > App.js を開き、以下のようにした。

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Axios from 'axios';

function App() {
  function click(){
    Axios.get('http://127.0.0.1:5000/').then(function(res){
      console.log(res);
      alert(res.data.user.age);
    }).catch(function(e){
      alert(e);
    })
  }

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React

          <br/>

        </a>
        <button onClick={() => {click()}}>click</button>

      </header>
    </div>
  );
}

export default App;

やっとこさ実行

ここまで来たらあとはコマンドプロンプトで動かすだけ!
早速コマンドプロンプトを開いて、

cd C:\Users\{user}\Documents\Sample\flask-test

で、指定ディレクトリに移動します。
ここで、

python run.py

を起動!

C:\Users\{user}\Documents\Sample\flask-test>python run.py
 * Serving Flask app "run" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

となれば成功!!

続いて、コマンドプロンプトを別ウィンドウでも開き、

cd C:\Users\{user}\Documents\Sample\react-test

でcreate-react-appのディレクトリに移動します。
そして前々回と同様に

C:\Users\{user}\Documents\Sample\react-test>npm start

で起動!これでうご...かない!?

Module not found: Can't resolve 'axios' ・・・

axiosのモジュールがないとか...
たしかに、インポートするaxiosのモジュールインストールした記憶ない...ってことで早速以下のコマンドを叩いてインストールします!

C:\Users\{user}\Documents\Sample\react-test>npm install axios

こんどこそいけるはず!ってことでもう一度

C:\Users\{user}\Documents\Sample\react-test>npm start

...

C:\Users\{user}\Documents\Sample\react-test>npm start
npm WARN lifecycle The node binary used for scripts is C:\Program Files (x86)\Nodist\bin\node.exe but npm is using C:\Program Files (x86)\Nodist\v-x64\10.15.3\node.exe itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.

> test@0.1.0 start C:\Users\sleep\Documents\React-Tutorial\test
> react-scripts start
Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.

Starting the development server...
Compiled successfully!

You can now view test in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.10.101:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

きた~~~~~~~~!

これでclickボタンを押すと...

hogeが返ってきた!!成功!!

次にclickしたときに飛ぶアドレスを変える!

App.js
function click(){
    Axios.get('http://127.0.0.1:5000/').then(function(res){
      console.log(res);
      alert(res.data.user.age);
    }).catch(function(e){
      alert(e);
    })
  }

これを

App.js
function click(){
    Axios.get('http://127.0.0.1:5000/user').then(function(res){
      console.log(res);
      alert(res.data.user.age);
    }).catch(function(e){
      alert(e);
    })
  }

こうして(アドレスの末尾にuserを追加)クリックボタンを押すと、

fugaに変わった!!!とりあえず想定通り動かせました、よかった~。
ちょっと躓くところもありましたが、環境構築さえできてしまえば何とかなりそうです!

おわりに

最後まで見てくださった方がいましたら、本当にありがとうございます。
初歩の初歩らしいところからやってみましたが、動かせるだけでめちゃくちゃうれしいものですね。
もっと改良してまたまとめていきたいと思います!

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

Reactで投稿のIDを取得する

はじめに

いなたつアドカレの二十二日目の記事です。

もうすぐ終わりですね。。。

今回はReactでURLから投稿などのIDを取得する方法の備忘録です。

じっそー

App.js
import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    <Router>

        <Route exact path='/:id' component={Hoge} />
    </Router>

  );
}


export default App;

/:idのURLでHogeのコンポーネントというRouteの定義ができました。

const id = props.match.params.id

Hogeのコンポーネントで上のようにすることで、idを使用できます

このidを使ってAPiを叩くなりしましょ

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

オレオレredux 型まわりベストプラクティスを書いてみたい

Redux+TSを書くときに個人的によくやるパターンを書いてみます

頑張って前日に書きました:sob:

はじめに

Reduxの流れ自体は単方向でグルグル回る感じになっています 画像引用元

redux_store_states.png

ReduxのTSに関しては単方向ではなく、Actionの型情報をComponentとReducerに渡す必要があり、ちょっと都合が違います。

スクリーンショット 2019-12-22 16.53.07.png

これをある程度楽する方法を書いていきます

書くもの概要

ユーザーの一覧の表示・追加・削除できる画面

雰囲気はこんなので
スクリーンショット 2019-12-22 17.23.08.png

以下ソース

model
モデル
// ユーザーのモデル
interface IUser {
  id: number
  name: string
  email: string
  age: number
}

Action
action
// アクションのTypeのあれ
export enum UserActions {
  ADD = 'user/ADD',
  REMOVE = 'user/REMOVE',
}

// アクションの一覧
export const userActions = {
  add: (payload: {user: User}) => ({
    type: UserActions.ADD as const,
    payload,
  }),
  remove: (payload: {userId: User['id']}) => ({
    type: UserActions.REMOVE as const,
    payload,
  }),
}

// 以下UtilityType

// MapをUnionとしてとりだす
type MapToUnion<T> = T[keyof T];

interface Action {
  type: string;
}

interface Actions {
  [key: string]: (...args: any) => Action;
}

// userActionsをUnionとして取り出すよう
type ActionToUnion<T extends Actions> = Extract<
  ReturnType<MapToUnion<T>>,
  Action
  >;

// userActionsを {[k: アクション名]: アクションの戻り値} にするやつ
type ActionToMap<T extends Actions> = { [P in keyof T]: ReturnType<T[P]> };

export type UserReducerAction = ActionToMap<typeof userActions>;
export type UserReducerActions = ActionToUnion<
  typeof userActions
  >;

Component
component
interface mapStateToProps {
  users: User[]
}

interface dispatchProps {
  // ここで作った型を指定
  add: (payload: UserReducerAction['add']['payload']) => void
  remove: (payload: UserReducerAction['remove']['payload']) => void
}

type Props = mapStateToProps & dispatchProps

// コンポーネントに関しては特に書かない
const _UserPage: FC<Props> = ({users, addUser, removeUser}) => (
  <UserList
    // ユーザー一覧を受け取って表示
    users={users}
    // 作成時のイベント
    onCreate={(user: User) => addUser({user})}
    // 削除時のイベント
    onRemove={(user: User) => {removeUser({userId: user.id})}}
  />
)

// ストアのイメージ
interface Store {
  user: {
    users: User[]
  }
}

const mapStateToProps =(state: Store): mapStateToProps => ({
  users: state.user.users,
})


const mapDispatchToProps = (dispatch: Dispatch<UserReducerActions>): dispatchProps => ({
  addUser: payload => dispatch(userActions.addUser(payload)),
  removeUser: payload => dispatch(userActions.removeUser(payload)),
})
})

export const UserPage = connect(
  mapStateToProps,
  mapDispatchToProps
)(_UserPage);
Reducer
reducer
interface UserState {
  users: User[]
}

const initialState: UserState = {
  users: [],
}

// UserReducerActions これでActionの型を指定する
export const userReducer = (state = initialState, action: UserReducerActions): UserState => {
  switch (action.type) {
    case UserActions.ADD: {
      return {
        ...state,
        users: state.users.concat(action.payload.user)
      }
    }
    case UserActions.REMOVE: {
      const index = state.users.findIndex(v => v.id === action.payload.userId)
      if (index === -1) {
        return state
      }

      return {
        ...state,
        users: state.users.splice(index, 1);
      }
    }
    default:
      return state
  }
}


解説

肝は
- アクションをオブジェクトにまとめる https://qiita.com/eretica/private/81e8567756a0f98a3393#action
- ActionToMapActionToUnion を利用して扱いやすい型にする
- 以降はComponentやReducerで適宜利用する
- redux-thunkredux-saga などでもconnectの部分以外は同じようなパターンで利用できる。使う際は dispatch(userActions.add()) で利用可能

UtilyTypeで使いやすい形にすることで、楽できるよ!ってのが伝わると嬉しい

interface dispatchProps {
  addUser: (payload: UserReducerAction['add']['payload']) => void
  removeUser: (payload: UserReducerAction['remove']['payload']) => void
}

よりみち

payload: Action['hoge']['payload'] が冗長であれば

interface dispatchProps {
  addUser: (payload: UserReducerAction['add']['payload']) => void
  removeUser: (payload: UserReducerAction['remove']['payload']) => void
}

こういうUtilityTypeをつくればもう少し楽できます

type ArgumentTypes<F extends Function> = F extends (args: infer A) => any ? (arg: A) => void : never;
type ActionToArguments<T extends Actions> = { [P in keyof T]: ArgumentTypes<T[P]>};
export type UserReducerActionArgument = ActionToArguments<typeof userActions>;

interface dispatchProps {
  addUser: UserReducerActionArgument['add']
  removeUser: UserReducerActionArgument['remove']
}

まとめ

  • actionを使いやすい型にしておくことで、楽をしたい

よりみち2

こちらの記事(なぜカスタムフックを作るのか)のように、custom hooks が状態やアクションを提供する形になればコネクトの部分の型定義は不要になるので、さらに楽できるのではないかと思っています。

個人的にはこのようなcustom hooksはcontainerコンポーネントに相当するものだけが利用(use***)し、従来通りのコンポーネント設計に留めて置くのが吉なのかなとは思っています

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

Reactコンポーネントライブラリ解発用のボイラープレートを作った

react-component-library-boilerplate

このテンプレートには以下のものが含まれています

  • react本体
  • rollup
  • typescript
  • example demo pages build by react-styleguidist
  • CSS Modules
    • your any css files budled to build file
  • eslint
  • prettier
  • husky
  • jest and @testing-library/react
  • build scripts
  • include your license and dependencies license to dist directory
  • github actions
    • プッシュされる度にgithub actions上でビルドタスクを実行します

usage

1. install

$ git clone https://github.com/aclearworld/react-component-library-boilerplate.git <your-library-name>
$ cd <your-library-name>
$ rm .git/ -rf
$ npm ci

2. edit LICENSE, package.json, README.md

以下のものをあなたが望むように編集して下さい

  • edit LICENSE
  • edit package.json fileds
    • name
    • description
    • private
    • repository
    • keywords
    • author
    • license
    • bugs
    • homepage
  • edit README.md

3. start devlopment

$ npm run start

4. enjoy your hacking!!

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

Reactコンポーネントライブラリ開発用のボイラープレートを作った

react-component-library-boilerplate

このテンプレートには以下のものが含まれています

  • react本体
  • rollup
  • typescript
  • example demo pages build by react-styleguidist
  • CSS Modules
    • your any css files budled to build file
  • eslint
  • prettier
  • husky
  • jest and @testing-library/react
  • build scripts
  • include your license and dependencies license to dist directory
  • github actions
    • プッシュされる度にgithub actions上でビルドタスクを実行します

usage

1. install

$ git clone https://github.com/aclearworld/react-component-library-boilerplate.git <your-library-name>
$ cd <your-library-name>
$ rm .git/ -rf
$ npm ci

2. edit LICENSE, package.json, README.md

以下のものをあなたが望むように編集して下さい

  • edit LICENSE
  • edit package.json fileds
    • name
    • description
    • private
    • repository
    • keywords
    • author
    • license
    • bugs
    • homepage
  • edit README.md

3. start devlopment

$ npm run start

4. enjoy your hacking!!

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

【Atomic Design入門】Todoアプリを作りながらAtomic Designを理解する

はじめに

こんにちは、masa08です。普段はエンジニア限定シェアハウス「GAOGAO-TOKYO」で過ごしつつ、スタートアップで業務委託エンジニアをしつつ、大学生してます(早く卒業したい)。

今年は多くの新しい技術に触れた年でした。特にフロントエンドに関しては知見を得たことが多かったです。去年の今ごろはどうにかしてJavaScriptがかけるくらいの実力だったのですが、今年一年通じて、TypeScriptやReact、Atomic Designと様々な技術や考え方に出会い、実務を通じて自分の理解を深めることができました。
今回の記事ではアウトプットも兼ねて、Atomic Designに沿ったディレクトリ構成でReact×TypeScriptを用いてTodoアプリを作っていきたいと思います。自分自身わからないことも多いので、何かあればご指摘いただけると幸いです。

スクリーンショット 2019-12-22 1.23.35.png

対象者

  • JavaScriptの基本的な知識を持っている
  • Reactの基本的な知識を持っている
  • Atomicデザインに触れたいと考えている
  • TypeScriptに触れたいと考えている

この記事を通じて得られること

  • Reactでアプリケーションを作る方法
  • TypeScriptでコードを書く経験
  • Atomic Designに関する知識

Atomic Designとは

スクリーンショット 2019-12-21 20.37.55.png
画像引用元サイト: Atomic Design Methodology

Atomic Designとは、Webサイト上に存在するものすべてをコンポーネントとみなし、粒度ごとにコンポーネントの種類を分けて管理していく、コンポーネントベースの開発手法のことです。小さいコンポーネントを組み合わせて、より大きなコンポーネントを作っていきます。以下の5つの粒度に分けて、コンポーネントを大別します。この際、上位層は下位層に依存します。

  • Atoms(原子)
  • Molecules(分子)
  • Organisms(有機体)
  • Templates(テンプレート)
  • Pages(ページ)

Atoms

Atoms層は、それ以上UIとして分解できない最小要素のことです。テキストやボタンが代表的な要素です。

Molecules

Molecules層は、2つ以上ののAtomsが組み合わされて作られたコンポーネントです。検索フォームなどが代表的な要素です。

Organisms

Organisms層はMoleculesやAtomsで構成されるコンポーネント群です。Headerなどが代表的な要素です。

Templates/Pages

Template層はページの雛形、Pages層は実際のページ、すなわちTemplate層にコンテンツを流し込んだものになります。ここにきてユーザーから見えるページが完成します。organisms層を中心として、Templates/Pagesを組み立てていきます。

Atomic Designを採用するメリット

従来の開発手法と比べて、コンポーネントベースでのUI開発には以下の利点があります。

  • アプリケーションのメンテナンスしやすくなる
  • 解決する問題が小さくなる(責務の分離)
  • チーム内で共通認識を持つことができる
  • コンポーネントの再利用、平行開発等にによって、開発のスピードが速くなる

実際にコードを書いて、Atomic Designの雰囲気を感じてみましょう。

参考サイト
Atomic Design について調べて見た
Atomic Designを分かったつもりになる
React, Components, and Design

Todoアプリを作る

Atomic Designがどのようなものか、atomsとmolecules、organisms、pagesを作る過程を追いながら確認していきます。

環境を整える

まず最初にnodeをインストールしましょう。以下のサイトからダウンロードします。
https://nodejs.org/ja/download/
ダウンロードしたファイルを開いて、nodeをインストールします。インストールが終わったら、ターミナルで以下のコマンドを打って、nodeがインストールされているのかを確認しましょう。またnodeをインストールするとnpmを一緒にインストールされるので、そちらも確認しましょう。

$ node -v # vxx.xx.xのような形で表示されれば正解
$ npm -v # vx.x.xのような形で表示されれば正解

プロジェクトを作成する

プロジェクトを作成しましょう。ターミナルを開き、以下のコマンドを実行しましょう。

$ cd path/to/your/directory
$ npx create-react-app sample-atomic --template typescript

sample-atomicディレクトリが作成されていることを確認したら、以下のコマンドを実行して、アプリケーションを立ち上げましょう。

$ cd sample-atomic
$ npm start

以下の画面が表示されれば成功です!
スクリーンショット 2019-12-21 20.30.53.png

Atomic Designを導入する

必要なフォルダを作成し、routingのためのライブラリをインストールします。

$ mkdir src/components
$ mkdir src/components/atoms
$ mkdir src/components/molecules
$ mkdir src/components/organisms
$ mkdir src/components/pages
$ touch src/components/pages/Home.tsx
$ npm install @types/react-router-dom

App.tsxを以下のように編集します。

App.tsx
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import './App.css';
import Home from './components/pages/Home';

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <Route exact path='/' component={Home}></Route>
    </BrowserRouter>
  );
}

export default App;

material-uiをインストールしてからHome.tsxを編集します。

$ npm i @material-ui/core
Home.tsx
import React, { useState } from "react";
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'

const Home: React.FC = () => {
  const [todos, setTodos] = useState<string[]>(["test1", "test2", "test3"]);
  const [value, setvalue] = useState<string>("");

  const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    const value = e.target.value;
    setvalue(value);
  };

  const onSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    setTodos([...todos.concat(value)]);
  };

  return (
    <div>
      <header>
        <h1>This is Header</h1>
      </header>

      <List>
        {todos.map(todo => (
          <ListItem key={todo}>{todo}</ListItem>
        ))}
      </List>

      <form onSubmit={onSubmit}>
        <input type='text' onChange={onChange} />
        <input type='submit' value='submit' />
      </form>
    </div>
  );
};

export default Home;

以下のような画面が出てくれば正解です。

スクリーンショット 2019-12-22 8.59.58.png

このアプリケーション内にある要素をAtomic Designに沿って分解していきます。上記をorganismsの粒度のコンポーネントに分けると、

  • Header
  • List
  • Form

となり、その中でさらにmoleculesとatomsに分解していきます。結果的に、src/componentsは以下のようなディレクトリ構成になりました(templateは今回作っていません)。

├── components
│   ├── atoms
│   │   └── FormInput.tsx
│   ├── molecules
│   │   └── Form.tsx
│   ├── organisms
│   │   ├── FormWrapper.tsx
│   │   ├── Header.tsx
│   │   └── TodoList.tsx
└── └── pages
        └── Home.tsx
Home.tsx
import React, { useEffect, useState } from "react";
import Header from '../organisms/Header';
import TodoList from "../organisms/TodoList";
import FormWrapper from "../organisms/FormWrapper";

const Home: React.FC = () => {
  const [todos, setTodos] = useState<string[]>(["test1", "test2", "test3"]);
  const [value, setvalue] = useState<string>("");

  const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    const value = e.target.value;
    setvalue(value);
  };

  const onSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    setTodos([...todos.concat(value)]);
  };

  return (
    <div>
      <Header text='This is Header' />

      <TodoList todos={todos} />

      <FormWrapper onChange={onChange} onSubmit={onSubmit} />
    </div>
  );
};

export default Home;
organisms/Header.tsx
import React from "react";

interface IProps {
  text: string;
}

const Header: React.FC<IProps> = ({text}) => {
  return (
    <header>
      <h1>{text}</h1>
    </header>
  );
};

export default Header;
organisms/TodoList.tsx
import React from "react";
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'

interface IProps {
  todos: string[];
}

const TodoList: React.FC<IProps> = ({todos}) => {
  return (
    <List>
      {todos.map(todo => (
        <ListItem key={todo}>{todo}</ListItem>
      ))}
    </List>
  )
}

export default TodoList;

organisms/FormWrapper.tsx
import React from "react";
import Form from "../molecules/Form"
import FormInput from "../atoms/FormInput"

interface IProps {
  onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const FormWrapper: React.FC<IProps> = ({onSubmit, onChange}) => {
  return (
    <Form onSubmit={onSubmit}>
      <FormInput onChange={onChange} />
      <input type='submit' value='submit' />
    </Form>
  );
};

export default FormWrapper;
molecules/Form.tsx
import React from "react";

interface IProps {
  onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}

const Form: React.FC<IProps> = ({ children, onSubmit }) => {
  return <form onSubmit={onSubmit}>{children}</form>;
};

export default Form;
atoms/FormInput.tsx
import React from "react";

interface IProps {
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const FormInput: React.FC<IProps> = ({ onChange }) => {
  return <input type='text' onChange={onChange} />;
};

export default FormInput;

コンポーネント分解し終えた後に、Todoアプリが通常通り動けば成功です!

終わりに

今回のように非常に小さなアプリケーションでは、Atomic Designを採用するメリットは見えづらいですが、実際に実務で使う場合は先述のメリットを享受することができます。今回の記事で、Atomic Designという考え方と、大まかな雰囲気を知っていただければ幸いです。

Atomic DesignはReactと相性が良く、今後も使われていくデザインの概念だと思います。ReactやVueなどのコンポーネント指向のライブラリを使う場合は、是非一緒に採用してみてください。

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

intersection observerを使用したInfinite Scrollの実装

この記事は CyberAgent 20 新卒 Advent Calendar 2019 の 22 日目です!
今回はタイトルにも書いてありますがintersection observerを使用してInfinite Scrollを実装したいと思います。

Intersection Observerとは

Intersection Observer API (交差監視 API) は、ターゲットとなる要素が、祖先要素もしくは文書の最上位のビューポートと交差する変更を非同期的に監視する方法を提供します。
https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API

useObserver

  • intersection observerのcustom hook
import React, { useEffect, useState, RefObject } from 'react';
function useObserver(
  ref: RefObject<HTMLDivElement>,
  ops?: IntersectionObserverInit
) {
  const [intersect, setIntersect] = useState<boolean>(false);
  useEffect(() => {
    function callback(entries: IntersectionObserverEntry[]) {
      entries.forEach(entry => {
        setIntersect(entry.isIntersecting);
      });
    }
    const observer = new IntersectionObserver(callback, ops);
    if (ref.current === null) {
      return;
    }
    observer.observe(ref.current);
    return () => {
      if (ref.current !== null) {
        observer.unobserve(ref.current);
      }
    };
  });
  return intersect;
}

使い方

export const Index: React.FC = () => {
  const target = React.useRef<HTMLElement>(null);
  const intersect = useObserver(target);

  return (
    <React.Fragment>
      <div style={{ position: 'fixed', top: 0, left: '50%' }}>
        {intersect ? 'visible' : 'not visible'}
      </div>
      <div style={{ height: 1000 }}></div>
      <div ref={target}></div>
    </React.Fragment>
  );
};
  • まず、ObserveしたいElementをuseObserverに渡すために、refを作成します。
    つぎに、ObserveしたいElementにpropsとしてrefを渡せば完了です。
    今回の例では、最初はrefを渡したDiv Elementが画面に表示されていないため、画面上部中央には、not visibleと表示されます。
    画面をスクロールして、refを渡したDiv Elementが画面に表示されると、visibleと表示されます。

infinite scroll

今回実装したもの

20191013001653.gif

ソースコード

import React, { useEffect, useState, RefObject } from 'react';
import axios from 'axios';
import './App.css';

interface Item {
  id: number;
  image: string;
}

const fetchItems = async (): Promise<Item[]> => {
  const { data } = await axios.get('http://localhost:3000/item');
  return data;
};

function useObserver(
  ref: RefObject<HTMLElement>,
  ops?: IntersectionObserverInit
) {
  const [intersect, setIntersect] = useState<boolean>(false);
  useEffect(() => {
    function callback(entries: IntersectionObserverEntry[]) {
      entries.forEach(entry => {
        setIntersect(entry.isIntersecting);
      });
    }
    const observer = new IntersectionObserver(callback, ops);
    if (ref.current === null) {
      return;
    }
    observer.observe(ref.current);
    return () => {
      if (ref.current !== null) {
        observer.unobserve(ref.current);
      }
    };
  });

  return intersect;
}

export const App: React.FC = () => {
  const [items, setItems] = useState<Item[]>([]);
  const target = React.useRef<HTMLDivElement>(null);
  const intersect = useObserver(target);

  useEffect(() => {
    async function fetch() {
      const data = await fetchItems();
      setItems(data);
    }
    fetch();
  }, []);

  useEffect(() => {
    async function fetch() {
      if (intersect) {
        const data = await fetchItems();
        setItems(prevItems => [...prevItems, ...data]);
      }
    }
    fetch();
  }, [intersect]);

  return (
    <React.Fragment>
      <div className="container">
        {items &&
          items.map((item) => {
            return (
              <React.Fragment key={item.id}>
                <div className="card">
                  <div className="card__item">
                    <img src={item.image}></img>
                  </div>
                </div>
              </React.Fragment>
            );
          })}
      </div>
      <div ref={target}>loading</div>
    </React.Fragment>
  );
};

export default App

おわりに

今回はintersection observerを使用してInfinite Scrollの実装を行いました.
intersection observerはその他にも,オフスクリーン時の画像の遅延読み込みやコンポーネントの遅延読み込みなどの使用法が考えられます.
要素をobserveして何かするという処理を書くことは結構あると思うので,積極的に使用していきましょう

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

React Nativeの公式チュートリアルをReact Hooks + TypeScriptで書き換える

前書き

個人的な学習のためReact Nativeの公式チュートリアルをやります。ただ、チュートリアルはReact Class APIを利用しているため、今回はそれをReact Hooks + TypeScriptに置き換えながら学習していこうと思います。
本記事は、「該当ページ」 「ClassAPIでの書き方」 「Hooksでの書き方」をセットにして記述していきます。

Learn the Basics

該当ページ: https://facebook.github.io/react-native/docs/tutorial

class
import React, { Component } from 'react';
import { Text, View } from 'react-native';

export default class HelloWorldApp extends Component {
  render() {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <Text>Hello, world!</Text>
      </View>
    );
  }
}
hooks
import React, {Component} from 'react';
import {Text, View} from 'react-native';

export default () => {
  return (
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Hello, world!</Text>
    </View>
  );
};

Props

該当ページ: https://facebook.github.io/react-native/docs/props

class
import React, { Component } from 'react';
import { Text, View } from 'react-native';

class Greeting extends Component {
  render() {
    return (
      <View style={{alignItems: 'center'}}>
        <Text>Hello {this.props.name}!</Text>
      </View>
    );
  }
}

export default class LotsOfGreetings extends Component {
  render() {
    return (
      <View style={{alignItems: 'center', top: 50}}>
        <Greeting name='Rexxar' />
        <Greeting name='Jaina' />
        <Greeting name='Valeera' />
      </View>
    );
  }
}
hooks
import React, {Component} from 'react';
import {Text, View} from 'react-native';

interface Props {
  name: string;
}

const Greeting = (props: Props) => {
  return (
    <View style={{alignItems: 'center'}}>
      <Text>Hello {props.name}!</Text>
    </View>
  );
};

export default class LotsOfGreetings extends Component {
  render() {
    return (
      <View style={{alignItems: 'center', top: 50}}>
        <Greeting name="Rexxar" />
        <Greeting name="Jaina" />
        <Greeting name="Valeera" />
      </View>
    );
  }
}

State

該当ページ: https://facebook.github.io/react-native/docs/state

class
import React, {Component} from 'react';
import {Text, View} from 'react-native';

class Blink extends Component {
  componentDidMount() {
    // Toggle the state every second
    setInterval(
      () =>
        this.setState(previousState => ({
          isShowingText: !previousState.isShowingText,
        })),
      1000,
    );
  }

  //state object
  state = {isShowingText: true};

  render() {
    if (!this.state.isShowingText) {
      return null;
    }

    return <Text>{this.props.text}</Text>;
  }
}

export default class BlinkApp extends Component {
  render() {
    return (
      <View>
        <Blink text="I love to blink" />
        <Blink text="Yes blinking is so great" />
        <Blink text="Why did they ever take this out of HTML" />
        <Blink text="Look at me look at me look at me" />
      </View>
    );
  }
}
hooks
import React, {Component, useEffect, useState} from 'react';
import {Text, View} from 'react-native';

interface Props {
  text: string;
}

const Blink = (props: Props) => {
  useEffect(() => {
    // Toggle the state every second
    setInterval(() => setState(!isShowingText), 1000);
  }, []);

  //state object
  const [isShowingText, setState] = useState(true);

  if (!isShowingText) {
    return null;
  }

  return <Text>{props.text}</Text>;
};

export default () => {
  return (
    <View>
      <Blink text="I love to blink" />
      <Blink text="Yes blinking is so great" />
      <Blink text="Why did they ever take this out of HTML" />
      <Blink text="Look at me look at me look at me" />
    </View>
  );
};

Style

該当ページ: https://facebook.github.io/react-native/docs/style

class
import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';

const styles = StyleSheet.create({
  bigBlue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
  },
  red: {
    color: 'red',
  },
});

export default class LotsOfStyles extends Component {
  render() {
    return (
      <View>
        <Text style={styles.red}>just red</Text>
        <Text style={styles.bigBlue}>just bigBlue</Text>
        <Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
        <Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
      </View>
    );
  }
}
hooks
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';

const styles = StyleSheet.create({
  bigBlue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
  },
  red: {
    color: 'red',
  },
});

export default () => {
  return (
    <View>
      <Text style={styles.red}>just red</Text>
      <Text style={styles.bigBlue}>just bigBlue</Text>
      <Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
      <Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
    </View>
  );
};

Height and Width

該当ページ: https://facebook.github.io/react-native/docs/height-and-width

class
import React, { Component } from 'react';
import { View } from 'react-native';

export default class FlexDimensionsBasics extends Component {
  render() {
    return (
      // Try removing the `flex: 1` on the parent View.
      // The parent will not have dimensions, so the children can't expand.
      // What if you add `height: 300` instead of `flex: 1`?
      <View style={{flex: 1}}>
        <View style={{flex: 1, backgroundColor: 'powderblue'}} />
        <View style={{flex: 2, backgroundColor: 'skyblue'}} />
        <View style={{flex: 3, backgroundColor: 'steelblue'}} />
      </View>
    );
  }
}
hooks
import React, {Component} from 'react';
import {View} from 'react-native';

export default () => {
  return (
    // Try removing the `flex: 1` on the parent View.
    // The parent will not have dimensions, so the children can't expand.
    // What if you add `height: 300` instead of `flex: 1`?
    <View style={{flex: 2}}>
      <View style={{flex: 1, backgroundColor: 'powderblue'}} />
      <View style={{flex: 2, backgroundColor: 'skyblue'}} />
      <View style={{flex: 3, backgroundColor: 'steelblue'}} />
    </View>
  );
};

Layout with Flexbox

該当ページ: https://facebook.github.io/react-native/docs/flexbox

class
import React, { Component } from 'react';
import { View } from 'react-native';

export default class FlexDirectionBasics extends Component {
  render() {
    return (
      // Try setting `flexDirection` to `column`.
      <View style={{flex: 1, flexDirection: 'row'}}>
        <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
        <View style={{width: 50, height: 50, backgroundColor: 'skyblue'}} />
        <View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
      </View>
    );
  }
};
hooks
import React, {Component} from 'react';
import {View} from 'react-native';

export default () => {
  return (
    // Try setting `flexDirection` to `column`.
    <View style={{flex: 1, flexDirection: 'row'}}>
      <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
      <View style={{width: 50, height: 50, backgroundColor: 'skyblue'}} />
      <View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
    </View>
  );
};
class
import React, {Component} from 'react';
import {View} from 'react-native';

export default class JustifyContentBasics extends Component {
  render() {
    return (
      // Try setting `justifyContent` to `center`.
      // Try setting `flexDirection` to `row`.
      <View
        style={{
          flex: 1,
          flexDirection: 'column',
          justifyContent: 'center',
        }}>
        <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
        <View style={{width: 50, height: 50, backgroundColor: 'skyblue'}} />
        <View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
      </View>
    );
  }
}
hooks
import React, {Component} from 'react';
import {View} from 'react-native';

export default () => {
  return (
    // Try setting `justifyContent` to `center`.
    // Try setting `flexDirection` to `row`.
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
        justifyContent: 'center',
      }}>
      <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
      <View style={{width: 50, height: 50, backgroundColor: 'skyblue'}} />
      <View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
    </View>
  );
};
class
import React, { Component } from 'react';
import { View } from 'react-native';

export default class AlignItemsBasics extends Component {
  render() {
    return (
      // Try setting `alignItems` to 'flex-start'
      // Try setting `justifyContent` to `flex-end`.
      // Try setting `flexDirection` to `row`.
      <View style={{
        flex: 1,
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'stretch',
      }}>
        <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
        <View style={{height: 50, backgroundColor: 'skyblue'}} />
        <View style={{height: 100, backgroundColor: 'steelblue'}} />
      </View>
    );
  }
};
hooks
import React, {Component} from 'react';
import {View} from 'react-native';

export default () => {
  return (
    // Try setting `alignItems` to 'flex-start'
    // Try setting `justifyContent` to `flex-end`.
    // Try setting `flexDirection` to `row`.
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'stretch',
      }}>
      <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
      <View style={{height: 50, backgroundColor: 'skyblue'}} />
      <View style={{height: 100, backgroundColor: 'steelblue'}} />
    </View>
  );
};

Handling Text Input

該当ページ: https://facebook.github.io/react-native/docs/handling-text-input

class
import React, { Component } from 'react';
import { Text, TextInput, View } from 'react-native';

export default class PizzaTranslator extends Component {
  constructor(props) {
    super(props);
    this.state = {text: ''};
  }

  render() {
    return (
      <View style={{padding: 10}}>
        <TextInput
          style={{height: 40}}
          placeholder="Type here to translate!"
          onChangeText={(text) => this.setState({text})}
          value={this.state.text}
        />
        <Text style={{padding: 10, fontSize: 42}}>
          {this.state.text.split(' ').map((word) => word && '?').join(' ')}
        </Text>
      </View>
    );
  }
}
hooks
import React, {Component, useState} from 'react';
import {Text, TextInput, View} from 'react-native';

export default () => {
  const [text, setText] = useState('');
  return (
    <View style={{padding: 10}}>
      <TextInput
        style={{height: 40}}
        placeholder="Type here to translate!"
        onChangeText={text => setText(text)}
        value={text}
      />
      <Text style={{padding: 10, fontSize: 42}}>
        {text
          .split(' ')
          .map(word => word && '?')
          .join(' ')}
      </Text>
    </View>
  );
};

Handling Touches

該当ページ: https://facebook.github.io/react-native/docs/handling-touches

class
import React, {Component} from 'react';
import {Button, StyleSheet, View} from 'react-native';

export default class ButtonBasics extends Component {
  _onPressButton() {
    alert('You tapped the button!');
  }

  render() {
    return (
      <View style={styles.container}>
        <View style={styles.buttonContainer}>
          <Button onPress={this._onPressButton} title="Press Me" />
        </View>
        <View style={styles.buttonContainer}>
          <Button
            onPress={this._onPressButton}
            title="Press Me"
            color="#841584"
          />
        </View>
        <View style={styles.alternativeLayoutButtonContainer}>
          <Button onPress={this._onPressButton} title="This looks great!" />
          <Button onPress={this._onPressButton} title="OK!" color="#841584" />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  buttonContainer: {
    margin: 20,
  },
  alternativeLayoutButtonContainer: {
    margin: 20,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
});
hooks
import React, {Component} from 'react';
import {Button, StyleSheet, View} from 'react-native';

export default () => {
  const _onPressButton = () => {
    alert('You tapped the button!');
  };

  return (
    <View style={styles.container}>
      <View style={styles.buttonContainer}>
        <Button onPress={_onPressButton} title="Press Me" />
      </View>
      <View style={styles.buttonContainer}>
        <Button onPress={_onPressButton} title="Press Me" color="#841584" />
      </View>
      <View style={styles.alternativeLayoutButtonContainer}>
        <Button onPress={_onPressButton} title="This looks great!" />
        <Button onPress={_onPressButton} title="OK!" color="#841584" />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  buttonContainer: {
    margin: 20,
  },
  alternativeLayoutButtonContainer: {
    margin: 20,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
});
class
import React, {Component} from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  TouchableHighlight,
  TouchableOpacity,
  TouchableNativeFeedback,
  TouchableWithoutFeedback,
  View,
} from 'react-native';

export default class Touchables extends Component {
  _onPressButton() {
    alert('You tapped the button!');
  }

  _onLongPressButton() {
    alert('You long-pressed the button!');
  }

  render() {
    return (
      <View style={styles.container}>
        <TouchableHighlight onPress={this._onPressButton} underlayColor="white">
          <View style={styles.button}>
            <Text style={styles.buttonText}>TouchableHighlight</Text>
          </View>
        </TouchableHighlight>
        <TouchableOpacity onPress={this._onPressButton}>
          <View style={styles.button}>
            <Text style={styles.buttonText}>TouchableOpacity</Text>
          </View>
        </TouchableOpacity>
        <TouchableNativeFeedback
          onPress={this._onPressButton}
          background={
            Platform.OS === 'android'
              ? TouchableNativeFeedback.SelectableBackground()
              : ''
          }>
          <View style={styles.button}>
            <Text style={styles.buttonText}>
              TouchableNativeFeedback{' '}
              {Platform.OS !== 'android' ? '(Android only)' : ''}
            </Text>
          </View>
        </TouchableNativeFeedback>
        <TouchableWithoutFeedback onPress={this._onPressButton}>
          <View style={styles.button}>
            <Text style={styles.buttonText}>TouchableWithoutFeedback</Text>
          </View>
        </TouchableWithoutFeedback>
        <TouchableHighlight
          onPress={this._onPressButton}
          onLongPress={this._onLongPressButton}
          underlayColor="white">
          <View style={styles.button}>
            <Text style={styles.buttonText}>Touchable with Long Press</Text>
          </View>
        </TouchableHighlight>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    paddingTop: 60,
    alignItems: 'center',
  },
  button: {
    marginBottom: 30,
    width: 260,
    alignItems: 'center',
    backgroundColor: '#2196F3',
  },
  buttonText: {
    textAlign: 'center',
    padding: 20,
    color: 'white',
  },
});
hooks
import React, {Component} from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  TouchableHighlight,
  TouchableOpacity,
  TouchableNativeFeedback,
  TouchableWithoutFeedback,
  View,
} from 'react-native';

export default () => {
  const _onPressButton = () => {
    alert('You tapped the button!');
  };

  const _onLongPressButton = () => {
    alert('You long-pressed the button!');
  };

  return (
    <View style={styles.container}>
      <TouchableHighlight onPress={_onPressButton} underlayColor="white">
        <View style={styles.button}>
          <Text style={styles.buttonText}>TouchableHighlight</Text>
        </View>
      </TouchableHighlight>
      <TouchableOpacity onPress={_onPressButton}>
        <View style={styles.button}>
          <Text style={styles.buttonText}>TouchableOpacity</Text>
        </View>
      </TouchableOpacity>
      <TouchableNativeFeedback
        onPress={_onPressButton}
        background={
          Platform.OS === 'android'
            ? TouchableNativeFeedback.SelectableBackground()
            : ''
        }>
        <View style={styles.button}>
          <Text style={styles.buttonText}>
            TouchableNativeFeedback{' '}
            {Platform.OS !== 'android' ? '(Android only)' : ''}
          </Text>
        </View>
      </TouchableNativeFeedback>
      <TouchableWithoutFeedback onPress={_onPressButton}>
        <View style={styles.button}>
          <Text style={styles.buttonText}>TouchableWithoutFeedback</Text>
        </View>
      </TouchableWithoutFeedback>
      <TouchableHighlight
        onPress={_onPressButton}
        onLongPress={_onLongPressButton}
        underlayColor="white">
        <View style={styles.button}>
          <Text style={styles.buttonText}>Touchable with Long Press</Text>
        </View>
      </TouchableHighlight>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    paddingTop: 60,
    alignItems: 'center',
  },
  button: {
    marginBottom: 30,
    width: 260,
    alignItems: 'center',
    backgroundColor: '#2196F3',
  },
  buttonText: {
    textAlign: 'center',
    padding: 20,
    color: 'white',
  },
});

Using a ScrollView

該当ページ: https://facebook.github.io/react-native/docs/using-a-scrollview

省略

Using List Views

該当ページ: https://facebook.github.io/react-native/docs/using-a-listview

class
import React, { Component } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';

export default class FlatListBasics extends Component {
  render() {
    return (
      <View style={styles.container}>
        <FlatList
          data={[
            {key: 'Devin'},
            {key: 'Dan'},
            {key: 'Dominic'},
            {key: 'Jackson'},
            {key: 'James'},
            {key: 'Joel'},
            {key: 'John'},
            {key: 'Jillian'},
            {key: 'Jimmy'},
            {key: 'Julie'},
          ]}
          renderItem={({item}) => <Text style={styles.item}>{item.key}</Text>}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
   flex: 1,
   paddingTop: 22
  },
  item: {
    padding: 10,
    fontSize: 18,
    height: 44,
  },
})
hooks
import React, {Component} from 'react';
import {FlatList, StyleSheet, Text, View} from 'react-native';

export default () => {
  return (
    <View style={styles.container}>
      <FlatList
        data={[
          {key: 'Devin'},
          {key: 'Dan'},
          {key: 'Dominic'},
          {key: 'Jackson'},
          {key: 'James'},
          {key: 'Joel'},
          {key: 'John'},
          {key: 'Jillian'},
          {key: 'Jimmy'},
          {key: 'Julie'},
        ]}
        renderItem={({item}) => <Text style={styles.item}>{item.key}</Text>}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 22,
  },
  item: {
    padding: 10,
    fontSize: 18,
    height: 44,
  },
});

Networking

該当ページ: https://facebook.github.io/react-native/docs/network

class
import React from 'react';
import { FlatList, ActivityIndicator, Text, View  } from 'react-native';

export default class FetchExample extends React.Component {

  constructor(props){
    super(props);
    this.state ={ isLoading: true}
  }

  componentDidMount(){
    return fetch('https://facebook.github.io/react-native/movies.json')
      .then((response) => response.json())
      .then((responseJson) => {

        this.setState({
          isLoading: false,
          dataSource: responseJson.movies,
        }, function(){

        });

      })
      .catch((error) =>{
        console.error(error);
      });
  }



  render(){

    if(this.state.isLoading){
      return(
        <View style={{flex: 1, padding: 20}}>
          <ActivityIndicator/>
        </View>
      )
    }

    return(
      <View style={{flex: 1, paddingTop:20}}>
        <FlatList
          data={this.state.dataSource}
          renderItem={({item}) => <Text>{item.title}, {item.releaseYear}</Text>}
          keyExtractor={({id}, index) => id}
        />
      </View>
    );
  }
}
hooks
import React, {useState, useEffect} from 'react';
import {FlatList, ActivityIndicator, Text, View} from 'react-native';

export default () => {
  const [isLoading, setIsLoading] = useState(true);
  const [dataSource, setDataSource] = useState([]);

  useEffect(() => {
    const fetchMovies = async () => {
      try {
        const response = await fetch(
          'https://facebook.github.io/react-native/movies.json',
        );
        const responseJson = await response.json();
        setIsLoading(false);
        setDataSource(responseJson.movies);
      } catch (error) {
        console.error(error);
      }
    };

    fetchMovies();
  }, []);

  if (isLoading) {
    return (
      <View style={{flex: 1, padding: 200}}>
        <ActivityIndicator />
      </View>
    );
  }

  return (
    <View style={{flex: 1, paddingTop: 20}}>
      <FlatList
        data={dataSource}
        renderItem={({item}) => (
          <Text>
            {item.title}, {item.releaseYear}
          </Text>
        )}
        keyExtractor={({id}, index) => id}
      />
    </View>
  );
};

感想

Class APIに対してReact Hooksはコンパクトで無駄のない記述ができて素晴らしいですね。
React Nativeは今回初めて触ったのですが、Reactに慣れていればとても使いやすそうだと感じました。
SwiftやKotlinでのネイティブアプリ開発をやってきましたが、もう戻れないんじゃないかと思うくらい開発体験が良さそうです。
もちろん実務で使うとなるとつらみは出てくるのでしょうが、それはそれとしてとにかく使ってみたいと思えるチュートリアルでした。

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

React Native ことはじめ

業務でReactNativeに強くなる必要があったので環境構築からメモ

いちいちググったりしなくていいようにここ見れば解決するような記事のつもり

画面のキャプチャなどは載せない予定

expoを使うと依存関係などまとめてくれるから楽らしい

expoはじめて使った感想としては、
・ライブプレビューすげえ!!
・(AndroidStudioと比較して)QRコード読み込むだけで(expoアプリのインストールは必要かな?)実機プレビューできんのヤバすぎでしょ!!
・各所での説明が初心者に優しいな

コード書き始めるまでにやったこと

まずはwindowsで環境構築した手順のメモ

expo

GetStartWithExpo
に従う
以降は従ってやったこと

Node.js

まずはNode.jsをインストール

インストーラーの指示の通りでOK

Node.jsインストール後

cmdで
npm install expo-cli --global
を実行

プロジェクト作成したいディレクトリに移動し、
expo init my-new-project
my-new-projectはそのままプロジェクト名になるので任意の名前に変更可能

プロジェクト作成後

プロジェクトのディレクトリに移動
cd my-new-project
し、
npm start

ここはやってみた感じ
expo start
の方がいいかもしれない
この
expo start
はライブプレビュー起動するコマンドなので、作業するたび実行する必要なコマンドかと

ここでエラー発生

エラーメッセージは、
Metro 'Bundler' process exited with code 1

stackoverflowを参照。

プロジェクト内の\node_modules\metro-config\src\defaults\blacklist.js

var sharedBlacklist = [
  /node_modules[/\\]react[/\\]dist[/\\].*/,
  /website\/node_modules\/.*/,
  /heapCapture\/bundle\.js/,
  /.*\/__tests__\/.*/
];

var sharedBlacklist = [
  /node_modules[\/\\]react[\/\\]dist[\/\\].*/,
  /website\/node_modules\/.*/,
  /heapCapture\/bundle\.js/,
  /.*\/__tests__\/.*/
];

に書き換えることで解決。

個人的にはエスケープシーケンスあたりが関係していそうかな、と思った。

これで初期環境構築はできた

ここからはApp.jsを中心にゴリゴリにコード書いていく

初期構築から2〜3時間触ってみた感想

  1. (どの言語、フレームワークでも当たり前のことかもしれないけど、)公式ドキュメント読み込まなきゃ

特に、ググって出てくる情報が少ないような気がする
2. レイアウトなど、スタイルの考え方はCSSの考え方が使えそう

React Nativeで使うスタイル記述は、CSSの記述をケバブケースからキャメルケースに直したものっぽい
3. いままで使ってきたPythonみたいなスクリプト言語とは作りが違うので、慣れるのに時間がかかりそう

特にコンポーネントの考え方とかは頭リセットして考えないとこんがらがりそう

この後は画面遷移中心に、がっつりアプリの形を作っていくので、react-navigationを含めて勉強していきます

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

MobX と hooks でプレーンな書き味の React コンポーネントを書く

MobX と React Hooks をうまく組み合わせ、プレーンなものと書き味の変わらない React コンポーネントを書くアイデアを紹介します。

(オリジナルのアイデアは私ではなく誰かのブログに書かれていましたが、そのリンクを失念してしまいました。見つけ次第、リンクを貼りたいと思います。)

MobX store を使うコンポーネントは特殊な書き方を強いられる

MobX の特徴 - observable な store クラス

MobX は Single Page Application における状態管理ライブラリーの一つで、よく React と組み合わせて使われます。

MobX が特徴的なのは、状態を observable な store クラスで表現する点です。具体的には次のように、@observable デコレーター をつけたメンバーを持つクラスです:

TodosStore.ts
import { observable, computed, action, flow } from 'mobx'

type Todo = {
  userId: number
  id: number
  title: string
  completed: boolean
}

/**
 * TODO 一覧を管理する store
 */
export default class TodosStore {
  /**
   * TODO 一覧
   */
  @observable todos: Todo[]

  /**
   * TODO 一覧を読み込み中のとき true
   */
  @computed get loading() {
    return !this.todos
  }

  /**
   * TODO の一つの完了/未完了を切り替える
   *
   * @param id 対象の TODO の ID
   */
  @action.bound toggle(id: number) {
    this.todos = this.todos.map(todo => {
      if (todo.id === id) {
        todo.completed = !todo.completed
      }
      return todo
    })
  }

  /**
   * TODO 一覧を API から取得する
   */
  fetch = flow(function*(this: TodosStore) {
    this.todos = yield fetch(
      'https://jsonplaceholder.typicode.com/todos?userId=1',
    ).then(r => r.json())
  }).bind(this)
}

React において、状態を表すオブジェクトは immutable であることが原則です。しかし MobX は、@observable デコレーターにより、mutable な store インスタンスを状態を表すオブジェクトとして扱えるようにしています。

上記の TodosStore において observable なのは todosloading の値です。コードのイメージは次のようになります:

const store = new TodosStore()

store.todos // undefined
store.loading // true

// 非同期で TODO 一覧を取得する
await store.fetch()

// store インスタンスは同一のまま、メンバーの値だけが変化する。つまり store が mutable
store.todos // [{...}, {...}, ...]
store.loading // false

強いられる特殊な書き方 - observer コンポーネント

React が immutable な状態オブジェクトを要求するのは、状態オブジェクトに変更があればそれを検知し直ちにコンポーネントを再描画するためです。Mutable なオブジェクトを状態に使うとその機構がうまく働かず、状態が変化したのに再描画が起きない不具合となります。その点は MobX を使ったとしても同じで、次のように書くと、TODO を読み込んだあとも再描画が起きなくなります:

App.tsx
import React, { useEffect } from 'react'
import { observer } from 'mobx-react'
import { Section, Title, Loading, Todo } from './components'
import TodosStore from './TodosStore'

const store = new TodosStore()

export default function App() {
  // store の todos や loading の値が変化したことをコンポーネントは検知できない。
  const { todos, loading, toggle, fetch: fetchTodos } = store

  // そのため API から TODO 一覧を取得しても、
  useEffect(() => {
    fetchTodos()
  }, [fetchTodos])

  // ずっと読み込み中表示のまま。
  if (loading) {
    return (
      <Section>
        <Loading />
      </Section>
    )
  }

  // ここには至らない。
  return (
    <Section>
      <Title>Todos</Title>

      {todos.map(({ id, title, completed }) => (
        <Todo
          key={id}
          label={title}
          completed={completed}
          onChange={() => toggle(id)}
        />
      ))}
    </Section>
  )
}

これを回避するため、MobX store を使うコンポーネントは特殊な書き方を強いられます。つまり、observer 関数 でラップしてやる必要があります:

App.tsx
 import React, { useEffect } from 'react'
+import { observer } from 'mobx-react'
 import { Section, Title, Loading, Todo } from './components'
 import TodosStore from './TodosStore'

 const store = new TodosStore()


-export default function App() {
+export default observer(function App() {
   const { todos, loading, toggle, fetch: fetchTodos } = store

   useEffect(() => {
 ...
       ))}
     </Section>
   )
-}
+})

コンポーネントが MobX 専用となってしまい、再利用性やテストの観点でやりづらさを生じ得ます。できることなら JSX.Element を返すだけのただの関数 としてプレーンなコンポーネントを定義したいものです。

useObserver hooks を使い observer をなくす

mobx-react 公式で紹介されている方法 - JSX.Element を返す

observer を使わない書き方は mobx-react のドキュメントで紹介されています。それは useObserver hook を使った書き方で、次のようなものです:

App.tsx
 import React, { useEffect } from 'react'
-import { observer } from 'mobx-react'
+import { useObserver } from 'mobx-react'
 import { Section, Title, Loading, Todo } from './components'
 import TodosStore from './TodosStore'

 const store = new TodosStore()


-export default observer(function App() {
-  const { todos, loading, toggle, fetch: fetchTodos } = store
+export default function App() {
+  const { fetch: fetchTodos } = store

   useEffect(() => {
     fetchTodos()
   }, [fetchTodos])


+  return useObserver(() => {
+    const { todos, loading, toggle } = store

     if (loading) {
       return (
         <Section>
           <Loading />
         </Section>
       )
     }

     return (
       <Section>
 ...
       </Section>
     )
-})
+  })
+}

たしかに observer によるラップは消えました。しかし useObserver の戻り値が JSX.Element で、違和感があります。何より、プレーンな関数であることが利点 のはずの hook が JSX.Element を返してしまうと、useObserver やほかの hooks をまとめてカスタムフックを作ることが難しくなります。

改良した方法 - store の slice を返す

ドキュメントでは JSX.Element を返す方法が紹介されていたものの、useObserver から JSX.Element を返す必要性はないようです。つまり store インスタンスを useObserver でラップしてやれば、コンポーネントは store の mutable な変化を検知できるようです:

App.tsx
 const store = new TodosStore()

 export default function App() {
-  const { fetch: fetchTodos } = store
+  // 複数の値を返すのにタプルを使っているが、単なる好み。オブジェクトを返すスタイルでもよい。
+  const [todos, loading, toggle, fetchTodos] = useObserver(() => [
+    store.todos,
+    store.loading,
+    store.toggle,
+    store.fetch,
+  ])

   useEffect(() => {
     fetchTodos()
   }, [fetchTodos])


-  return useObserver(() => {
-    const { todos, loading, toggle } = store
-
     if (loading) {
       return (
         <Section>
 ...
         ))}
       </Section>
     )
-  })
 }

useObserver から、JSX.Element を返す代わりに、store の一部分 (slice) を返しています。JSX.Element を useObserver で囲むことも、コンポーネント全体を observer で囲むこともなくなり、プレーンなコンポーネントの書き方となりました。

この書き方をベースに useContext を加え、より意味のあるカスタムフックを作ってみます。コンテキストを使うことにより App コンポーネントへ任意の store インスタンスを渡すことが可能になるほか、コンポーネントからは store を使っていることがほとんど意識されない書き味になります:

App.tsx
 import React, { useEffect } from 'react'
-import { useObserver } from 'mobx-react'
 import { Section, Title, Loading, Todo } from './components'
-import TodosStore from './TodosStore'
+import useTodosStore from './useTodosStore'
-
-const store = new TodosStore()

 export default function App() {
-  const [todos, loading, toggle, fetchTodos] = useObserver(() => [
+  // このファイルにべた書きされた store インスタンスではなく、コンテキスト経由の store を使うことで、
+  // テスト時や Storybook を使うときにモックしやすくなる。
+  const [todos, loading, toggle, fetchTodos] = useTodosStore(store => [
     store.todos,
     store.loading,
     store.toggle,

ここで使われているカスタムフック useTodosStore の実装は次のようになります:

useTodosStore.ts
import { createContext } from 'react'
import useStore, { Selector } from './useStore'
import TodosStore from './TodosStore'

const context = createContext<TodosStore | null>(null)

export const TodosProvider = context.Provider

// TodosStore の slice を取得するための hook
// 汎用の useStore を TodosStore 専用にした。
export default function useTodosStore<TSelection>(selector: Selector<TodosStore, TSelection>) {
  return useStore(context, selector)
}
useStore.ts
import { useContext } from 'react'
import { useObserver } from 'mobx-react'

export type Selector<TStore, TSelection> = (store: TStore) => TSelection

// useContext と useObserver を組み合わせた、任意の store 型に対応したカスタムフック。
// この hook を介して store slice を取得すれば、コンポーネントが store の mutable な変更を検知できる。
export default function useStore<TStore, TSelection>(
  context: React.Context<TStore>,
  selector: Selector<TStore, TSelection>,
) {
  const store = useContext(context)
  if (!store) {
    throw new Error('need to pass a value to the context')
  }

  return useObserver(() => selector(store))
}

カスタムフックによって store の実体が隠蔽され、store がどこでインスタンス化されたかだけでなくそれが MobX store かどうかすら気にすることなく、store を使えるようになりました。

Store の種類を増やすたびに useXxxStore のような hooks 名や定義ファイルが増えるのが気になる場合は、store の定義ファイルに hooks を書くなり、static メソッドとして hooks を(ついでに provider も)持たせるなりすればよいと思います。

まとめ

  • MobX store を使うコンポーネントは特殊な書き方を強いられる
    • observer によるラップ
    • MobX 専用の書き方はなくしたい
  • useObserver hook を使い observer をなくせる
    • useObserver から store slice を返す
    • コンテキスト経由で store を受け渡すカスタムフックを作り、store の実体を隠蔽する
      • カスタムフックの実装を変えれば、コンポーネントを修正することなく、MobX 以外の状態管理ライブラリーに切り替えることも可能
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む