20200331のReactに関する記事は10件です。

今更ながらReact Nativeに入門してみた①

はじめに

実は最近、React Nativeなるものに入門してしまいました...。

React Nativeとはなんなのか?といったところの説明から、実際に簡易的なTODOアプリを作成するところまでご紹介できればと思います。

では、早速行ってみましょう〜!

React Nativeとは何か

React Nativeとは、

Reactを使って、iOSやAndroidなどのネイティブアプリを開発することができるクロスプラットフォームな開発ができるフレームワークです。

公式サイトはこちら

なぜReact Nativeで作るか

個人的にメリットだと感じる点はいくつかあります。

  1. React(JavaScript)で書ける
  2. プラットフォームを意識せず開発できる

普段JavaScriptでバックエンドやフロントエンドを書いている自分にとって、同じような感覚でネイティブアプリも作れてしまうのは非常に魅力的です!

また、本来であればiOSならSwift、AndroidならKotlinなど、プラットフォームごとに言語を使い分けるか、どちらかのプラットフォームに絞って開発していたものが、多少UIを変更するだけで良いのはユーザー獲得という観点でもメリットが大きいように思います。

また、ネイティブアプリだけでなく、WEBアプリ版も作成する時、WEBでもReactを使えば、アプリケーション自体はそのまま使い回し、UIのみ変更すれば良いので、そういう意味でもクロスプラットフォームな開発ができます。

Expoとは何か

React Nativeによるネイティブアプリの開発は、Reactを使えることによってかなり開発しやすくなったとはいえ、普段WEBを開発しているエンジニアにとっては少しハードルが高くなります。

そんなネックを解消してくれるのが、Expoです。

Expoは、React Nativeによる開発をより簡単に、わかりやすくしてくれる開発用ツールです。

どう簡単にしてくれるかというと、

  • ほぼReactでWEBアプリを開発している感覚でコードが書ける
  • iOSアプリの場合、アプリのアップデートが再審査なしで行える

などがあります。

ただ、デメリットとして、

  • 実装できる機能に制限がある

ことは念頭に置いておいた方が良さそうです。

公式サイトはこちら

簡易的なTODOアプリの作成

それでは、早速React Nativeを使ってTODO管理ができるアプリを作ります。

今回はアプリの配信はせず、ローカル環境で動作させるだけに留めます。

また、今回は、

  • 開発のしやすさ
  • 実装したい機能を全て実現可能

ということで、Expoも使っていきます。

まずは公式ドキュメントに沿ってReact NativeとExpoの環境構築を済ませておいてください。

プロジェクトを作成したいディレクトリで、Expoプロジェクトを作成します。

expo init todo_app_expo

プロジェクトを作成したら、プロジェクトのルートディレクトリでExpoを起動します。

expo start

コンポーネントに切り分ける

まずは、作りたいものをなるべく最小単位で、コンポーネント化していきます。

今回はシンプルなTODOアプリなので、必要なコンポーネントは下記の通りです。

  1. ユーザーの入力を受け付けるフォーム&ボタン
  2. 登録されたTODOを表示する場所

もっと細かくコンポーネント化できるかもしれません。

その辺りは追々身につけていきます笑

UIを作成する

次に、コンポーネントごとにまずは画面上に表示させてみましょう。

ExpoでiOSシミュレーターを起動させたら、自動でコードの変更を検知して更新されるような設定になっています。

コンポーネントは、ルートディレクトリ直下にcomponentsディレクトリを作成して、その下に配置していきます。

各コンポーネントの実際のコードは下記の通りです。

./components/Form.tsx

フォームの作成は、React NativeのTextInputを使用します。

また、ボタンはReact NativeのButtonを使用します。

ドキュメントはこちら

コンポーネントのソースは下記の通りです。

./components/Form.tsx
import React from 'react';
import { View, Button, TextInput, StyleSheet } from 'react-native';

const Form = () => {

  const [value, onChangeText] = React.useState('');
  const placeholder = 'テキストを入力してください'

  return (
    <View style={styles.form}>
      <TextInput
        style={styles.form}
        onChangeText={text => onChangeText(text)}
        value={value}
        placeholder={placeholder}
      />
      <Button
        onPress={() => {alert('pressed!')}}
        title="Submit"
        color="blue"
      />
    </View>
  )
}

const styles = StyleSheet.create({
  form: {
    height: 40,
    width: '100%',
    borderColor: 'gray',
    borderWidth: 1,
    margin: 'auto',
  }
})

export default Form;

ボタンを押した時のalertは仮実装なので、後ほどきちんと作ります。

./components/TodoList.tsx

TODO一覧のコンポーネントは、現状ダミーのデータを用意しています。

ロジックとしては、TODOが配列で格納されているので、そのデータをReact NativeのFlatListでループ処理して表示しています。

削除ボタンは現状alert出すだけの仮実装です。

ソースは下記の通りです。

./components/TodoList.tsx
import React from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';

const TodoList = () => {

  const todoList = ['TODO1', 'TODO2', 'TODO3'];

  return (
    <View style={styles.list}>
      <FlatList
        data={todoList}
        renderItem={({item}) => (
          <View>
            <Text style={styles.item}>{item}<Text onPress={() => {alert('delete!')}}> ×</Text></Text>
          </View>
        )}
        keyExtractor={(item, index) => index.toString()}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  list: {
    alignItems: 'center',
    marginBottom: 50
  },
  item: {
    fontSize: 22
  }
})

export default TodoList;

あとは軽く配置を整えてとりあえずUIは完成とします。

現状はこんな感じです。

stateでコンポーネントの状態を保持する

さて、UIが完成したら、実際にTODOアプリとして動作するようにしていきます。

TODOアプリにするためには、

  1. 入力したTODOを登録できる
  2. 入力したTODOが一覧表示される
  3. 完了したTODOを削除できる

上記の要件を満たす必要があります。

また、上記の要件を満たすためには、TODOを一時的なデータとしてクライアント側で保持しておく必要があります。

そういったことを実現するために、React Nativeにはstateという物が用意されています。

stateの公式ドキュメントはこちら

stateとは、各コンポーネントの状態を保持するための機能で、

コンポーネントの状態とは、ここでいうTODOが登録されたり削除されたり、といったことを指します。

では早速、stateを用いてTODOの登録、一覧表示、削除を実装していきましょう。

stateの定義・初期化

まずは、現状のTodoデータは下記のコードで定義しています。

const todoList = ['TODO1', 'TODO2', 'TODO3'];

これをstateを使用するように記述すると、下記のようになります。

const [list, setList] = useState(['TODO1', 'TODO2', 'TODO3']);

TODOリストなので、現状は空の配列でstateの値を初期化しておきましょう。

const [list, setList] = useState([]);

これでコンポーネント内で値を保持しておくことができるようになりました!

stateの値を表示・更新する

次は実際にstateで保持している値を表示して、更新するところまでです。

stateの値を使用するときは、下記のように使用できます。

const [list, setList] = useState([]);

const FuncComponent = () => {
    return(
        <Text>{list}</Text>
    )
}

そして、値の更新は下記のように行います。

const [list, setList] = useState([]);

const FuncComponent = () => {
    setList(['TODO']);
    return(
        <Text>{list}</Text>
    )
}

上記のコードだと、更新された値が表示されています。

少し解説すると、stateを作成するのに使用している「useState」ですが、引数には初期値を渡します。

今回のTODOアプリで言うと、空の配列を渡していますよね。

そして、useStateは実際のstateの値と、stateの値を更新する関数を返します。

上記のコードでは、それを変数に格納しているというわけです。

以上を踏まえて、実際に実装したTODO登録の関数とTODO削除の関数を作成しました。

(TODO登録の関数)
javascript
const addList = (item: string) => {
const newList = [...list, item];
setList(newList);
}

(TODO削除の関数)
javascript
const deleteList = (item: string) => {
const newList = list.filter(li => { return li !== item });
setList(newList);
}

一点注意点としては、stateの値の操作で配列を扱う際、

破壊的なメソッドを使用するとリアクティブにレンダリングされなくなります

ですので、値の追加にはpushなどは使わず、今回のようなスプレッド演算子を使うか、concatなどで追加します。

値の削除も同じように、popやshiftではなく、filterなどを使用すると良いでしょう。

propsで親コンポーネントから子コンポーネントに値を渡す

最後に、切り分けたコンポーネント間で値を受け渡せるようにしましょう。

