- 投稿日:2020-03-31T23:07:51+09:00
今更ながらReact Nativeに入門してみた①
はじめに
実は最近、React Nativeなるものに入門してしまいました...。
React Nativeとはなんなのか?といったところの説明から、実際に簡易的なTODOアプリを作成するところまでご紹介できればと思います。
では、早速行ってみましょう〜!
React Nativeとは何か
React Nativeとは、
Reactを使って、iOSやAndroidなどのネイティブアプリを開発することができるクロスプラットフォームな開発ができるフレームワークです。
公式サイトはこちら
なぜReact Nativeで作るか
個人的にメリットだと感じる点はいくつかあります。
- React(JavaScript)で書ける
- プラットフォームを意識せず開発できる
普段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アプリなので、必要なコンポーネントは下記の通りです。
- ユーザーの入力を受け付けるフォーム&ボタン
- 登録されたTODOを表示する場所
もっと細かくコンポーネント化できるかもしれません。
その辺りは追々身につけていきます笑
UIを作成する
次に、コンポーネントごとにまずは画面上に表示させてみましょう。
ExpoでiOSシミュレーターを起動させたら、自動でコードの変更を検知して更新されるような設定になっています。
コンポーネントは、ルートディレクトリ直下にcomponentsディレクトリを作成して、その下に配置していきます。
各コンポーネントの実際のコードは下記の通りです。
./components/Form.tsx
フォームの作成は、React NativeのTextInputを使用します。
また、ボタンはReact NativeのButtonを使用します。
ドキュメントはこちら
コンポーネントのソースは下記の通りです。
./components/Form.tsximport 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.tsximport 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アプリにするためには、
- 入力したTODOを登録できる
- 入力したTODOが一覧表示される
- 完了した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をある程度使えるようになったら、また記事にしてみようかなあ。
- 投稿日:2020-03-31T23:05:41+09:00
【React Hooks】グローバル状態管理にはContextAPIを使おう
Reactでグローバルな状態管理といえば
Redux
ですが、Context API
を使えば似たようなことができるので紹介します。まずは、グローバル状態を定義します。
Global.jsimport 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> ); };ここでは
GlobalContext
とGlobalProvider
の二つをexportします。次に、アプリケーションのルートにあたるファイル(./src/index.jsまたは./src/App.js)内でGlobalProviderを読み込ませます。
index.jsimport 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.jsimport 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
よりもこちらの方が便利だと思いました。
- 投稿日:2020-03-31T22:37:31+09:00
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
- 投稿日:2020-03-31T22:35:26+09:00
【備忘録】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/datetimepickerexpoを使う場合
expo install react-native-modal-datetime-picker @react-native-community/datetimepicker
利用例
App.jsimport 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を使うことでダークモードにも対応。他にもいろいろ属性あるので試してみてください。
何か良さげな使い方見つけたら更新しますのでチェックしててください!
それでは!
- 投稿日:2020-03-31T20:52:25+09:00
自動で伸縮する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.scrollHeight
をtextarea.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
- 投稿日:2020-03-31T20:31:16+09:00
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.tsximport 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.tsimport { 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.tsimport { 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点に修正を加えます。
actions/example.ts
(上に書いたsrc/index.tsx
と同じディレクトリ内に作る。reducers
やsagas
共同じディレクトリ)reducers/example.ts
components/App.tsx
(上に書いたsrc/index.tsx
でimportしているやつ)actions/example.tsimport 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.tsimport { 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 exampleReducerApp.tsximport 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/example
のgetData.started
)を経て、
exampleReducer
のType. STARTED_GET_DATA
からステイトが返却されます。ですがこのままではAPIへのリクエストを出していないので、
reducers/example.ts
のinitialState.startedGetData
がtrue
に変化するだけとなってしまいます。
(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.tsimport { 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.tsimport 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.ts
のgetDataSaga
の処理は
yield put(getFiles.succeeded(result))
まで進み、
reducers/example.ts
のSUCCEEDED_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
- 投稿日:2020-03-31T19:20:14+09:00
静的サイトジェネレーター 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://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://qiita.com/Takumon/items/da8347f81a9f021b637f
https://qiita.com/okumurakengo/items/c34aa980afec9957a928
- 投稿日:2020-03-31T17:56:18+09:00
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.jsfunction 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.jsfunction 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(
Button
、Panel
、Avatar
など)- UI自体が複雑である場合(
App
、Comment
など)コンポーネントのpropsは変更してはいけない
Reactのルールとして、すべてのReactコンポーネントは自身のpropsを変更してはいけないという取り決めがある。これは、Reactのコンポーネントは純粋関数でなければいけないというルールがあるためである。
純粋関数とは?
渡された値を変更せず、必ず同じ結果を返す関数を純粋関数と呼ぶ。
以下のような関数は、必ずa+bの結果を返すので純粋関数と呼べる。
sum.jsfunction sum(a, b) { return a + b; }対して、以下のような関数は渡された値を変更しているため、純粋関数とは呼べない。
withdraw.jsfunction withdraw(account, amount) { account.total -= amount; }一言コメント
React公式チュートリアルをやっただけでは、以下の2点は曖昧なままなのでやはりメインコンセプトは読んでおく価値があると思う。
- Reactコンポーネントには関数コンポーネントとクラスコンポーネントが存在する
- Reactコンポーネントは純粋関数でなければいけない
5. stateとライフサイクル
知っておくべきポイント
クラスコンポーネントの作成
Reactコンポーネントに状態を持たせる場合はクラスコンポーネントを使うというのは、「4. コンポーネントとprops」の章で紹介したとおり。
クラスコンポーネントは以下のような構成となっている。clock.jsclass 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.jsclass 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.jsclass 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.props
とthis.state
を非同期で更新するため、stateの値変更がthis.props
とthis.state
の値に依存していると意図しない動作になる恐れがある。例えば、以下のケースでは
this.state.counter
の値変更がthis.state.counter
の値とthis.props.increment
の値に依存している。
this.props
とthis.state
は非同期で更新されるので、this.state.counter
の加算が非同期で実行される前に、次の加算処理が行われる可能性がある。そうすると、処理結果が意図しないものになる恐れがある。ng.jsthis.setState({ counter: this.state.counter + this.props.increment, });この問題を回避するために、
this.setState()
にはthis.state
やthis.props
の値を使用しての値変更を行う際に同期的に値変更を行う方法が用意されている。
以下のように、this.setState
に関数(第1引数にstate、第2引数にprops)を渡すことで、stateの値変更は値が確定している状態のstateとpropsが使われる。ok.jsthis.setState((state, props) => ({ counter: state.counter + props.increment }));単方向データフロー
いかなるstateも必ず特定のコンポーネントが所有しており、stateから生ずるすべてのデータやUIは、コンポーネントの親子関係における子のコンポーネント側にのみ影響するデータフローとなっている。
このようなデータフローは一般的には単方向データフローと呼ばれている。コンポーネントのツリー構造において、React公式はデータフローのイメージを「propsをデータが流れ落ちる滝であるとすれば、各コンポーネントのstateは任意の場所から合流してくる追加の水源であり、それもまた下に流れ落ちていくもの」と表現している。
一言コメント
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.jsclass 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.jsclass 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.jsclass 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のイベントハンドラに引数を渡すのはよくあるので、これも書き方を覚えておいたほうがよい。
- 投稿日:2020-03-31T16:46:20+09:00
【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
要素が格納される。
- 投稿日:2020-03-31T00:01:41+09:00
フロントエンドフレームワーク 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の等価なモデルに変換してみましょう。
MobXclass Counter { @observable counter = 0; @computed public get doubleCounter() { return this.counter * 2; } @action.bound increment() { this.counter++; } } const c = new Counter();↓↓↓↓
mobx-state-treeconst 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-treeconst 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-treeconst 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のバージョンアップで結構型まわりが失敗しはじめることがよくありました。