20200708のReactに関する記事は6件です。

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 defined

Next.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/datemath

install後は、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の開発サイクルが遅かった
  • 独立したアプリとして切り出した
  • 大幅な速度改善できた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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>)

https://codepen.io/snamiki1212/pen/JjGLgEq

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

未経験が独学でWEBアプリを作成する

はじめに

初めまして。rikuと申します。
qiitaに記事を投稿するのは初めてです。
この記事は、未経験からWEBエンジニアを目指して独学で作成したオリジナルアプリの実装内容や今までの学習方法の振り返りをしたいと思います。

自己紹介

仕事で利用していた社内アプリに少々不満があり、もっと効率がよくなるアプリが作れたらと思いYouTubeでWebアプリを作成する動画をみたのがきっかけでプログラミングにはまりました。

若いうちにやりたい仕事につきたいと思い新卒から約8ヶ月で仕事を退職しWEBエンジニアを目指している23歳です。

成果物

オリジナルアプリ
github

一つのことを習慣化することに焦点を当てた、自分の成長を確認したり共有できるアプリです。

スクリーンショット 2020-07-08 10.54.28.png スクリーンショット 2020-07-08 10.55.58.png

アプリ作成の目的

基礎的な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/users
import { 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系エンジニアのフロントエンドを目指していますが、まだまだ業界のことは勉強不足で、これからは業界や企業の情報収集もしていきます。

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

【React Native】 axios を使用し、一覧表示する。

axios

axiosとは、ブラウザやnode.js上で動くPromiseベースのHTTPクライアントです。
DBの情報をバックエンドのApiを叩いて取得する際にオーソドックスな方法になります。

完成形

スクリーンショット 2020-07-08 10.33.24.png

現在勉強をかねて、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) FlatList

1) useState

useStateはFunctionコンポーネントで記述する際に、状態の変化を受け取るために使用します。
現在推奨としてfunctionコンポーネントでの書き方のようなので、こちらでは使用しています。

https://ja.reactjs.org/docs/hooks-effect.html

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();
  }, []);

https://ja.reactjs.org/docs/hooks-effect.html

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>
  );

https://reactnative.dev/docs/flatlist

上記がHomeScreen.js でのAxiosの使用方法になります。
それをListitemコンポーネントに値を継承させ少しコードをすっきりさせています。

React Native初心者になるため、何か間違い等あればご指摘お願いします!!!!

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

日本一わかりやすい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.js

state の名前によらず、これら Redux 関連ファイルのファイル名は統一します。

各ファイルの役割をまとめます。

actions.js

Fluxフローにおける最初の窓口。アプリから受け取った state の変更依頼を受け取り、 reducers.js に渡す。

operations.js

actions.js の前に実行したい「何らかの複雑な処理」を書くための場所(例:外部APIやDBから値を取得する、等)

このファイルを用意することで、actions.jsreducers.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.js
import { 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.jsx
import 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 をみてみると、
image.png

問題なく取得ができています(0000 は initialState.jsで定義した user.uid)

さらに、既に定義してある signInAction を用いて、Store 内の state を更新し、getUserId()で正しく参照できるかを試してみます。

src/templates/login.jsx
import 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 より、ボタンをクリックすると、
image.png

image.png

無事、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.js
import { 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.jsx
import 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 の情報が更新されたのち、ルートへリダイレクトされるはずです。

http://localhost:3000/login
image.png

↓ ログインボタンをクリックすると、

image.png

operations.jsの記述の通り、uid が 0002 へ変更されています!

ついでに、この画面(Home.jsx)で username も表示させてみます。templates ファイルでStore内のstateを取得するためには、selectors.jsで、 username を取得する関数を定義する必要があります。

reducks/users/selectors.js
import { 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.jsx
import 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 して使います。ブラウザで確認しましょう。

http://localhost:3000/
image.png

↓ localhost:3000/login でボタンを押すと、

image.png

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.js
import 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.jsx
import React, {Component} from 'react';

export default class LoginClass extends Component {
  render() {
    return (
      <div>
      <h2>ログイン</h2>
      <button onClick={() => dispatch(signIn())} >
        ログイン
      </button>
    </div>
    )
  }
}
src/templates/LoginClass.jsx
import 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.jsx
export {default as LoginContainer } from './Login'
src/Route.jsx
import 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アプリの開発を通じた学習が始まる予定です。

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

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.lock

app.pyはこんな感じ。

app.py
from 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) y

React-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.lock

paths.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.lock
Flaskアプリを立ち上げる
$ cd Flask-Backend
$ python app.py

http://localhost:5000/ にアクセスします。

以上。

追記: ejectしない方法

yarn ejectしてwebpackの設定を弄らずともつなげられるようです。

React-Frontendに移動します。
yarn buildします。
React-Frontend内にbuildフォルダが追加されます。
app.py内でstaticフォルダとtemplatesフォルダのパスを指定します。

app.py
from 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()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む