コンポーネントの階層としては、Appコンポーネントが親コンポーネントとなり、それぞれFormコンポーネントとTodoListコンポーネントを子コンポーネントとしています。

親コンポーネントから子コンポーネントに値を渡す際に使えるのが、propsです。

例えば、親コンポーネントで保持している数字を、子コンポーネントで表示する際は下記のようなコードになります。

(親コンポーネント)

const ParentsFunc = () => {
    const [number, setNumber] = useState(0);

    return (
        <ChildFunc num={number} />
    )
}

(子コンポーネント)

const ChildFunc = (props) => {
    return (
        <Test>{props.num}</Test>
    )
}

上記のように記述することで、親コンポーネントから渡ってきた値を子コンポーネントで使用することができます。

また、

const ChildFunc = ({num}) => {
    return (
        <Test>{num}</Test>
    )
}

propsは受け取る時点で展開しておけば、記述量が少なくなります。

このpropsを利用して、同じ容量で先ほど作成した登録用の関数と削除用の関数を親コンポーネント内で定義し、子コンポーネント内で発火させるように実装していきます。

そしてついに完成しました!

完成品のコードはこちら

終わりに

今回初めてReactNativeを使ってみましたが、ドキュメントも充実していて学びやすかったです。

WEBアプリと比べて、ネイティブアプリの方が一般の方に使ってもらいやすいサービスが作れそうでワクワクします!

次はReduxをある程度使えるようになったら、また記事にしてみようかなあ。

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

【React Hooks】グローバル状態管理にはContextAPIを使おう

Reactでグローバルな状態管理といえばReduxですが、Context APIを使えば似たようなことができるので紹介します。

まずは、グローバル状態を定義します。

Global.js
import React, { useState } from 'react';

export const GlobalContext = React.createContext({});

export const GlobalProvider = ({
    children
}) => {
    const [someState, setSomeState] = useState(null);
    const [someState2, setSomeState2] = useState(null);

    return (
        <GlobalContext.Provider
            value={{
                someState,
                setSomeState,
                someState2,
                setSomeState2
            }}
        >
            {children}
        </GlobalContext.Provider>
    );
};

ここではGlobalContextGlobalProviderの二つをexportします。

次に、アプリケーションのルートにあたるファイル(./src/index.jsまたは./src/App.js)内でGlobalProviderを読み込ませます。

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

import App from './App';
import { GlobalProvider } from '@/contexts/Global';

ReactDOM.render((
    <GlobalProvider>
        <App />
    </GlobalProvider>
), document.getElementById('root'));

あとは、コンポーネント内でGlobalContextを読み込めば、グローバル状態を参照、更新できるようになります。

Page.js
import React, { useEfefct, useContext } from 'react';
import { GlobalContext } from '@/contexts/Global';

export default () => {
    const { someState, setSomeState } = useContext(GlobalContext);

    useEfefct(() => {
        // handler
    }, [someState]);

    return (
        <div>
            {someState}
        </div>
    );
};

Reduxよりもお手軽にグローバルな状態管理ができるようになりました。
個人的にはフローがややこしいReduxよりもこちらの方が便利だと思いました。

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

Apollo-Client のネットワークエラーを表示する

Apollo-client では 400 エラー等を受け取った際、デフォルトではエラーメッセージが表示されない。
Apollo-Client 内部でエラー処理され、エラーステータスに沿ったメッセージがerror.messageに格納される。

  • 400 エラーの例
Error: Network error: Response not successful: Received status code 400

環境

{
  "dependencies": {
    "@apollo/react-hooks": "^3.1.3",
    "apollo-cache-inmemory": "^1.6.5",
    "apollo-client": "^2.6.8",
    "apollo-link-error": "^1.1.12",
    "apollo-link-http": "^1.5.16"
  }
}

apollo-link-error

apollo-client では、link という仕組みがあり、単純に言えばミドルウェアのような働きをします。

簡単に言うと、Apollo Links は連鎖可能な「ユニット」であり、これを組み合わせることで、各 GraphQL リクエストが GraphQL クライアントでどのように処理されるかを定義することができます。GraphQL リクエストを実行すると、各リンクの機能が次々と適用されます。これにより、アプリケーションに適した方法でリクエストのライフサイクルを制御することができます。例えば、リンクはリトライ、ポーリング、バッチ処理などを提供することができます。

Composable networking for GraphQL - Apollo Link - Apollo GraphQL Docs

エラー文表示にはエラーハンドリングのための link である apollo-link-error を使用します。

$ yarn add apollo-link-error

client に link を追加

  • before
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";

const link = new HttpLink({
  uri: "https://sample.com/graphql/",
  fetch: fetch
});
const cache = new InMemoryCache();
const client = new ApolloClient({ link, cache });
  • after
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
import { onError } from "apollo-link-error";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";

const httpLink = new HttpLink({
  uri: "https://sample.com/graphql/",
  fetch: fetch
});
const cache = new InMemoryCache();
// errorLink
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.map(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      )
    );
  if (networkError) console.log(`[Network error]: ${networkError}`);
});
const link = ApolloLink.from([errorLink, httpLink]);
const client = new ApolloClient({ link, cache });

errorLink を作成し、onError 関数を定義します。
ApolloLink.from で link を連結し、ApolloClient に渡します。

注意
ApolloLink.from でリンクを連結する際、順番には気を付ける必要があります。

// error
const link = ApolloLink.from([httpLink, errorLink]);

自分の環境では、以下のようなエラーメッセージが出ました。

Error: You are calling concat on a terminating link, which will have no effect
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】react-native-modal-datetime-picker導入手順

はじめに

今回の記事は完全俺得な、備忘録になっています。
公式

導入しようと思ったきっかけ

日付の入力欄を作ろうとすると結構面倒なことが増える
 ・型チェック
  →文字列と日付型以外はNG
 ・うるう年の確認
  →ただめんどくさい
 etc...
そうゆう入力チェックとかめんどくせえなって思っていい感じのライブラリないかなと思ったらあった
それでいて使いやすい。最高
共有します。

導入コマンド

expoを使わない場合

#npmをご利用の方
$ npm i react-native-modal-datetime-picker @react-native-community/datetimepicker

#yarnをご利用の方
$ yarn add react-native-modal-datetime-picker @react-native-community/datetimepicker

expoを使う場合

expo install react-native-modal-datetime-picker @react-native-community/datetimepicker

利用例

App.js
import React, { useState } from "react";
import { Button, View, StyleSheet } from "react-native";
import DateTimePickerModal from "react-native-modal-datetime-picker";

const App = () => {
  const [isDatePickerVisible, setDatePickerVisibility] = useState(false);

  const showDatePicker = () => {
    setDatePickerVisibility(true);
  };

  const hideDatePicker = () => {
    setDatePickerVisibility(false);
  };

  const handleConfirm = date => {
    console.warn("A date has been picked: ", date);
    hideDatePicker();
  };

  return (
    <View style={styles.container}>
      <Button title="Show Date Picker" onPress={showDatePicker} />
      <DateTimePickerModal
        isVisible={isDatePickerVisible}
        mode="date"
        onConfirm={handleConfirm}
        onCancel={hideDatePicker}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  }
})
export default App;

公式に乗っているサンプルを少しだけいじっただけです。はい。
このサンプルでいうことがあるとしたらmode="date"と設定されていますが、もちろんいろんなタイプがあります。
'time'と'datetime'あるそうです。
要は時間も設定できるやつもあるよってことです。試してみてください。
またIOSのみですが、isDarkModeEnabledを使うことでダークモードにも対応。

他にもいろいろ属性あるので試してみてください。
何か良さげな使い方見つけたら更新しますのでチェックしててください!
それでは!

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

自動で伸縮するtextareaコンポーネントの実装

はじめに

入力に合わせてサイズが変わるテキストエリアは、MDN にあるように Element.scrollHeight で要素の高さを取得しこれを HTMLElement.style.height にセットすることで実装できる。
(MDN の例は oField.clientHeight < oField.scrollHeight によりテキストを削除してもサイズは小さくならない。)
CSS の設定も必要で、overflow-y: hidden; を書かないとスクロールバーが出てしまい正しく動作しない。

これを React コンポーネントでやる場合の記事がなかったのと、textarea.style.height = textarea.scrollHeight + "px"; の前に一度 textarea.style.height をリセットしないと正しく動作しないというのに小一時間嵌ったので、メモ程度にどんな感じになのか残しておく。

自動で伸縮する textarea コンポーネントの実装

