- 投稿日:2020-07-08T18:32:47+09:00
kibana pluginを独立したReact Appとして開発する方法
はじめに
kibana plugin開発サイクルが非常に遅い問題
kibana pluginを開発していると、コードを修正して画面で確認、という基本的な開発サイクルに非常に時間がかかる問題にぶつかりました。
主な原因はkibana本体を含んだ開発環境にありそうだったので、それを解決する手段として、kibana本体から独立したアプリ(以下、独立用アプリ)とする方法を試し、劇的に速度改善したので、その備忘録です。
ゴール
開発サイクル上、kibana pluginがkibana本体に依存しているものとして
- ビルド環境
- Elastic UI framework
- Elasticsearchとの通信処理
がありますが、これらをkibana本体無しで動作させ、開発サイクルを速くするのがゴールです。
独立用アプリの構築方針
- すでに実装済みのkibana pluginアプリを動作する環境を構築する
- 上記アプリをkibana本体から引き剥がすのではなく、ゼロから開発環境を構築して同アプリが動くようにする
- 開発サイクルとしての想定は、ある程度出来上がるまで独立用アプリで開発し、まとまった段階で本来のkibana pluginのコードにマージして動作確認する
環境構築手法による差異が大きいと思われるため、細かい手順は端折って、考慮すべきポイントを残しておきます。
ビルド環境
Node.js + React.js
kibana pluginはNode.js(サーバ側)とReact.js(クライアント側)で構成されていますので、まず、その環境が必要になります。Reactに慣れてなかったので、この環境構築手段があれこれあって迷いました。
Next.js
今回は、開発環境だけで良くて(プロダクション環境等は不要で)簡単に上記環境が動かせる方法として、Next.jsを使って構築しました。
なお、今回のアプリは、Node.js側はElasticsearch APIを叩くだけの処理しかなかったので、ほぼReact.jsでの実装となっています。そこで、それっぽいNext.jsのsample api-routes-restを参考に構築を進めました。
クライアントサイドレンダリング
環境構築後、まるっとkibana plugin側のアプリコード(主に
public/
フォルダ)を移行したところ、ビルドで以下のようなエラーがでました。ReferenceError: window is not definedNext.jsは基本サーバサイドレンダリングになるで、クライアントでしか動かないコードはエラーになるわけですね。
解決手段
Next.jsはクライアントサイドレンダリング用の書き方を用意しています。
具体的なコードは以下です。
page/index.js// このままだとMainコンポーネントはサーバサイドレンダリングになる // import Main from "../public/components/main_component" // Mainコンポーネントをクライアントサイドレンダリングにする import dynamic from 'next/dynamic' const Main = dynamic( () => import('../public/components/main_component'), { ssr: false } ) export default function Index() { return <Main /> }これでビルド時エラーは無くなり、動作もしました。
※Mainコンポーネントとして読み込んでいる
main_component.js
は、アプリの起点になるファイルです。Elastic UI framework(EUI)の適用
kibana plugin(に限らずElastic関連サービス全般)のUI用に、Elastic UI framework が用意されています。kibana pluginではgeneraterコマンドを実行した時点で、すでに利用できる状態になっていますが、独立用アプリでは自分で用意します。
install
yarn add @elastic/eui # 他に利用しているcomponentによって追加する yarn add @elastic/datemathinstall後は、importで読み込んで利用します。
import
EUIをスタンドアローンで利用する方法は、using-eui-in-a-standalone-projectに書いてあります。
実際の方法は以下です。
pages/_app.js// darkテーマを選択 import '@elastic/eui/dist/eui_theme_dark.css'; (略)なお、
pages/_app.js
ファイルは新規で作成しました。このファイルはNext.js
アプリのカスタム用に使います。
また、元々使っていたcssも合わせて反映させるよう、_app.js全体を以下のようにしました。pages/_app.js// darkテーマを選択 import '@elastic/eui/dist/eui_theme_dark.css'; // kibana pluginで利用していたapp.scss import '../public/app.scss' // 上記対応しても反映されなかったstyleは、新規でファイル作成して対応 import '../public/additional.scss' function MyApp({ Component, pageProps }) { return <Component {...pageProps} /> } export default MyAppこれで、kibana plugin側と、サイドバーやタイトル部分を除いたplugin部分のみ、見た目が同じになりました。
※追加したadditional.scssについてはこちらElasticsearchとの通信処理
EUI同様Elasticsearch API用モジュールを入れて、通信処理を書くだけです。
install
yarn add @elastic/elasticsearch実装ファイルPath
元のkibana plugin側の実装時のAPI URIに合わせるため、ファイルを以下に設置しました。(ルーティング設定でも良いと思います。)
pages/api/<plugin name>/<api用root path>例)
pages/api/hoge_plugin/elastic_search/search.js開発サイクル速度の比較
環境ができたので、実際の速度比較です。
kibana plugin上の開発サイクルで遅かったのは3点
- clientコードのビルド
- ブラウザのリロード
- serverの起動/再起動
これらの速度を比較します。
比較
実施環境
- 実施マシン
- MacBook Pro (Retina, 13-inch, Early 2015)
- 2.7 GHz デュアルコアIntel Core i5
- 16 GB 1867 MHz DDR3
- 対象アプリ
- 主にReact.jsで構築された、データ可視化アプリ
結果
ちゃんと計測システムを入れたわけではないので、ざっくりです。
kibana plugin環境 独立用アプリ環境 コードのビルド 10〜15秒 3〜6秒 ブラウザのリロード 10秒 1秒(自動リロード) serverの起動 270秒 + 90秒(ブラウザ初回表示) 50秒(自動リロード含む) そもそもが遅すぎというのがありますが、 劇的に向上できました!
(というかwebアプリってこれぐらいの速度欲しいですよね。。。秒数にすると多少の差に見えますが、この1サイクルの差を、1日に数十、数百回繰り返すので、その差はとてつもなく大きくなります。
課題
サーバサイドでElasticsearchのAPIの結果を返すレスポンス処理が、コールバック内処理なので以下のようなメッセージが出る。
API resolved without sending a response for /api/(略), this may result in stalled requests.ただし、クライアントは期待通り動いているので、今の所問題ないとしてます。
実施バージョン情報
name version kibana 7.6.2 next 9.4.4 @elastic/eui 26.3.0 @elastic/elasticsearch 7.8.0 その他Tips
- Elasticsearch API ライブラリは、サーバサイドでのみ稼働する
- clientだけでは、Elasticsearch API接続できません。serverサイドの処理が必要
- kibana本体の環境変数設定ファイル.envを使っている場合はそれをコピー
- style
hidden
が効かなかったので、新規scssファイル追加して対応まとめ
- kibana pluginの開発サイクルが遅かった
- 独立したアプリとして切り出した
- 大幅な速度改善できた
- 投稿日:2020-07-08T17:38:42+09:00
[React] <Component {...props} /> なpropsの渡し方
tl;dr
タイトルのようなpropsの渡し方でも無駄に再レンダリングが起きることはない、ということの確認の記事。
code
スプレッド演算子でオブジェクトを展開すると「新しいオブジェクト」が生成される。なので、レンダリング最適化の観点で、無駄に再レンダリングが起きそうにも見える。
だが、実際は「新しいオブジェクト」が生成されるだけで、要素については新しい値なわけではない。
const func = () => console.log('hello world') const props = { func } const newProps = { ...props } props.func === newProps.func // => trueなので、レンダリング最適化されていれば、この渡し方でも無駄にレンダリングが起きることはない
// count が増加してもChildが再レンダリングされることはない const App = () => { const [count, increment] = React.useReducer(prev => ++prev, 0) const func = React.useCallback(() => console.log('hello world'), []) const props = { func } return( <div> <button onClick={increment}>increment({count})</button> <Child {...props} /> </div> ) } const Child = React.memo(({func}) => <button onClick={func}>CLICK_ME</button>)
- 投稿日:2020-07-08T14:04:10+09:00
未経験が独学でWEBアプリを作成する
はじめに
初めまして。rikuと申します。
qiitaに記事を投稿するのは初めてです。
この記事は、未経験からWEBエンジニアを目指して独学で作成したオリジナルアプリの実装内容や今までの学習方法の振り返りをしたいと思います。自己紹介
仕事で利用していた社内アプリに少々不満があり、もっと効率がよくなるアプリが作れたらと思いYouTubeでWebアプリを作成する動画をみたのがきっかけでプログラミングにはまりました。
若いうちにやりたい仕事につきたいと思い新卒から約8ヶ月で仕事を退職しWEBエンジニアを目指している23歳です。
成果物
一つのことを習慣化することに焦点を当てた、自分の成長を確認したり共有できるアプリです。
アプリ作成の目的
基礎的なHTML,Css,Javascriptを学んだ後に、Reactを4ヶ月Udemyや公式ドキュメントを何度も往復し基本的な書き方が身についてきました。
しかし、アウトプットをあまりできていなかったので今までの学習のアウトプットとして作成。開発環境
TypeScript
Typescriptを使ってみて型宣言や型注釈があることで開発効率と安全性を高められることや強力な入力補完が使えることを実感しました。
また、npmでインストールしたサードパーティ製のライブラリなどがTypescriptで書かれていることが多いのでそれらを読めるようになるのは非常にメリットだと感じられました。React
HTML,Css,JavascriptをUdemyなどで1ヶ月勉強した後、UIの実装などフロントエンドに興味がありReactを選択する。
Redux-Toolkit
Redux ToolkitではcreateSlice()という関数があり、これを使うと、初期値とaction creatorとreducerをまとめて作れます。
今までのReduxでのファイル管理で迷子になっていましたがこれを使うとDucksパターンで管理で初心者でも扱いやすく思いました。コードの書き方が冗長ですがcreateSliceを使うとユーザーに関する機能が一つのファイルで管理できました。
modules/usersimport { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { AppThunk, RootState } from '../store'; import { auth, FirebaseTimestamp, db, createRef, FirebaseFieldValue, } from '../../firebase/index'; import { flashMessage } from './flashMessages'; import { setInitialState } from './habits'; const usersRef = db.collection('users'); export type CurrentUserProps = { uid: string; username: string; email: string; created_at: any; updated_at: any; hasHabit: number; likeHabitCount: number; points: number; level: number; }; type UserState = { isSignedIn: boolean; isFetching: boolean; usersFetching: boolean; userFetching: boolean; userList: CurrentUserProps[]; currentUser: CurrentUserProps; user: CurrentUserProps; }; const initialState: UserState = { isSignedIn: false, isFetching: true, usersFetching: true, userFetching: true, userList: [], currentUser: { uid: '', username: '', email: '', created_at: null, updated_at: null, hasHabit: 0, likeHabitCount: 0, points: 0, level: 0, }, user: { uid: '', username: '', email: '', created_at: null, updated_at: null, hasHabit: 0, likeHabitCount: 0, points: 0, level: 0, }, }; export const usersSlice = createSlice({ name: 'users', initialState, reducers: { setCurrentUser: (state, action: PayloadAction<CurrentUserProps>) => { state.currentUser = action.payload; state.isFetching = false; if (action.payload) { state.isSignedIn = true; } else { state.isSignedIn = false; } }, signInSuccess: (state, action: PayloadAction<CurrentUserProps>) => { if (action.payload) { state.currentUser = action.payload; } state.isSignedIn = true; state.isFetching = false; }, signInFailure: (state) => { state.isFetching = false; }, signOutSuccess: (state, action: PayloadAction<CurrentUserProps>) => { state.isSignedIn = false; state.currentUser = action.payload; }, fetchUsersSuccess: (state, action) => { state.userList = action.payload; state.usersFetching = false; }, fetchUserStart: (state) => { state.userFetching = true; }, fetchUserSuccess: (state, action) => { state.user = action.payload; state.userFetching = false; }, }, }); export const { setCurrentUser, signInSuccess, signInFailure, signOutSuccess, fetchUserStart, fetchUsersSuccess, fetchUserSuccess, } = usersSlice.actions; const createUserDocument = async (user: any, addData?: any) => { const uid = user.uid; const userRef = usersRef.doc(uid); const snapShot = await userRef.get(); if (!snapShot.exists) { const { email, displayName } = user; const timestamp = FirebaseTimestamp.now(); const userInitialDate = { username: displayName, email: email, uid: uid, created_at: timestamp, updated_at: timestamp, hasHabit: 0, likeHabitCount: 0, points: 0, level: 1, ...addData, }; await userRef.set(userInitialDate); } return userRef; }; export const listenAuth = (): AppThunk => async (dispatch) => { return auth.onAuthStateChanged(async (user) => { if (user) { if (user.displayName) { await createUserDocument(user); } const uid = user.uid; usersRef.doc(uid).onSnapshot((doc) => { const data = doc.data() as CurrentUserProps; dispatch(signInSuccess(data)); }); } else { dispatch(signInFailure()); } }); }; export const signIn = async (email: string, password: string) => { return auth .signInWithEmailAndPassword(email, password) .then((result) => { const user = result.user; if (!user) { throw new Error('ユーザーIDを取得できません'); } }) .catch(() => { throw new Error('サインインに失敗しました。'); }); }; export const signUp = async ( username: string, email: string, password: string ) => { return auth .createUserWithEmailAndPassword(email, password) .then(async (result) => { const user = result.user; if (user) { await user?.updateProfile({ displayName: username, }); return await createUserDocument(user, { username }); } }) .catch(() => { throw new Error('アカウント登録に失敗しました。もう1度お試しください。'); }); }; export const signOut = (): AppThunk => async (dispatch) => { const userInitialState = { uid: '', username: '', email: '', created_at: null, updated_at: null, hasHabit: 0, likeHabitCount: 0, points: 0, level: 0, }; const habit: any = []; auth.signOut().then(() => { dispatch(signOutSuccess(userInitialState)); dispatch(setInitialState(habit)); }); }; export const levelUp = (): AppThunk => async (dispatch, getState) => { const { users } = getState(); const userRef = createRef('users', users.currentUser.uid); try { userRef .set( { level: FirebaseFieldValue.increment(1), }, { merge: true } ) .then(() => { dispatch( flashMessage(`レベルが上がりました!Habitをメニューから作成できます。`) ); }); } catch (error) { throw new Error(error); } }; export const fetchUsers = (): AppThunk => async (dispatch) => { try { const snapshots = await usersRef.limit(20).orderBy('level', 'desc').get(); const userList: any = []; snapshots.forEach((snapshot) => { const data = snapshot.data(); userList.push(data); }); dispatch(fetchUsersSuccess(userList)); } catch (error) { throw new Error(error); } }; export const fetchUser = (uid: string): AppThunk => async (dispatch) => { dispatch(fetchUserStart()); try { usersRef.doc(uid).onSnapshot((doc) => { const data = doc.data() as CurrentUserProps; dispatch(fetchUserSuccess(data)); }); } catch (error) { throw new Error(error); } }; export const selectUser = (state: RootState) => state.users; export default usersSlice.reducer;FireStore
フロントエンドの勉強に集中できるのと、以前にFlutterの環境構築をしていたのでモバイルアプリ開発の際に応用できそうと感じFirestoreを使用しました。
css
Material-Ui
Styled-Componentsデプロイ
Firebase Hosting
環境構築
create-react-app #myapp --template redux-typescript
設計
作成するアプリを決める
普段の1日の行動を見直して無駄にしている時間を改善できることをアプリにしようと思い、習慣化するアプリを作成することを決めました。
そこで他の習慣化のアプリを実際に使ってみて差別化とできそうなところを考え、一つのことを着実に習慣化することと自分の習慣化を可視化できるアプリを作ろうと決めました。
期限の設定
アプリ作成の期間を1ヶ月に設定しました。
独学だと明確な期限や納期などないですが、実務では納期までに実装していくことになると思うので、優先順位を決めて期間までにできるところを実装することにしました。画面設計
習慣化アプリのhabitifyやDribbbleのデザインを参考にしながら、手書きで大雑把に作成しました。
機能一覧
ログイン
- メールアドレス
- Google 認証ユーザー
- レベル機能
習慣を継続して行っているとポイントが溜まりレベルが上がり所持できる習慣の数が増えます。作成機能
- 習慣の作成機能
- 活動を振り返る機能
習慣ができているかの確認をします。できているとポイントがたまります。閲覧
- みんなの習慣を閲覧する機能
- ユーザーレベルのランキングの閲覧機能開発をして思ったこと
開発中に苦戦したことや開発をして感じた課題
開発できない時こそ実装方法やエラーの対処を考える。
仕事や移動中などで開発ができない時こそ、機能の実装方法やエラーで詰まったところをどのようにしてソースコード書くのかをいろいろな実装パターンを想像していました。
想像したソースコードを実際に実装してみるということを繰り返し行うことで開発時にどのように実装するかで時間を使う必要がなくスムーズに実装できました。自走力と公式ドキュメントを理解する
一番大事だと感じたのは、公式ドキュメントを理解する力だと思いました。
エラーの対処法や機能の実装方法というのは、調べれば解決に近い方法が見つけられるのですが、Redux-toolkitなどの新しめのライブラリは、Stack Overflowでも情報が少ないので何時間調べても解決しない時は、一度諦めることも大事だと感じました。
公式ドキュメントを見ると、ライブラリの基本的な使い方やiussesを確認すれば、エラーの解決方法も確認することができるので、公式ドキュメントで理解できることは開発効率の向上に繋がると思いました。期間までに実装できなかったこと
機能
チャット機能
習慣のいいね機能テスト
Jestを使ったアプリのテスト期間には間に合わなかったですが、Jestを使ったテストの勉強をしていこうと思います。
利用した学習サービス
学習のほとんどはUdemyとYouTubeと公式ドキュメントからでした。
Udemyでやって良かった教材
Udemyで10個ほど教材を購入しましたがその中でも特に良かったと思う教材を紹介します。
(1) 20 Web Projects With Vanilla JavaScript
https://www.udemy.com/course/web-projects-with-vanilla-javascript/HTML,Css,Javascriptを使ったタイピングゲームなどのミニアプリを20個構築します。
HTML,Cssがなんとなくわかってき後に、Javascriptを学ぶ時に、良さそうだと思います。
ミニアプリなので複雑にならず、Javascriptの配列操作、DOM操作、Apiとの非同期通信など基本的なことをアプリを作成しながら学べました。(2) React For The Rest Of Us
https://www.udemy.com/course/react-for-the-rest-of-us/Reactを学ぶ際に利用しました。比較的最近の教材でHooksを使ったFunction Componentでのアプリ設計が勉強できます。また、Create-react-appを使わず、webpackでReactをコンパイルする構築も学べます。
まとめ
コードを書いているときや、帰ってどういう実装をしようか考えている時、新しい技術を学んでいる時などオリジナルアプリを開発するのは、何より楽しいと感じました。
ただ、web系エンジニアのフロントエンドを目指していますが、まだまだ業界のことは勉強不足で、これからは業界や企業の情報収集もしていきます。
- 投稿日:2020-07-08T11:13:39+09:00
【React Native】 axios を使用し、一覧表示する。
axios
axiosとは、ブラウザやnode.js上で動くPromiseベースのHTTPクライアントです。
DBの情報をバックエンドのApiを叩いて取得する際にオーソドックスな方法になります。完成形
現在勉強をかねて、Instagramを模倣してアプリケーションを作成しています。
こちらの表示もバックエンドからaxiosを使用しデータを取得しています。使用技術
- expo
- ReactNative
- JSX
ファイルディレクトリー
---components | |---- Listitem.js (一つ一つの投稿をコンポーネント化) --screens | |---- HomeScreen.js (ホーム画面)ソースコード
HomeScreen.js
import React, { useState, useEffect } from 'react'; import { StyleSheet, View, FlatList } from 'react-native'; import ListItem from '../components/ListItem'; import axios from 'axios'; //* 今回はDBにFirebaseを使用しています。 const URL = 'https://firestore.googleapis.com/v1/projects/?????/databases/(default)/documents/????'; //* navigationは遷移させるときに必要になるものです。 export default HomeScreen = ({ navigation }) => { //* Hooks の導入 const [posts, setPosts] = useState([]); // * useEffect 導入 コンポーネントのマウント時に発火させるアクションを宣言 useEffect(() => { fetchPosts(); }, []); // * Axios getMethods const fetchPosts = async () => { try { const response = await axios.get(URL); const arrayPost = response.data.documents; setPosts(arrayPost); // console.log(arrayPost); dataの確認 } catch (error) { console.error(error); } } return ( <View style={ styles.container }> <FlatList data={ posts } renderItem={({ item }) => ( <ListItem item = {item.fields} userName={ item.fields.user_name.stringValue } userImage={ item.fields.user_image.stringValue } imageUrl={ item.fields.urlToImage.stringValue } content={ item.fields.content.stringValue } onPress={() => navigation.navigate('Article', { article: item })} /> )} /> </View> ); }Listitem.js
import React from 'react'; import { StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; const ListItem = ({userImage, userName, imageUrl, content, onPress}) => { return ( <View style={ styles.postWrapper } onPress={ onPress }> <View style={ styles.topBox }> <View style={ styles.topBoxLeft }> <View style={ styles.userImage }> <Image style={ styles.userImage } source={{ uri: userImage }} /> </View> <Text style={ styles.userName }>{ userName }</Text> </View> </View> <View style={ styles.middleBox }> <Image style={ styles.middleBox } source={{ uri: imageUrl }} /> </View> <View style={ styles.bottomBox }> <View style={ styles.bottomBoxArea }> <View style={ styles.bottomLeftArea }> <Icon name="heart-o" size={30} style={styles.icon1}/> <Icon name="comment-o" size={30} style={styles.icon2}/> {/* 詳細画面に遷移する */} <TouchableOpacity onPress={ onPress }> <Icon name="send-o" size={30} style={styles.icon3}/> </TouchableOpacity> </View> <View style={ styles.bottomCenterArea }></View> <View style={ styles.bottomRightArea }> <TouchableOpacity onPress={() => {//action}}> <Icon name="bookmark-o" size={30} style={styles.icon4}/> </TouchableOpacity> </View> </View> <View style={ styles.bottomTopArea }> <Text>{ content }</Text> </View> </View> </View> ) }ここでは、FontAwesome等を使用していますが、一旦そちらの説明は割愛致します。
HomeScreen.js 説明
ここで使用しているものは、
1) useState
2) useEffect
3) axios
4) FlatList1) useState
useStateはFunctionコンポーネントで記述する際に、状態の変化を受け取るために使用します。
現在推奨としてfunctionコンポーネントでの書き方のようなので、こちらでは使用しています。import React, { useState, useEffect } from 'react'; const [posts, setPosts] = useState([]); //からの配列に値を入るように設定2) useEffect
useEffectは、component が mount されたタイミングで api を呼ぶために使用しています。
ページが開かれた際に真っ先に読んで欲しいapiのため、こちらを使用します。import React, { useState, useEffect } from 'react'; const [posts, setPosts] = useState([]); // * useEffect 導入 コンポーネントのマウント時に発火させるアクションを宣言 useEffect(() => { fetchPosts(); }, []);3) axios
import axios from 'axios'; // * Axios getMethods const fetchPosts = async () => { try { const response = await axios.get(URL); const arrayPost = response.data.documents; setPosts(arrayPost); // console.log(arrayPost); dataの確認 } catch (error) { console.error(error); } }今回は、async function を使用しています。
非同期関数 — AsyncFunction オブジェクトである関数を定義します。非同期関数はイベントループを介して他のコードとは別に実行され、結果として暗黙の Promise を返します。ただし、非同期関数を使用したコードの構文および構造は、通常の同期関数と似たものになります。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function
こちらのtry以降でバックエンドから配列で[] 値を取得しています。
そしてそれをuseStateで作成したconst [posts, setPosts] = useState([]);第二引数のsetPost関数の中に格納します。
それにより、
第一引数であるpostsのなかにdataが格納されます。4) FlatList
return ( <View style={ styles.container }> <FlatList data={ posts } //先ほどのdataを指定 renderItem={({ item }) => ( <ListItem item = {item.fields} // こちらはFirebase特有に少し独特です... userName={ item.fields.user_name.stringValue } userImage={ item.fields.user_image.stringValue } imageUrl={ item.fields.urlToImage.stringValue } content={ item.fields.content.stringValue } onPress={() => navigation.navigate('Article', { article: item })} /> )} /> </View> );上記がHomeScreen.js でのAxiosの使用方法になります。
それをListitemコンポーネントに値を継承させ少しコードをすっきりさせています。React Native初心者になるため、何か間違い等あればご指摘お願いします!!!!
- 投稿日:2020-07-08T08:25:43+09:00
日本一わかりやすいReact-Redux入門#8~#10 学習備忘録
はじめに
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux入門』の学習備忘録です。
前回の記事はこちら。
要約
- Reduxファイルは、
reducksパターン
で管理すると、開発・運用・保守全ての面で効率的になるselectors.js
にセレクター関数を定義することで、Store内のstateの値を、任意のコンポーネントで簡単に参照・取得できる。- 外部API、DBとの通信時には、
redux-thunk
による非同期処理制御を入れる- Redux store からの state の取り出し方は「1.コンテナーコンポーネント」「2.
React-Hooks
」の二通りあるが、基本的には後者を採用するべき#8...re-ducksパターンでファイル管理をしよう
re-ducksパターンとは?
Redux 関連ファイルのディレクトリ構成パターン。ファイルの分割基準をルールとして決めてしまうことで、ファイルを管理しやすくする、というもの。
ディレクトリ構成
reducks ├ users ├ products ⋮state ごとにディレクトリを分けます。
ファイル構成
users ├ actions.js ├ index.js ├ operation.js ├ reducers.js ├ selectors.js └ type.jsstate の名前によらず、これら Redux 関連ファイルのファイル名は統一します。
各ファイルの役割をまとめます。
actions.js
Fluxフローにおける最初の窓口。アプリから受け取った state の変更依頼を受け取り、
reducers.js
に渡す。operations.js
actions.js
の前に実行したい「何らかの複雑な処理」を書くための場所(例:外部APIやDBから値を取得する、等)このファイルを用意することで、
actions.js
やreducers.js
の記述をシンプルかつ画一的に保つことができるようになる。次回動画以降登場。reducers.js
actions.js
からデータを受け取り、 Store の state をどう変更するか決める。types.js
Typescript 使用時のみ作成する。型定義を記述して export する。
selectors.js
Store で管理している state を参照する関数を定義して export する。
「どこに何を書くべきか」を決めておくことで、開発スピードアップだけでなく、保守・運用時の手間も最小化できそうです。
selectors.jsの使い方
今回は、
selectors.js
を作成して、state の参照を実行してみます。src/reducks/users/selectors.jsimport { createSelector } from "reselect"; const usersSelector = (state) => state.users; export const getUserId = createSelector( [usersSelector], state => state.uid )
getUserId
という関数を定義しています。これで、Store の中で管理されている state のうち、 users.uid を、任意のコンポーネントで参照・取得できます。早速、 Home.jsx で使ってみます。
src/templates/Home.jsximport React from 'react'; import {getUserId} from '../reducks/users/selectors'; import {useSelector} from 'react-redux' const Home = () => { const selector = useSelector(state => state); const uid = getUserId(selector); return ( <div> <h2>Home</h2> <p>{uid}</p> </div> ); }; export default Home
useSelector()
はReact Hooks
の一種で、Store 全体の state を受け取ります。。これをgetUserId()
に渡すことで、 uid を取り出せます。localhost:3000 をみてみると、
問題なく取得ができています(0000 は
initialState.js
で定義した user.uid)さらに、既に定義してある
signInAction
を用いて、Store 内の state を更新し、getUserId()
で正しく参照できるかを試してみます。src/templates/login.jsximport React from 'react'; import {useDispatch} from "react-redux"; import {push} from "connected-react-router"; import {signInAction} from "../reducks/users/actions" const Login = () => { const dispatch = useDispatch(); return ( <div> <h2>ログイン</h2> <button onClick={() => { dispatch(signInAction({uid:"0001", username: "torahack"})) dispatch(push('/'))}} > ログイン </button> </div> ); }; export default Login
<button>
をクリックすることでsignInAction
が発火し、state.user が更新されるはずです、。http://localhost:3000/login より、ボタンをクリックすると、
↓
無事、state.user.uid が更新が確認できます!
#9...redux-thunkで非同期処理を制御すべし
非同期処理とは?
時間のかかる処理
と並行して、次の処理を進めてしまうこと。
時間のかかる処理
とは、例えば外部APIとのリクエスト・レスポンスの処理や、データベースとの通信などを指す。通常の React では”時間のかかる処理”が完了する前に次の処理がどんどん進んでいきます(非同期で処理が進む)。しかし例えば「データベースへクエリを出し、返ってきた結果を Redux の Store に保存する」といった場面では、結果が返ってくるまでは処理を止めておかなければ、正常は画面描画を行えません。
redux-thunk とは?
React で非同期処理を制御するためのライブラリ。
actions.js
からreducers.js
へフローを渡すタイミングを制御できます。通常、redux-thunk の記述は、operations.js
に書くケースが多いです。redux-thunk を導入
store.js
に、redux-thunk を導入します。src/reducks/store/store.js⋮ import thunk from "redux-thunk"; export default function createStore(history) { ⋮ applyMiddleware( routerMiddleware(history), thunk ) ) }たった2行追加するだけでOK。次に、
operations.js
を追加します。src/reducks/users.operations.jsimport { signInAction } from "./actions"; import { push } from "connected-react-router"; export const signIn = () => { return async (dispatch, getState) => { const state = getState() const isSignedIn = state.users.isSignedIn if(!isSignedIn) { // 実際は以下にfirebaseと通信をするようなサインイン処理を書くが、 // まだ実装していないのでダミー処理を書く const url = 'https://api.github.com/users/deatiger' const response = await fetch(url) .then(res => res.json()) .catch(() => null) const username = response.login dispatch(signInAction({ // state.user に対する変更内容 isSignedIn: true, uid:"0002", username: username, })) // 上記処理後、ルートへリダイレクト dispatch(push('/')) } } }
async
と書くことで、await
(その処理が完了するまで次の処理に進まない)を使えるgetState()
で store から state を取得できる。dispatch()
で actions および push メソッドを使用できる。
if(!isSignedIn){...
以下は、本来であればバックエンド( fireabase など)との通信を行い、ユーザー認証の結果を action へ渡すことで、store 内の state の変更を行います。今回はバックエンド側は未実装なので、ダミーとして ユーザー名
deatiger
のgithub APIを叩く仕様にしています。正常に動けば、該当ユーザーのユーザー名を取得し、それを username に格納するよう、action へ命令を出します。この
operations.js
を使用できるように、templates に変更を加えます。今回はLogin.jsx
内のログインボタンを押した時にoperations.js
が発火し、記述した通りの state の変更がなされるように記述します。src/templates/Login.jsximport React from 'react'; import {useDispatch} from "react-redux"; import {signIn} from "../reducks/users/operations" const Login = () => { const dispatch = useDispatch(); return ( <div> <h2>ログイン</h2> <button onClick={() => dispatch(signIn())} > ログイン </button> </div> ); }; export default Login
<button>
タグの onClick イベントとして 先ほどのoperations.js
をセットしています。上手くいけば、 state.user の情報が更新されたのち、ルートへリダイレクトされるはずです。↓ ログインボタンをクリックすると、
operations.js
の記述の通り、uid が 0002 へ変更されています!ついでに、この画面(
Home.jsx
)で username も表示させてみます。templates ファイルでStore内のstateを取得するためには、selectors.js
で、 username を取得する関数を定義する必要があります。reducks/users/selectors.jsimport { createSelector } from "reselect"; const usersSelector = (state) => state.users; export const getUserId = createSelector( [usersSelector], state => state.uid ) // 以下追記 export const getUserName = createSelector( [usersSelector], state => state.name )state.user.username を取得する関数として
getUserName()
を定義します。src/templates/Home.jsximport React from 'react'; import {getUserId, getUserName} from '../reducks/users/selectors'; import {useSelector} from 'react-redux' const Home = () => { const selector = useSelector(state => state); const uid = getUserId(selector); const username = getUserName(selector); return ( <div> <h2>Home</h2> <p>{uid}</p> <p>ユーザー名: {username}</p> </div> ); }; export default Home
getUserName()
を import して使います。ブラウザで確認しましょう。↓ localhost:3000/login でボタンを押すと、
username も変更されています!
もし今回の処理を redux-thunk による非同期処理制御を入れなかった場合、github api よりユーザー情報を取得する処理の完了を待たずに action の発行に進んでしまうため、ユーザー情報がうまく画面が表示されなくなってしまいます。
「外部APIやDBとの通信を行う際には非同期制御を入れる」と覚えておけば、大体のケースには対応できそうです。
#10...コンテナーの役割
コンテナーコンポーネントとは
Store とコンポーネントの中継役。 Redux(Store) の世界と React(アプリ) の世界をつなぐ。
かつては更新された Store 内 state を アプリに渡すために唯一の手段でしたが、現在は
Redux-Hooks
を用いることでも Store 内 stateを渡せるようになりました。基本的には
Redux-Hooks
の方が記述が少なくて楽なため、コンテナーコンポーネントを使う場面は限られています。いつ使うべき?
明示的に state をフィルタリングしたいに使用します。
例えばユーザー認証情報など、セキュリティの関連から state を渡すコンポーネントを最小限に抑えたいときなどで、コンテナーコンポーネントがしばしば用いられます。
また、「
React Hooks
が登場する以前に書かれた React+Reduxコードを理解するために、知識としては持っておくべき」という観点も、学習するモチベーションと言えます。connect() の使い方
コネクトコンポーネントは
src/containers/
の下に保存します。今回は、
React Hooks
で実装していた、Login Component への state の引き渡しを、コネクトコンポーネントで実装してみます。実装ファイルは、以下の5ファイル。
1. src/containers/Login.js (コンテナーコンポーネント) 2. src/templates/LoginClass.jsx (クラスコンポーネントで実装したログインコンポーネント。コンテナーコンポーネントからの state を受け取るためには、関数コンポーネントではなく、クラスコンポーネントである必要がある) 3. src/containers/index.js 4. src/Route.jsx (/login の読み込み先を、LoginClassコンポーネントへ変更)src/containers/Login.jsimport LoginClass from '../templates/LoginClass' import {compose} from 'redux' import {connect} from 'react-redux'; import * as Actions from '../reducks/users/operations'; const mapStateToProps = state => { return { users: state.users // 渡したい state だけをオブジェクト型で記述 } } const mapDispatchToProps = dispatch => { return { actions: { signIn() { dispatch(Actions.signIn()) // Store から Dispatch する関数 } } } } export default compose( connect( mapStateToProps, mapDispatchToProps ) )(LoginClass)コネクトコンポーネントで state を渡されるコンポーネントは、クラスコンポーネントがある必要があります。Login Component をクラスコンポーネントで定義し直したものを用意します。
src/templates/LoginClass.jsximport React, {Component} from 'react'; export default class LoginClass extends Component { render() { return ( <div> <h2>ログイン</h2> <button onClick={() => dispatch(signIn())} > ログイン </button> </div> ) } }src/templates/LoginClass.jsximport React, {Component} from 'react'; export default class LoginClass extends Component { render() { return ( <div> <h2>ログイン</h2> <button onClick={() => this.props.actions.signIn()} > ログイン </button> </div> ) } }src/containers/index.jsxexport {default as LoginContainer } from './Login'src/Route.jsximport React from 'react'; import {Route, Switch} from "react-router"; import {Login, Home} from "./templates"; import {LoginContainer} from "./containers" const Router = () => { return ( <Switch> {/* <Route exact path={"/login"} component={Login} /> */} <Route exact path={"/login"} component={LoginContainer} /> <Route exact path={"(/)?"} component={Home} /> </Switch> ); }; export default Routerここまで実装することで、#9の最後と同じブラウザ表示を確認することができるはずです。
コンテナーコンポーネントによる実装は、
Redux Hooks
に比べ記述量が多く、かつファイル数も増えてしまいます。知識としては持っておくべきですが、特別な事情がない限りは
Redux Hooks
を使用すべきでしょう。おわり
今回記事を要点をまとめると、
- Reduxファイルは、
reducksパターン
で管理すると、開発・運用・保守全ての面で効率的になるselectors.js
にセレクター関数を定義することで、Store内のstateの値を、任意のコンポーネントで簡単に参照・取得できる。- 外部API、DBとの通信時には、
redux-thunk
による非同期処理制御を入れる- Redux store からの state の取り出し方は「1.コンテナーコンポーネント」「2.
React-Hooks
」の二通りあるが、基本的には後者を採用するべきです。
今回はここまで!次回からは実践編として、実際にECアプリの開発を通じた学習が始まる予定です。
- 投稿日:2020-07-08T02:19:32+09:00
ReactとFlaskをつなげる
create-react-appで作ったフロントとFlaskで作ったAPIをつなげる(サーバーをふたつ立ち上げる必要をなくす)方法の備忘録です。
こちらの動画を参考にさせていただきました。
Serving React with a Flask Backend前提
ツリー構造はこんな感じ。
react-flask-app/ ├─Flask-Backend | ├─static | ├─templates | └─app.py └─React-Frontend ├─public ├─src ├─package.json └─yarn.lockapp.pyはこんな感じ。
app.pyfrom flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run()Webpack解放
webpackの設定を弄れるようにする$ cd React-Frontend $ yarn eject $ Are you sure you want to eject? This action is permanent. (y/N) yReact-Frontendフォルダの下にconfigフォルダが追加されます。
react-flask-app/ ├─Flask-Backend | ├─static | ├─templates | └─app.py └─React-Frontend ├─config | └─jest | ├─env.js | ├─paths.js | ├─webpack.config.js | └─webpackDevServer.config.js ├─public ├─src ├─package.json └─yarn.lockpaths.js内で、buildフォルダを作成する場所のパスを
build
からFlask-Backend/static/react
に変更します。paths.js(省略) module.exports = { (省略) appBuild: resolveApp('../Flask-Backend/static/react'), (省略) }webpack.config.js内で、
static/js/[name].[chunkhash:8].js
->js/[name].[chunkhash:8].js
のように、ファイル名のパスがstatic/
から始まっているところを全て消します。
次に、HtmlWebpackPluginオブジェクト内にfilenameを追加し、Flask-Backendフォルダのtemplatesフォルダ下にindex.htmlをビルドするよう指定します。webpack.config.js(省略) plugins: [ new HtmlWebpackPlugin( Object.assign( {}, { inject: true, template: paths.appHtml, filename: '../../templates/index.html' <-- 追加 }, (省略) ) ) ]package.json内にhomepageの項目を追加し、buildフォルダ(ここではreactフォルダ)を指定します。
package.json{ "homepage": "/static/react", }ビルドして立ち上げる
yarn build
でビルドします。staticフォルダとtemplatesフォルダの下にビルド後のファイルが追加されます。react-flask-app/ ├─Flask-Backend | ├─static | | └─react | | ├─css | | └─js | ├─templates | | └─index.html | └─app.py └─React-Frontend ├─config | └─jest | ├─env.js | ├─paths.js | ├─webpack.config.js | └─webpackDevServer.config.js ├─public ├─src ├─package.json └─yarn.lockFlaskアプリを立ち上げる$ cd Flask-Backend $ python app.pyhttp://localhost:5000/ にアクセスします。
以上。
追記: ejectしない方法
yarn eject
してwebpackの設定を弄らずともつなげられるようです。React-Frontendに移動します。
yarn build
します。
React-Frontend内にbuildフォルダが追加されます。
app.py内でstaticフォルダとtemplatesフォルダのパスを指定します。app.pyfrom flask import Flask, render_template app = Flask(__name__, static_folder='../React-Frontend/build/static', template_folder='../React-Frontend/build') @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run()