React において setState() に類する操作は非同期的に行われるため、setContent() による変更が確実に反映されている useEffect() でサイズの変更を行う。
基本的には、素の JS でやる場合と同様に textarea.scrollHeighttextarea.style.height にセットするだけだが、React でやる場合はこの処理の前に textarea.style.height をリセットする必要がある。
これにより正しく伸縮が行われ、コンポーネントの最小の高さを指定することも可能になる。

.auto-height-textarea {
  overflow-y: hidden; /* 必須 */
  overflow-wrap: break-word;
  resize: none;
}
import React, { useCallback, useEffect, useRef, useState } from "react";

type Props = {
  minHeight: number;
};

const AutoHeightTextarea: React.FC<Props> = ({ minHeight }) => {
  const [content, setContent] = useState("");
  const ref = useRef<HTMLTextAreaElement>(null);

  useEffect(() => {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const textarea = ref.current!;
    textarea.style.height = `${minHeight}px`; // 必須, これより小さくならない
    textarea.style.height = `${textarea.scrollHeight}px`;
  }, [content, minHeight]);

  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      setContent(e.target.value);
    },
    []
  );

  return (
    <textarea
      className="auto-height-textarea"
      value={content}
      onChange={handleChange}
      ref={ref}
    />
  );
};

参考URL

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

ReactとReduxで非同期処理をやってみる【Redux-saga】

Reduxで非同期処理を始める

Redux-thunkかRedux-sagaを使うのが一般的だと思いまが、ここではRedux-sagaについて触れます。
たぶんRedux-sagaの内容に関する記事等はあると思うので機能やシステムについてはあまり触れません。
基本的にコードメインで書いてます。

まずReduxとRedux-saga、あとredux-devtools-extensionを入れておきます。

$ cd your_project_client
$ yarn add redux react-redux @types/react-redux redux-devtools-extension redux-saga

そして、src/配下のindex.tsxにReduxの設定を行います。

index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'

import createSagaMiddleware from 'redux-saga'
import { createStore, compose, applyMiddleware } from 'redux'
import { devToolsEnhancer } from 'redux-devtools-extension'
import { Provider } from 'react-redux'
import RootReducer from './reducers'
import RootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  RootReducer,
  compose(applyMiddleware(sagaMiddleware), devToolsEnhancer({})),
)
sagaMiddleware.run(rootSaga)

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

RootReducerについて

src/index.tsxでimportしているRootReducerは./reducers/index.tsに定義します。
exampleReducerの中身はこのあと作っていきます。

*CombinedReducers型のimport元の@modelsはwebpack.config.jsでsrc/modelsのエイリアスに設定しています。

reducers/index.ts
import { combineReducers } from 'redux'
import { CombinedReducers } from '@models'
// CombinedReducers型の中身の例
// interface CombinedReducers {
//   example: { data: string[] }
// }

import exampleReducer from './examples'

// exampleReducer.dataは文字列の配列
export const RootReducer: CombinedReducers = combineReducers({
  example: exampleReducer,
})

RootSagaについて

src/index.tsxでimportしているRootSagaは、./sagas/index.tsに定義します。
exampleWatcherの中身はこのあと作っていきます。

index.ts
import { all, fork } from 'redux-saga/effects'
import { exampleWatcher } from './examples'

export default function* RootSaga() {
  yield all([fork(exampleWatcher)])
}

Reduxのシステムを作る

Redux自体については前提知識として、以下の記事など学習に良さそうでした。
https://qiita.com/mpyw/items/a816c6380219b1d5a3bf

以下3点に修正を加えます。

  1. actions/example.ts
    (上に書いたsrc/index.tsxと同じディレクトリ内に作る。reducerssagas共同じディレクトリ)
  2. reducers/example.ts
  3. components/App.tsx
    (上に書いたsrc/index.tsxでimportしているやつ)
actions/example.ts
import Models from '@models'

interface Request {
  userId: string
}
interface Response {
  data: string[]
}

// 別ファイルに分けても良いかも
export namespace Types {
  export const STARTED_GET_DATA = 'STARTED_GET_DATA'
  export const SUCCEEDED_GET_DATA = 'SUCCEEDED_GET_DATA'
  export const FAILED_GET_DATA = 'FAILED_GET_DATA'
}

export const getData = {
  // 取得開始を宣言するアクション
  started: (request: Request) => {
    return {
      type: ActionType.STARTED_GET_DATA as typeof ActionType.STARTED_GET_DATA,
      payload: request, // sagaに渡した時に、payload.userIdで取得する
    }
  },
  // 取得完了成功を宣言するアクション
  succeeded: (response: Response) => ({
    type: ActionType.SUCCEEDED_GET_DATA as typeof ActionType.SUCCEEDED_GET_DATA,
    payload: response,
  }),
  // 取得完了失敗を宣言するアクション
  failed: (response: Response) => ({
    type: ActionType.FAILED_GET_DATA as typeof ActionType.FAILED_GET_DATA,
    payload: response,
  }),
}

export type DataAction =
  | ReturnType<typeof getData.started>
  | ReturnType<typeof getData.succeeded>
  | ReturnType<typeof getData.failed>
reducers/example.ts
import {
  ExampleAction,
  Types,
} from '@actions/example'

export interface ExampleState {
  data: string[]
  startedGetData: boolean
  getDataStatus: {
    succeeded: boolean
    failed: boolean
  }
}

export const initialState: ExampleState = {
  data: [],
  startedGetData: false,
  getDataStatus: {
    succeeded: false,
    failed: false,
  },
}

const exampleReducer = (
  state: ExampleState = initialState,
  action: ExampleAction,
): ExampleState => {
  switch(action.type) {
    case Types.STARTED_GET_DATA:
      return {
        ...state,
        startedGetData: true,
      }
    case Types.SUCCEEDED_GET_DATA:
      return {
        ...state,
        data: action.payload.data, // APIの返却方法によって形が変わる
        startedGetData: false,
        getDataStatus: {
          succeeded: true,
          failed: false
        },
      }
    case Types.FAILED_GET_DATA:
      return {
        ...state,
        startedGetData: false,
        getDataStatus: {
          succeeded: false,
          failed: true,
        },
      }
    default:
      console.warn(`There is something wrong with the action passed: ${JSON.stringify(action, null, 2)}`)
      return state
  }
}

export default exampleReducer
App.tsx
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { CombinedReducers } from '@models' // reducers/index.tsでimportしたのと同じもの
import { getData } from '@actions/example'

const App: React.FC = () => {
  const dispatch = useDispatch()
  const data: Example[] = useSelector((state: CombinedReducers) => {
    return state.example.data
  })
  React.useState(() => {
    // APIを呼んでdataにデータを注入する。
    dispatch(getData.started)
  }, [])

  return (
    <>
      <h1>Datas</h1>
      {
        datas.map((item: string, index: number) => (
          <div key={index}>
            <p>{item}</p>
          </div>
        ))
      }
    </>
  )
}

export default App

ここまででApp.tsxを表示した際にdispatch(getData.started)が実行され、
アクションクリエイター(actions/examplegetData.started)を経て、
exampleReducerType. STARTED_GET_DATAからステイトが返却されます。

ですがこのままではAPIへのリクエストを出していないので、reducers/example.tsinitialState.startedGetDatatrueに変化するだけとなってしまいます。
(ReduxDevtoolsのchrome拡張機能が入っているのであればそこでも確認できます)

ですので、ここからAPIリクエストを出す部分のコードを書いていきます。
ここでRedux-sagaが登場します。

Redux-sagaで非同期処理を行う

非同期処理を行うために、App.tsx内に処理を書けばいいじゃん?と考える人もいるかもしれませんが、それはあまり推奨されません。
コンポーネントの中に書くと記述が煩雑になりますし、もし他の場所で同様のAPIを呼ぶ場合などがあるとコードの可用性が損なわれてしまいます。

ですので、コンポーネント外で非同期の処理を行うわけですが、ここでRedux-sagaとRedux-thunkが登場します。
基本的にどちらを使うかは好みによりますが、コードの可読性や可用性を考慮するとRedux-sagaの方が個人的にはオススメです(Redux-sagaは従来のReduxアーキテクチャとは疎結合して動いているので、Redux-thunkよりかはスッキリした書き方ができます)。
ただ、正直なところRedux-sagaはRedux-thunkより学習コストが高いです。しっかりしている分仕方がないと思いますが、軽く手っ取り早く初心者が非同期でReduxを使いたいような場合にはもしかするとRedux-thunkのほうが向いているかもしれないです。

sagasに下記のファイルを追加する。

sagas/example.ts
import { call, put, takeLatest } from 'redux-saga/effects'
import { getData, Types } from '@actions/example'
import { getDataApiFunction } from '@apis/example'

// ジェネレータ関数でyieldごとに非同期で処理を実行していく
function* getDataSaga(action: ReturnType<typeof getData.started>) {
  console.log(`action: ${JSON.stringify(action, null, 2)}`)
  // action: {
  //   "type": "STARTED_GET_DATA",
  //   "payload": { "userId": "XXXXX" }
  // }
  try {
    // userIdをクエリとするために引数にpayload.userIdを含めてgetDataApiFunction()を実行する
    const result = yield call(getDataApiFunction(), action.payload.userId)
    console.log(`result: ${JSON.stringify(result, null, 2)}`)
    // result: ["hoge", "foo", "bar", "baz"]

    // 取得完了成功宣言
    yield put(getFiles.succeeded(result))
  } catch(err) {
    console.error('Failed to get data')
    // 取得完了失敗宣言
    yield put(getFiles.failed({ data: [] }))
  }
}

// これをexportしてsagas/index.ts内のRootSaga内でforkしてあげます。
// そうすると、Redux-sagaはtakeLatestの第一引数のアクションが作成された際にgetDataSagaを実行するようになります。
export function* exampleWatcher() {
  yield takeLatest(Action.STARTED_GET_DATA, getDataSaga)
}

そして、最後にAPIリクエストを出している部分をapisに追加します。
ここに記述するgetDataApiFunctionが先ほどgetDataSaga内で最初にyieldで非同期実行する内容です。

apis/example.ts
import axios from 'axios'

interface ApiConfig {
  baseURL: string
  timeout: number
}

const API_CONFIG: ApiConfig = {
  baseURL: 'API URI to get data',
  timeout: 7000
}

export const getDataApiFunction = async (userId: string) => {
  const instance = axios.create(API_CONFIG)
  try {
    // ここでデータ取得
    const response = await instance.get(`${API_CONFIG.baseURL}?userId=${userId}`)
    if (response.status !== 200) {
      throw new Error(`Failed: status code is ${response.status}`)
    }
    console.log(`response: ${JSON.stringify(response, null, 2)}`)
    const data: string[] = response.data
    return data
  } catch(err) {
    throw new Error(err)
  }
}

上記のgetDataApiFunctionの実行が滞りなく行われると、
sagas/example.tsgetDataSagaの処理は
yield put(getFiles.succeeded(result))まで進み、
reducers/example.tsSUCCEEDED_GET_DATAの、文字列配列dataを含めたステイトを返却します。

すると、Api.tsxのmapしている部分までデータが渡り、ブラウザにデータが表示されるようになります。

以上でRedux-sagaを使った非同期処理を一通り実装できます。
一応主観ですが、わかりやすく書くためにinterfaceをファイルごとに同じものを定義しましたが、本来はどこかにまとめてそこを見にいくようにした方が良いです。

最終的なsrcディレクトリ内の構成

.
├── actions
│   └── example.ts
├── components
│   └── App.tsx
├── index.tsx
├── models
│   └── index.ts
├── public
│   ├── index.css
│   └── index.html
├── reducers
│   ├── example.ts
│   └── index.ts
├── sagas
│   ├── example.ts
│   └── index.ts
└── apis
    └── example.ts
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

静的サイトジェネレーター Gatsby

静的サイトジェネレーターとは?

Static Site Generator(SSG)

WebサイトのHTMLファイルを生成するツールのこと

Wordpressのような従来CMSの仕組みは、MySQLなどのDBをもとに、サーバーでHTMLを生成して返すものだった

それに対し静的サイトジェネレーターは、コンパイル時にGraphQLやAPIからすべてのデータを取得し全てのHTMLを最初に生成する

さらに、生成されたファイルを、Netlifyなどのホスティングサービスを用いて、サーバーレスで公開する仕組みが主流になっている

静的サイトジェネレーターのメリット

※ Netlifyなどホスティングサービスを用いた場合

  • レスポンスが速い。サーバーでHTMLを動的に生成しないから

  • サーバー代 ¥0✨ サーバーが必要ないため

  • サーバー落ちない。メンテが不要

    ※ ただしホスティングサービスが落ちる可能性はあります

  • headless CMS と相性が良い

    headless CMS・・・HTMLなどビュー機能がないCMS。コンテンツを管理する仕組みのみを持ち、APIのみ提供する

  • LPやブログなどに相性が良い

デメリット

  • 膨大なページ量のWebサイトには向かない

  • ビルドの時間がかかる。ページ数が多くなるほど遅くなる

  • 頻繁にデータ更新があるサイトに向かない

    データの更新のたびにビルドを走らせる必要があるため

  • API頻繁にリクエストするサイトには向かない

有名な静的サイトジェネレーター

400を超える数がある

過去2年で、フレームワークが成熟したらしく、代表的なものは以下↓

  • Gatsby

    これが一番有名、欧米で流行っている。

    React.js、Webpack、GraphQL、CSSなどで SPA を作成するのに最適

    Headless CMS、SaaSサービス、API、データベースなどに対応

  • Next, Nuxt

    Gatsbyが登場する前は、Next, Nuxt が主流だった

  • Hugo

  • Jekyll

2020年3月現在、静的ジェネレータ人気順が見れるサイト によると、Javascript 言語では Gatsby と Next.js が人気

Gatsbyの特徴

  • Reactベース

  • GraphQLと相性良い

  • Gatsby, Inc.(2015年設立)が開発、シリコンバレーにある

  • IBM、PayPal、Braun、Airbnb などが利用

静的サイトジェネレータは、もともとNextやNuxtのアイデンティティでしたが

Gatsbyの登場により静的サイトジェネレータは別にNextやNuxtじゃなくてもいいのでは?という風潮になってきた

  • Gatsbyプラグインが多数、npmで公開されていて以下のようなことができる

    TypeScript化, PWA対応, WordPress連携, Contentful連携,GA組み込み などなど その他はこちら参考

Jamstack(ジャムスタック)と Lampstack(ランプスタック)

Gatsbyに代表されるようなモダンなウェブサイトの仕組みのことを指すワードがここ1〜2年で注目されてきた

対義語としてLampstackがある

Jamstack

J ・・・ JavaScript

a ・・・ API

m ・・・ Markup

Webサーバーに依存しない、つまりサーバーレスであるウェブサイトのことをJamstackであると言える

Lampstack

L ・・・ Linux

a ・・・ Apache, Webサーバ

m ・・・ MariaDB・MySQL

p ・・・ PHP・Perl・Python

WordPress のようなサーバーサイドとクライアントサイドが密結合なウェブサイトをLampstackであると言える

Create React App, Nuxt (Next), Gatsby の使い分け

Create React App

すべてCSR(クライアントサイドレンダリング)SEOを捨てることになる

ブラウザベースのWEBアプリケーションに向いている

APIのレスポンスを待ってDOM描画するため、描画まで待ち時間が発生する

コーポレートサイトやLPには不向き

Nuxt (Next)

APIのレスポンス結果も含めてSSRするのでSEOは完璧。

データ更新が多い(APIレスポンスが頻繁に変わる)サイトに向いている

Gatsby

ビルド時点で静的なHTMLを生成する。SEOは完璧。描画や遷移が爆速。

ただし、頻繁なデータ更新には向かない。都度ビルドとデプロイが必要。

Create React App Nuxt Gatsby
SEO ×
頻繁なデータ更新
描画速度
LP ×
CMS系(e.g. ブログ, コーポレートサイト)
WEBアプリ(e.g. TODOアプリ) ×
大規模Webサービス × ×
サーバーコスト・メンテナンス ×

Gatsbyを試したサンプル

自分の環境で試したい方は

公式ドキュメントのクイックスタートで簡単に試すことができます

https://www.gatsbyjs.org/docs/quick-start/

gatsby-cli をインストールし、開発環境を作るとあらかじめディレクトリ、ファイルなどが用意されます

以下コマンドで開発環境を起動します

$ gatsby develop

少し触ってみたサンプル

遷移には <Link></Link> を使う

pjaxのような挙動をする

カーソルをリンク先に乗せた瞬間に、遷移先のhtmlをprefetchし、DOM書き換えとpushStateにより遷移

これにより爆速で遷移される

GraphQLを試す

起動後

http://localhost:8000/___graphql にアクセスすると、GraphQL Explorerが使える

GitHub GraphQL API と同様、ここでGraphQLにより取得できるデータを試すことができる

GraphQL を用いて.md からデータを取得する

gatsby-source-filesystem というプラグインを使い、ローカルファイルを取得することができる

さらに、gatsby-transformer-remark でマークダウンファイルを解析する。HTML形式へ変換して取得も可能。

$ yarn add gatsby-source-filesystem gatsby-transformer-remark

gatsby-config.js に以下を追記

    // ローカルファイルのデータをGatsbyに渡せるプラグイン
    {
      resolve: "gatsby-source-filesystem",
      options: {
        path: `${__dirname}/blog`,
        name: "blog",
      },
    },

/blog 以下のファイルをgraphQLで取得できるようになる

/blog/hello-world.md を追加

---
title: Hello World this is title.
date: "2020-04-01"
categories: []
---

これは、hello-world.md の本文です。

**太字**

## 見出し

Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。

Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。Hello World。

さらに、/pages/get-markdown.js を追加

import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"

const GetMarkdown = () => {
  // useStaticQuery は gatsby に用意されているメソッド
  // ビルド時にGraphQLでクエリすることができる
  // https://www.gatsbyjs.org/docs/use-static-query/#composing-custom-usestaticquery-hooks
  const data = useStaticQuery(graphql`
    query {
      allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
        totalCount
        edges {
          node {
            id
            html # 本文をHTMLに変換して取得する
            frontmatter {
              title
              date(formatString: "YYYY年MM月DD日")
            }
            excerpt # 本文抜粋
          }
        }
      }
    }
  `);

  console.log('data:', data);

  return (
    <Layout>
      <strong>投稿数 ( {data.allMarkdownRemark.totalCount} ) </strong>
      {data.allMarkdownRemark.edges.map(
        ({
          node: {
            id,
            html,
            frontmatter: { title, date },
            excerpt,
          },
        }) => (
          <div key={id}>
            <div>{date}</div>
            <h2>{title}</h2>
            {/* <p>本文抜粋:{excerpt}</p> */}
            <div dangerouslySetInnerHTML={{ __html: html }} />
          </div>
        )
      )}
    </Layout>
  )
}

export default GetMarkdown;

結果はこちら:https://gatsby-site-umamichi.netlify.com/get-markdown/

Contentful からデータを取得し、DOMに反映する

こちらも、 gatsby-source-contentful というプラグインを用いる

$ yarn add gatsby-source-contentful

gatsby-config.js に以下を追記

    // contenful からデータを取ってくるプラグイン
    {
      resolve: `gatsby-source-contentful`,
      options: {
        spaceId: '**********',
        accessToken: '**********',
      },
    },

spaceId accessToken はContentfulの管理画面から取得できる

今回はシンプルに以下のようなUserデータを作成

name age
john 5
umamichi 27

pages/contentful.js を作成

import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"

const Contentful = () => {
  const data = useStaticQuery(graphql`
    query {
      # contentful からデータを取得する
      allContentfulUser {
        edges {
          node {
            id
            age
            name
          }
        }
      }
    }
  `);

  console.log('data:', data);

  return (
    <Layout>
      {data.allContentfulUser.edges.map((
        {
          node: { id, name, age }
        }
      ) => (
        <h3 key={id}>
          {name} (age: {age})
        </h3>
      ))}
    </Layout>
  )
}

export default Contentful;

結果はこちら:https://gatsby-site-umamichi.netlify.com/contentful/

試したい方は、詳細な手順はこちらから。
https://qiita.com/ozaki25/items/cf7a0d9cc346e55469bc

ビルドしてNetlifyで公開する

ビルド

$ gatsby build

public ディレクトリにこのようにファイルが生成される

contentfulのデータなども一式組み込まれている

ちなみに、上記のサンプル含めたった4ページで初回ビルドにかかった時間は 27.9s

こちらはまだまだ改善の余地ありそう。というかぜひしていただきたい。

キャッシュされるようで、2回目以降のビルドは 15s くらいに落ち着いた。

Netlifyの公開手順は省略しますが、このあたりの記事を参考にしてみてください。5分で公開完了?

https://gatsby-site-umamichi.netlify.com/

まとめ

  • Gatsbyに代表される静的サイトジェネレーターによりwebpackで環境構築するコストが削減できる

    ページ遷移の爆速化、画像の最適化圧縮なども自動で行ってくれる

  • 開発するWebサイトの特性に合わせて、フレームワーク使い分けが重要

  • WordPressなどLampstackなCMSが淘汰される

  • Gatsbyの学習コストは肌感でjQuery習得くらい(Nuxtほど大変ではない)が、山のようにあるプラグインを使いこなせるかが重要

    保守されなくなるプラグインありそうなので、プラグイン選びは慎重にすべき

  • 頻繁にAPIリクエストするような動的Webサイト以外はすべて、Gatsby採用しても良いでしょう

  • HTMLコーダーに求められるスキルが React や Vue 込みになってくる

  • 日本は世界のトレンドから1年くらい遅れる傾向があるので2020年から静的サイトジェネレータがもっと普及していくと予想(期待)

参考

https://cloudlance-motio.work/post/static-site-generator-blog-3/

https://ferret-plus.com/9413

https://snipcart.com/blog/choose-best-static-site-generator

https://qiita.com/uehaj/items/1b7f0a86596353587466

https://qiita.com/hppRC/items/00739eaf9ae7fc95c1ca

https://note.com/erukiti/n/na654ad7bd9bb#PArTE

https://watablogtravel.com/cra-create-react-app-next-js-gatsby%E3%80%90-%E3%81%A9%E3%81%86%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9F%E3%80%91/#Nextjs

https://www.wikiwand.com/ja/LAMP_(%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%90%E3%83%B3%E3%83%89%E3%83%AB)

https://jp.techcrunch.com/2019/09/27/2019-09-26-gatsby-raises-15m-series-a-for-its-modern-web-development-platform/

https://qiita.com/Takumon/items/da8347f81a9f021b637f

https://qiita.com/okumurakengo/items/c34aa980afec9957a928

https://qiita.com/ozaki25/items/cf7a0d9cc346e55469bc

https://watablogtravel.com/cra-create-react-app-next-js-gatsby%E3%80%90-%E3%81%A9%E3%81%86%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9F%E3%80%91/

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

Reactのメインコンセプト要点まとめ(2)

この記事の位置づけ

React公式ドキュメントの「メインコンセプト」について、実際にReactの開発を行う中で特に参考になった章を要点の解説付きで纏めたものである。実際にReact公式ドキュメントを読み解いてもらうのが一番だが、時間がない人に要点だけでも読んでもらえるようにしたいと思い、纏めている。

公式のメインコンセプトでは、以下の章立てで説明をしている。
1. Hello World
2. JSXの導入
3. 要素のレンダー
4. コンポーネントとprops
5. stateとライフサイクル
6. イベント処理
7. 条件付きレンダー
8. リストとkey
9. フォーム
10. stateのリフトアップ
11. コンポジションvs継承
12. Reactの流儀

当記事では、以下の3章分について要点まとめをしている。
4. コンポーネントとprops
5. stateとライフサイクル
6. イベント処理

出典元資料

React公式ドキュメントのメインコンセプト

メインコンセプト

4. コンポーネントとprops

知っておくべきポイント

コンポーネントの種類

Reactのコンポーネントには「関数コンポーネント」と「クラスコンポーネント」の二種類がある。

関数コンポーネント

関数コンポーネントには以下の特徴がある。

  • JavaScriptの関数として定義する
  • propsを引数として受け取ることができる
  • stateは持たない
  • React要素を返す
function-comp.js
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
クラスコンポーネント

クラスコンポーネントには以下の特徴がある。

  • React.Componentクラスを継承したクラスとして定義する
  • propsを引数として受け取ることができる
  • stateを持つ
  • render()関数でReact要素のイミュータブルな置き換えを定義する
関数コンポーネントとクラスコンポーネントの使い分け

以下の基準で関数コンポーネントとクラスコンポーネントを使い分けるのがよいと考えている。

  • コンポーネントに状態を持たせる必要があるか?

    • 状態を持たせる必要がないなら、関数コンポーネントを使用する
    • 状態持たせる必要があるなら、クラスコンポーネントを使用する
  • リファクタリングでstateのリフトアップを実施した場合

    • 子のコンポーネントはstateを持たなくなるので、関数コンポーネントに変更する
    • 親のコンポーネントはstateを持つ必要があるので、クラスコンポーネントを使用する

関数コンポーネントは状態を持たないのでステートレスコンポーネント、クラスコンポーネントは状態を持つのでステートフルコンポーネントと一般的に呼ばれる。

stateのリフトアップでstateはできるだけ親に集約することを推奨している通り、ステートフルコンポーネントはできるだけ少なくし、基本的にステートレスコンポーネントでUIを構成することが望ましい。

ユーザ定義のコンポーネントを使用する

以下の例のように、Welcomeというコンポーネントを定義してレンダリングすることが可能。
ユーザ定義のコンポーネントを定義することで、コンポーネント単位のUI分割や再利用が可能となる。
※以下の例でもWelcomeコンポーネントを3回再利用している

welcom.js
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

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

React公式は、できるだけUI部品を小さなコンポーネントに分割することを推奨している。
その理由としては以下の通り。

  • コンポーネントが大きくなると、React要素内のネストが深くなるため変更が難しくなる
  • 個々のUI部品を再利用するのが困難になる

React公式は、経験則から以下のケースでコンポーネントの分割と再利用を検討すべきとしている。

  • 複数回使われるUI(ButtonPanelAvatarなど)
  • UI自体が複雑である場合(AppCommentなど)

コンポーネントのpropsは変更してはいけない

Reactのルールとして、すべてのReactコンポーネントは自身のpropsを変更してはいけないという取り決めがある。これは、Reactのコンポーネントは純粋関数でなければいけないというルールがあるためである。

純粋関数とは?

渡された値を変更せず、必ず同じ結果を返す関数を純粋関数と呼ぶ。

以下のような関数は、必ずa+bの結果を返すので純粋関数と呼べる。

sum.js
function sum(a, b) {
  return a + b;
}

対して、以下のような関数は渡された値を変更しているため、純粋関数とは呼べない。

withdraw.js
function withdraw(account, amount) {
  account.total -= amount;
}

一言コメント

React公式チュートリアルをやっただけでは、以下の2点は曖昧なままなのでやはりメインコンセプトは読んでおく価値があると思う。

  • Reactコンポーネントには関数コンポーネントとクラスコンポーネントが存在する
  • Reactコンポーネントは純粋関数でなければいけない

5. stateとライフサイクル

知っておくべきポイント

クラスコンポーネントの作成

Reactコンポーネントに状態を持たせる場合はクラスコンポーネントを使うというのは、「4. コンポーネントとprops」の章で紹介したとおり。
クラスコンポーネントは以下のような構成となっている。

clock.js
class Clock extends React.Component {  //ポイント1
  render() {  //ポイント2
    return (  //ポイント3
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock date={new Date()} />,
  document.getElementById('root')
);

ポイントは以下の3点。
1. React.Componentを継承するクラスを作成する
2. render()メソッドを定義し、returnでReact要素を返す
3. コンポーネントのpropsにはthis.propsでアクセスする

クラスコンポーネントにローカルのstateを追加する

上記のクラスコンポーネントには状態をまだ持たせていない。
コンポーネントの状態(ローカルのstate)を持たせると、以下のような構成となる。

clock.js
class Clock extends React.Component {
  constructor(props) {
    super(props);  //ポイント1
    this.state = {date: new Date()};  //ポイント2
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>  //ポイント3
      </div>
    );
  }
}

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

ポイントは以下の3点。
1. コンストラクタで親クラスにpropsを渡す
2. コンポーネントのローカルstate(this.state)に値を格納する
3. ローカルstateにはthis.stateでアクセスする

クラスコンポーネントのライフサイクルメソッド

多数のコンポーネントを有するアプリケーションの場合、コンポーネントが不要となったときにコンポーネントが保持しているリソースを開放することが重要。
それを実現するために、Reactのクラスコンポーネントはライフサイクルメソッドという仕組みを持っている。

clock.js
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};  //ポイント1
  }

  componentDidMount() {  //ポイント2
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {  //ポイント3
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

ポイントは以下の2点。

  • Clockコンポーネントのローカルstateに現在日付をセットしている
  • ClockコンポーネントがDOMとして描画された後にcomponentDidMount()メソッドが実行される
    • このことを、Reactではマウント(mounting)と呼ぶ
    • この例では、this.timerIDに1秒のタイマーをセットしている
    • タイマーが1秒過ぎるとtick()が実行され、ローカルstateの日時が更新される
      • これによって、ClockコンポーネントのDOMが描画されてから1秒後に日時が更新されることになる
  • Clockコンポーネントが生成したDOMが削除されるときにcomponentWillUnmount()メソッドが実行される
    • このことを、Reactではアンマウント(unmounting)と呼ぶ
    • この例では、this.timerIDの1秒タイマーをリセットしている
      • これによって、ClockコンポーネントのDOMが描画されるとタイマーがリセットされることになる

stateを正しく使用する

Reactコンポーネントのstateを扱うにあたって、いくつか知っておくべきことがある。

stateは直接変更しない

this.stateの値を直接変更してはいけない。直接変更すると、Reactは値の変更を検知できないためDOMが再レンダーされない。
this.stateの値を直接変更してよいのは、Reactコンポーネントのコンストラクタ内だけである。
(コンストラクタでthis.stateに初期値を設定する処理は再レンダーの必要がないため)

this.stateの値を変更したい場合、this.setState()を使用する。this.setState()による値の変更はReactが検知できるため、値の変更によってDOMの再レンダーが行われる。

ng.js
// this.stateを直接変更するのはコンストラクタ内以外ではNG
this.state.comment = 'Hello';
ok.js
// this.stateの値変更はthis.setState()で行う
this.setState({comment: 'Hello'});
stateの更新は非同期に行われることがある

Reactはthis.propsthis.stateを非同期で更新するため、stateの値変更がthis.propsthis.stateの値に依存していると意図しない動作になる恐れがある。

例えば、以下のケースではthis.state.counterの値変更がthis.state.counterの値とthis.props.incrementの値に依存している。
this.propsthis.stateは非同期で更新されるので、this.state.counterの加算が非同期で実行される前に、次の加算処理が行われる可能性がある。そうすると、処理結果が意図しないものになる恐れがある。

ng.js
this.setState({
  counter: this.state.counter + this.props.increment,
});

この問題を回避するために、this.setState()にはthis.statethis.propsの値を使用しての値変更を行う際に同期的に値変更を行う方法が用意されている。
以下のように、this.setStateに関数(第1引数にstate、第2引数にprops)を渡すことで、stateの値変更は値が確定している状態のstateとpropsが使われる。

ok.js
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));
単方向データフロー

いかなるstateも必ず特定のコンポーネントが所有しており、stateから生ずるすべてのデータやUIは、コンポーネントの親子関係における子のコンポーネント側にのみ影響するデータフローとなっている。
このようなデータフローは一般的には単方向データフローと呼ばれている。

コンポーネントのツリー構造において、React公式はデータフローのイメージを「propsをデータが流れ落ちる滝であるとすれば、各コンポーネントのstateは任意の場所から合流してくる追加の水源であり、それもまた下に流れ落ちていくもの」と表現している。
image.png

一言コメント

React公式チュートリアルをやっていれば、stateを直接変更してはならないことは知っているはず。
ただ、ライフサイクルメソッドという機能についてはチュートリアルだけでは知らないままなので、この機能は是非抑えておきたい。

6. イベント処理

知っておくべきポイント

Reactにおけるイベント処理

onclickなどのDOM要素のイベントと同様の動作を、Reactのイベント処理として設定することが可能。

DOM要素のonclickイベントでactivateLasers関数を実行する例
click.js
<button onclick="activateLasers()">
  Activate Lasers
</button>
ReactのonClickイベント処理でactivateLasers関数を実行する例
click.js
<button onClick={activateLasers}>
  Activate Lasers
</button>

JSXのコールバックにおけるthisの扱い

Reactのイベント処理でクラスメソッドを呼び出す場合、以下の3つの方法がある。

方法1:クラスコンポーネントのコンストラクタでthisを束縛しておく

コンストラクター内でthis.handleClickで使用するthisを束縛しておく。
ただ、この方法だとすべてのクラスメソッドについてthisを束縛しておく必要があるので、クラスメソッドの数が多い場合はあまり現実的ではない。

toggle.js
class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    this.handleClick = this.handleClick.bind(this);  //コンストラクタで各クラスメソッドにthisをbindする
  }

  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}
方法2:パブリッククラスフィールド構文を使用する(Create React Appの場合はデフォルトでこの書き方が可能)

クラスメソッドをアロー演算子の関数として定義しておくことで、thisを予め束縛しておくことが可能。
Create React Appでアプリを初期生成した場合、この構文はBabelで予め有効になっている。

logginbutton.js
class LoggingButton extends React.Component {
  handleClick = () => {  //handleClickメソッドをアロー演算子の関数として定義することでthisを束縛する
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}
方法3:Reactのイベント処理でアロー関数を使用する(パブリッククラスフィールド構文を使用していない場合はこの手法を取る)

JSX内のReactイベント処理をアロー関数で記述することで、thisを束縛した状態でクラスメソッドを呼び出すことができる。

loggingbutton.js
class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={() => this.handleClick()}>  //Reactイベント呼び出し時にアロー演算子を使用することでthisを束縛する
        Click me
      </button>
    );
  }
}

Reactのイベントハンドラに引数を渡す

Reactのイベント処理に対して、引数を渡すことができる。
第1引数は任意の値、第2引数はReactイベントを渡す。

click.js
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>

一言コメント

Reactによるイベント処理は、クリックしたときやフォーカスが外れたときの動作を定義したいときによく使う。thisの束縛については、Create React Appを使用しているのなら「パブリッククラスフィールド構文」を使用しておけば間違いないと思うが、thisはJSの理解を深めるために避けて通れないので、他の書き方についても理解しておいたほうがよいと思う。
また、Reactのイベントハンドラに引数を渡すのはよくあるので、これも書き方を覚えておいたほうがよい。

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

【React hooks】意外と知らないrefの使い方

Reactでコンポーネントから子コンポーネントや要素などを操作するときに便利なrefだが、
意外に調べても使い方が出てこなかったので、様々な利用シーンに合わせて使い道をまとめてみた。

DOMにアクセス

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

const Component = () => {
    const el = useRef(null);

    useEffect(() => {
        console.log(el.current);
    }, []);

    return (
        <div ref={el}>DOM</div>
    );
};

export default Component;

el.currentからdiv要素にアクセスできるようになる。
divの幅、高さを取ってきたり、D3などでDOMにグラフを描画する際に使用する。

子コンポーネントのインスタンスにアクセス

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

class Child extends React.Component {
    someFunc = () => {
        return 'sample';
    }

    render() {
        return <div> </div>;
    }
}

const Component = () => {
    const ins = useRef(null);

    useEffect(() => {
        console.log(ins.current);
    }, []);

    return (
        <Child ref={ins} />
    );
};

export default Component;

ins.currentから子コンポーネントChildのインスタンスにアクセスができる。
例えば、ins.current.someFunc()で子コンポーネントの関数を実行することができる。
※関数コンポーネント (functional component)ではインスタンスが作成されないため利用できない。

refのフォワーディング(forwardRef)

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

const Child = React.forwardRef((props, ref) => {
    return (
        <div ref={ref}>DOM</div>
    );
});

const Component = () => {
    const el = useRef(null);

    useEffect(() => {
        console.log(el.current);
    }, []);

    return (
        <Child ref={el} />
    );
};

export default Component;

el.currentで親コンポーネントから子コンポーネントを介してdiv要素にアクセスすることができる。
HOC(Higher-Order Component)などでコンポーネントを関数で囲む際などに、refをそのまま受け渡すという目的で利用する。
アプリケーションが複雑になればなるほど重宝する機能。

複数refs

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

const data = [0, 1, 2];

const Component = () => {
    const els = useRef([]);
    data.forEach((_, i) => {
        els.current[i] = React.createRef();
    });

    useEffect(() => {
        console.log(els.current);
    }, []);

    return (
        <div>
            {
                data.map((n, i) => {
                    return (
                        <div key={n} ref={els.current[i]} >{n}</div>
                    );
                })
            }
        </div>
    );
};

export default Component;

els.currentにはdiv要素の配列が格納されるようになる。
複数の要素にアクセスが必要なシーンで利用する。

応用編(複数Refs × 複数Refs)

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

const data = [0, 1, 2];

const ChildComponent = React.forwardRef(({ val1 }, ref) => {
    const els = useRef([]);
    data.forEach((_, i) => {
        els.current[i] = React.createRef();
    });

    useEffect(() => {
        ref.current = els.current;
        return () => {
            ref.current = null;
        };
    }, []);

    return (
        <div>
            {
                data.map((val2, i) => {
                    return (
                        <div key={val2} ref={els.current[i]}>
                            {val1}
                            -
                            {val2}
                        </div>
                    );
                })
            }
        </div>
    );
});

const Component = () => {
    const els = useRef([]);
    data.forEach((_, i) => {
        els.current[i] = React.createRef();
    });

    useEffect(() => {
        console.log(els.current);
    }, []);

    return (
        <div>
            {
                data.map((val1, i) => {
                    return (
                        <ChildComponent key={val1} val1={val1} ref={els.current[i]} />
                    );
                })
            }
        </div>
    );
};

export default Component;

els.currentには[[div,div,div], [div,div,div], [div,div,div]]みたいな感じで配列の配列でdiv要素が格納される。

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

フロントエンドフレームワーク mobx-state-tree の紹介

Qiita に mobx-state-tree記事が全然なくて寂しいので紹介記事を書こうと思います。

  • この記事ではReactと組み合わせて利用しています。
  • 最近のReactがわからないと読むのがつらいと思います。
  • サンプルコードの動作確認はしていません。雰囲気を掴むためだけのサンプルコードです。
    • 書いてたけど、情熱が尽きてしまった…

MobX の紹介

mobx-state-tree を紹介する前に、 MobX を簡単に紹介したいと思います。

MobXはすごーーーく簡単に言えば、モデルの値の変更に自動連動するReactコンポーネントを作ることができるフレームワークです。

MobXでカウンタを作ってみる

たとえば、状態の定義として MobX で下記のようなクラス(ストア)を定義しておき、

class Counter {
  // トラッキング可能な値の宣言
  @observable
  counter = 0;

  // トラッキング可能な値から算出される値の宣言
  @computed
  public get doubleCounter() {
    return this.counter * 2;
  }

  // トラッキング可能な値を変更するメソッドの宣言
  // 基本的に @action (@action.bound) 内でのみ値は変更可能
  @action.bound
  increment() {
    this.counter++;
  }
}

上記のストアを React (+mobx-react-lite) で下記のように使うことにより、カウンタを作ることができます。

const c = new Counter();

const CounterReact: React.FC<{}> = () => {
  // useObserver(...) 内でトラッキング可能な値の変化があった場合、コンポーネントを自動で再レンダリングしてくれる
  return useObserver(() => (
    <div>
      <p>現在のカウント: {c.counter}</p>
      <p>倍のカウント: {c.doubleCounter}</p>
      <p><button onClick={c.increment}>カウントを増やす</button></p>
    </div>
  ))
}

上記のようにオブジェクト指向風味にコードを書くだけで、クラスのプロパティなどの @observable / @computed で装飾された値/計算式の変化に勝手に追従するReactコンポーネントを作ることができます。

直感的にアプリのモデルを書けるのがいいですね。

ログインフォームを作ってみる

もうちょっと複雑な例を見ていきましょう。

class LoginForm {
  @observable
  loginId = "";

  @observable
  password = "";

  @observable
  message = "";

  @computed
  get submitDisabled() {
    // ログインIDもパスワードも両方入れないとログインさせないようにする
    return this.loginId.length === 0 || this.password.length === 0;
  }

  @action.bound
  setLoginId(value: string) {
    this.loginId = value
  }

  @action.bound
  setPassword(value: string) {
    this.password = value
  }

  @action.bound
  submitLogin() {
    loginApi
    .sendLogin({ loginId: this.loginId, password: this.password })
    .then(action(() => {
      // コールバック内は @action.bound が効いてないので、actionを呼ぶ
      this.message = "ログインしました";
    }))
  }
}

const f = new LoginForm();

const FormReact: React.FC<{}> = () => {
  return useObserver(() => <div>
    <p>ログインID: <input type="text" value={f.loginId} onChange={ev => f.setLoginId(ev.currentTarget.value)} /></p>
    <p>パスワード: <input type="text" value={f.password} onChange={ev => f.setPassword(ev.currentTarget.value)} /></p>
    <p><button disabled={f.submitDisabled} onClick={f.submitLogin}>ログイン</button></p>
    <p>{f.message}</p>
  </div>)
}

ログインIDとパスワードを両方いれたら送信ボタンが有効になる簡素なログインフォームです。

mobx-state-tree の紹介

mobx-state-tree は MobX の補助ライブラリです。

クラスでも、普通の変数でも、なんでもモデル(ストア)にできる自由すぎる MobX に対して、秩序あるモデル(ストア)作成方法を導入してくれます。

なぜ mobx-state-tree が必要か? MobX の問題点

MobX はそれはそれで素晴らしいのですが、いくつか問題点があります。

  • モデル(ストア) の作りが自由すぎる。基本的にはクラスで作ることが多いが、クラスで表現できることが多彩すぎる。
  • モデル(ストア)となっていたクラスのインスタンスは基本的に HMR (Hot Module Replacement) できない。
    • なにかファイルを更新した時にアプリの状態を引き継いだままHMRしてほしい。古いストアの値を新しいストアにコピーしたいけど、素直にはできない。
    • 結局はJSONになるReduxはこの辺は難なくできる。
  • Reduxみたいにアプリの状態をまるごと見たい。SSoT (Single Source of Truth)したい。
  • デコレータがウザい。JavaScriptにはないデコレータ機能を使いたくない。
  • 非同期処理の記述が難しい。

mobx-state-tree は、JavaScriptのクラスをストアとして使うのではなく、 mobx-state-tree が提供するライブラリでストアを構築させることにより上記を解決してくれます。

mobx-state-tree でカウンタを作ってみる

MobXで実装されたカウンタモデルの例を、mobx-state-treeの等価なモデルに変換してみましょう。

MobX
class Counter {
  @observable
  counter = 0;
  @computed
  public get doubleCounter() {
    return this.counter * 2;
  }
  @action.bound
  increment() {
    this.counter++;
  }
}

const c = new Counter();

↓↓↓↓

mobx-state-tree
const Counter = types.model("Counter", {
  // 初期値0の数値プロパティの宣言
  counter: types.optional(types.number, 0)
})
.views(self => ({
  get doubleCounter() { return self.counter * 2 }
}))
.actions(self => ({
  increment() { self.counter++ },
}))

const c = Counter.create();

(動的にモデルを定義するコードが出てきて不安になったと思いますが、上記はきちんと型安全に定義されます。TypeScriptの型推論ってすごいね!)

モデルを使うコードは特にMobXでもmobx-state-treeでも特に変わりありません。

const CounterReact: React.FC<{}> = () => {
  return useObserver(() => (
    <div>
      <p>現在のカウント: {c.counter}</p>
      <p>倍のカウント: {c.doubleCounter}</p>
      <p><button onClick={c.increment}>増やす</button></p>
    </div>
  ))
}

mobx-state-tree ならではのJSONとの相互変換機能を試してみる

mobx-state-treeの何が嬉しいかわからないので、mobx-state-treeならではの操作を紹介します。

c.increment();
c.counter // => 1

// 今のカウンタの状態をJSONとして抜き出す (getSnapshot)
const counterJson = getSnapshot(c); // → { counter: 1 }

c.increment();
c.increment();
c.counter // => 3

// カウンタの状態をJSONを与えて戻す (applySnapshot)
applySnapshot(c, counterJson);
c.counter // => 1

上記は、モデルの状態をJSONにしたり、逆にJSONからモデルに状態を反映させたりしています。

  • これはたとえば、HMRする時に、HMR前のアプリの状態を getSnapshot() で退避させておき、HMR後のアプリに applySnapshot() すれば、アプリ状態をHMR前後で引き継ぐことが可能ということを表しています。
  • また、getSnapshot() で取得したJSONを参照することにより、アプリの状態を一発で把握することもできます。

mobx-state-tree でログインフォームを作成してみる

前述したMobXの例は、下記のように書けます。

mobx-state-tree
const LoginForm = types.model("LoginForm", {
  loginId: types.optional(types.string, ""),
  password: types.optional(types.string, ""),
  message: types.optional(types.string, "")
)
.views(self => ({
  get submitDisabled() { return self.loginId.length === 0 || self.password.length === 0 }
}))
.actions(self => ({
  setLoginId(value: string) { self.loginId = value },
  setPassword(value: string) { self.password = value },
  submitLogin: flow(function * () {
    yield loginApi.sendLogin({ loginId: self.loginId, password: self.password });
    self.message = "ログインしました";
  })
}))

submitLogin に mobx-state-tree の非同期処理の書き方が現れています。 flow というmobx-state-treeが提供する関数に、awaitのかわりにyieldするジェネレータ関数を渡すことで、mobx-state-treeが非同期処理を面倒見てくれます。これは、結構楽です。

この手の非同期処理、たとえば、ページの初期ロード時にいろんなAPIをたたいて処理する場合、こんな感じに書くこともできます。

mobx-state-tree
const ProfilePage = types.model("ProfilePage", {
  // "init" | "loading" | "failed" | "loaded" のユニオン型で初期値が "init" という意味
  state: types.optional(types.enumeration(["init", "loading", "failed", "loaded"]), "init"),
  userEmail: types.optional(types.string, ""),
})
.actions(self => ({
  init: flow(function * () {
    self.state = "loading";
    try {
      const profileData: Profile = yield profileApi.getProfile();
      self.userEmail = profileData.email
      self.state = "loaded";
    } catch (exception) {
      self.state = "failed";
    }
  })
}))

const p = ProfilePage.create();

const ProfilePageReact: React.FC<{}> = () => useObserver(() => <div>
  <h2>プロファイルページ</h2>

  { p.state === "loading" && <p>データロード中...</p> }
  { p.state === "failed" && <p>失敗! <button onClick={p.init}>再チャレンジ</button></p> }
  { p.state === "loaded" && <p>あなたのメルアド: {p.userEmail}</p> }
</div>)

最近はReactコンポーネントの中に非同期処理などを書く例を目にする機会が多いのですが、私はビジネスロジック的なものは、原則すべてモデルの中に入れて、Reactはモデルの描画だけに使うほうが好きですね。(老害)

リアルワールドな例

もっとリアルワールドな例を見てみましょう。(色々省いているけど…)

import { ModelA } from "./ModelA"
import { ModelB } from "./ModelB"
import { ModelC } from "./ModelC"
import { ModelD } from "./ModelD"

// ルートとなるモデル: 他全モデルをプロパティとして持つ
const App = types.model("App", {
  a: ModelA,
  b: ModelB,
  c: ModelC,
  d: ModelD
}).views(省略).actions(self => {
  const init = () => {
    // アプリの初期化処理
  };

  return {
    init
  }
})

// アプリのインスタンスを作成して、初期化処理を開始する
const app = App.create();
app.init();

// 普通、ストアはコンテキストを経由して渡す
const ModelContext = React.createContext(app)
const useModel = () => useContext(ModelContext);
const useModelA = () => useModel().a

// コンテナ等React要素の定義
const AppElement: React.FC<{}> = () => {
  return <div>
    <FeatureA />
    <FeatureB />
    <FeatureC />
    <FeatureD />
  </div>
}

cosnt FeatureA: React.FC<{}> = () => {
  const a = useModelA();

  return useObserver(() => <div>
    {a.foobar}
  </div>);
}

// アプリのレンダリング
ReactDOM.render(<AppElement />, document.getElementById("root"));

基本的には mobx-state-tree でこういった全モデルのルートとなるモデルを作成して、Reactに描画させるようにする感じになるかと思います。

ということで

SPAの状態管理が楽になるので、 mobx-state-tree 使う人がもっと増えないかと思っています。私は仕事でバリバリ使っています。(有名どころだと、DAZNが使っているらしいです。他社事例

私ももともとはReduxを使っていたんですが、あのコード量に嫌気がさしてしまい(最近はそうでもないらしいですが)、MobXに流れ着き、そしてmobx-state-treeにたどり着いた経緯があります。

ちなみに、この記事では特に触れてないですが、

  • きちんとReactコンポーネントは分割しましょう
    • 特にコンテナとプレゼンテーショナルコンポーネントはわけましょう。
    • 普通はあんな野放図にモデルを参照するコンポーネントは書かないでしょう。
  • mobx-state-tree は、それなりに罠があります。
    • 初期の頃は非常にMobXに比べて重かったりとかありました。
    • TypeScriptのバージョンアップで結構型まわりが失敗しはじめることがよくありました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む