- 投稿日:2019-12-24T20:36:34+09:00
React で side effect のエラーハンドリング
この記事は CodeChrysalis Advent Calendar 2019 の記事です。
はじめに
ReactjsにはError Boundariesという、エラーをcatchしたときに専用のComponentをrenderしてくれる機能がありますが、これはrender時におけるエラーのみcatchする機能です。
componentDidCatch
というライフサイクルが用意されていて、これを以てサードパーティのエラー監視パッケージに通知するようにと推奨されています。ですが多くの場合、フロントエンドはバックエンドと通信する等のside effectを持ちます。
- side effectを用いたときのエラーハンドリングが必要となる
- side effectを用いたときもエラーメッセージを表示するComponentを共通化したい
- side effectを用いたときもサードパーティのエラー監視パッケージ(Sentryなど)に通知したい
という要件に対してのエラーハンドリング専用のComponentを考えました。
前提として、React Hooksを使います。
設計の内容
エラーが発生したときにContextを使ってエラーの情報を保存し、専用Componentでそれらの情報を使うようにしました。
コードの内容
Context
import React, { useState, useContext, createContext } from 'react'; const ErrorContext = createContext({ hasError: false, userMessage: null, error: null, setContextError: (userMessage: string, error: Error) => {}, setCotextErrorDone: () => {}, }); export const ErrorProvider: React.FC<object> = props => { const [hasError, setHasError] = useState<boolean>(false); const [userMessage, setUserMessage] = useState<string | null>(null); const [error, setError] = useState<Error | null>(null); const setContextError = (userMessage: string, error: Error) => { setUserMessage(userMessage); setError(error); setHasError(true); }; const setCotextErrorDone = () => { setUserMessage(null); setError(null); setHasError(false); }; return ( <ErrorContext.Provider value={{ hasError, userMessage, error, setContextError, setCotextErrorDone }} {...props} /> ); }; export const useError = () => { const context = useContext(ErrorContext); if (context === undefined) { throw new Error(`useError must be used within a ErrorProvider`); } return context; };要素の説明
hasError
はこれがtrue
になっているとErrorハンドリングのComponentをrenderするようにするためのstateです。userMessage
はエラーが発生した箇所でエラー内容が特定できるものに関してはその場でこのstateにエラーメッセージを保管し、ErrorハンドリングのComponentをrenderするときに使えるようにしています。error
はエラーが発生した箇所のtrycatch
で取得したerror
インスタンスそのものを入れています。Sentry
に送るためのものです。setContextError
でエラー発生時にエラー内容を保存し、hasError
のstateを変更します。setContextErrorDone
で、ErrorハンドリングのComponentからどこかに遷移するときにhasError
のstateをfalse
に変更するようにしています。import React, { useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import * as Sentry from '@sentry/browser'; import { Header } from 'components'; import { useUser } from 'context/UserContext'; import { useError } from 'context/ErrorContext'; import styles from './ErrorBoundary.module.css'; const ErrorBoundary: React.FC<RouteComponentProps> = ({ history: { push } }) => { const { userId } = useUser(); const { hasError, userMessage, error, setCotextErrorDone } = useError(); const [eventId, setEventId] = useState<string | null>(null); const handleHelp = () => { setCotextErrorDone(); setEventId(null); push({ pathname: '/somewhere' }); }; if (hasError && !eventId) { Sentry.withScope(scope => { if (attendanceId) { scope.setUser({ id: userId }); } scope.setExtras({ userMessage }); scope.setTag('errorCategory', 'Side Effect'); const currentEventId = Sentry.captureException(error); setEventId(currentEventId); }); } let title = 'エラーが発生しました'; if (hasError) { return ( <aside className={styles.mainContainer}> <Header /> <h1 className={styles.mainHeader}>{title}</h1> <h2 className={styles.subHeader}> <span>{userMessage}</span> </h2> <button className={styles.inquiryButton} onClick={handleHelp}>ヘルプ</button> </aside> ); } return null; }; export default withRouter(ErrorBoundary);要素の説明
- ErrorハンドリングのComponentを作成することで、Error時に表示するUIもカスタマイズで作成できます。
- このComponentがrenderされたらSentryにメッセージを通知するようにしています。以下のコードの部分です。
Sentry.withScope(scope => { if (attendanceId) { scope.setUser({ id: userId }); } scope.setExtras({ userMessage }); scope.setTag('errorCategory', 'Side Effect'); const currentEventId = Sentry.captureException(error); setEventId(currentEventId); });
- ヘルプボタンを押すと
handleHelp
を実行して、setContextErrorDone
が実行され、エラーに関するContextがクリアされると同時にどこかのURIに移動するようにしています。- 別のContextである
userUser
からuserId
を取得して、Sentryに送信し、もし問い合わせが来たらSentry上のエラーメッセージと関連付けて調査できるようにしています(Sentry便利!)let title = ...
の部分はもしErrorの内容によってタイトルを変えたいときのためにlet
にしています。Sentry
Sentryの設定で、Slackに送信するように連携しておけばこれらのメッセージをSlackに送信できます。
また、
scope.setUser({ id: userId });これは任意のユーザー属性を割り当てることができます。
scope.setExtras({ userMessage });これは補足情報を割り当てることができます。ここではユーザーにどのような情報を表示しているのかを把握するために割り当てています。
scope.setTag('errorCategory', 'Side Effect');Sentryの画面にタグを表示できるので、React純粋のError BoundaryとこのカスタムのBoundaryを切り分けています。前者はrenderのエラー、後者はSide Effectで十中八九バックエンドか通信周りが関係していると悟ります。
さいごに
もしより良いエラーハンドリングの構造があればぜひ教えてください!
- 投稿日:2019-12-24T20:33:54+09:00
これからはFunction Componentですべて解決できる――というのはどうやら幻想だったようです。
何がしたかったのか
Reactには、Lazy Componentというものがあります。
MyComponent.tsximport React, { FC } from 'react'; const MyComponent: FC = () => ( <div>Hello LazyComponent!</div> ); export default MyComponent;MyApp.tsximport React, { FC, Suspense, lazy } from 'react'; const MyComponent = lazy(() => import('./MyComponent')); const MyApp: FC = () => ( <div> <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> </div> ); export default MyApp;とすると、MyComponentのロードが完了するまでfallbackに設定された
<div>Loading...</div>
を代わりにレンダリングしてくれるというものです。で、いろいろ調べてたらこんなこともできると判明。
LazyComponent.jsimport React, { Component } from 'react'; let result = null; const timeout = (msec) => new Promise(resolve => { setTimeout(resolve, msec) }); const LazyComponent = () => { if (result !== null) { return ( <div>{result}</div> ) } throw new Promise(async(resolve) => { await timeout(1000); result = 'Done' resolve(); }) }; export default LazyComponent;こう書いたら、throwしたPromiseがresolveされたときにもう1回レンダリングされるらしく。私の探し方が悪いのか何なのか、この仕様はReactのドキュメント上で見つけることができませんでした。どこに書いてあるのか知っている人がいたらこっそり教えてほしいです。
それはさておきこの仕様、ドキュメントで見つからなかったので動かない前提で試しに書いてみました。
試しに書いたコードimport React, { FC, lazy, Suspense } from 'react'; const PromiseTest= lazy(async () => { let state = 0; const TestInner: FC = () => { if(state) { return ( <div>Done! {state}</div> ) } throw new Promise((res) => { setTimeout(() => { state = 5; res(); }, 5000); }); }; return { default: TestInner, }; }); const TestApp: FC = () => { return ( <div> <Suspense fallback={<div>WAITING...</div>}> <PromiseTest /> </Suspense> </div> ); }やってみた結果……動く!動くぞ!
さて、問題のコードに移ろうじゃないか
さて、Promiseをthrowしたら期待通りに動くことが分かったんですけれど。
state = 5
ってPromiseの中で変数に代入しちゃってるじゃないですか。ぶっちゃけキモいですよね。
useState
フックに置き換えてもいけるんじゃね?って思った私、置き換えてみました。置き換えてみたimport React, { FC, lazy, Suspense, useState } from 'react'; const PromiseTest= lazy(async () => { const TestInner: FC = () => { const [state, setter] = useState(0); if(state) { return ( <div>Done! {state}</div> ) } throw new Promise((res) => { setTimeout(() => { setter(5); res(); }, 5000); }); }; return { default: TestInner, }; }); const TestApp: FC = () => { return ( <div> <Suspense fallback={<div>WAITING...</div>}> <PromiseTest /> </Suspense> </div> ); }あれ、動かん
動かんぞ。
useState
に置き換える前は動いたコードが、置き換えた瞬間動かなくなりました。てゆうか、setter
は普通に呼ばれているはずなのに、state
の値は0のまま。なんでや、、、。諦めてComponent classにしてみた
というわけで、
PromiseTest
の実装をComponent classに変えてみました。state
をthis.state.state
、setter
をthis.setState
に変えただけですけどね。classに書き換えてみたclass TestInner extends React.Component<{}, { state: number }> { constructor(props: {}) { super(props); this.state = { state: 0, }; } render() { if(this.state.state) { return ( <li>Done! {this.state.state}</li> ); } throw new Promise((res) => { setTimeout(() => { console.log('resolved'); this.setState({ state: 5 }); res(); }, 5000); }); } } const PromiseTest = lazy(async () => { return { default: TestInner, }; }); const TestApp: FC = () => { return ( <div className='board-list-container'> <Suspense fallback={<div>WAITING...</div>}> <PromiseTest /> </Suspense> </div> ); }こうすると、動きました。動いてしまいました。え、なんでなんや、、、
……Function Componentが使えない極めてまれなケースの1つを発見した身としては、非常に頭が痛いです。こういう重要なことはもっとわかりやすくドキュメントに書いておいて下せぇ……。結論
React、なんもわからん。
- 投稿日:2019-12-24T19:57:57+09:00
[備忘録]Next.jsプロジェクト導入方法
個人の備忘録です。
Next.jsの導入方法
前提条件
Node.jsは導入済みとする。
方法
プロジェクトフォルダにpackage.jsonファイルを作成する。
内容は以下の通り"package.json"{ "scripts": { "dev": "next", "build": "next build", "start": "next start", "export": "next export" } }フォルダ内で以下のコマンドを実行する。
npm install --save next react react-domページ作成
プロジェクトフォルダにpagesフォルダを作成する.
このフォルダがWebページを配置しておくための場所になる。このフォルダ内にindex.jsファイルを作成する。
内容は下記の通りindex.jsexport default () => <div> <h1>Next.js</h1> <p>新しいプロジェクトです!</p> </div>プロジェクトの実行
プロジェクトフォルダ内で下記のコマンドを実行
npm run dev以上
- 投稿日:2019-12-24T14:57:18+09:00
作者直伝! Fluxフレームワーク Fleur 完全攻略ガイド
こんにちは〜 pixiv(VRoid Hub)のフロントエンドエンジニアでFleur開発者のHanakla(Twitter: @_ragg_) です!
React #2 Advent Calendar 2019 24日目となるこの記事では、モダンで小中規模くらいのフロントエンドのためのReact用Fluxライブラリ「
@fleur/fleur
」について作者としてダイマさせて頂きます!Fleurについては、今年5月にpixiv insideにてご紹介させていただきましたが、この時よりさらに改善されていますので、その点も含めて解説していきたいと思います!
(@ky7ieeeさんのTypescriptとReact HooksでReduxはもうしんどくないという記事が出ているところ恐縮なんですが、
typescript-fsa
… お前3年前に居てほしかったよ……!)話すこと
- VRoid Hubの実際の構成を基にしたFleurでのアプリケーション設計
- Redux設計パターン・Redux Style Guideとのかみ合わせ
(この規則はいいぞ、この規則は現実的じゃないぞなど)こういう時にFleurを使ってくれ
- とりあえず何も考えず最速で堅牢なそこそこスケールするSPAを組みたい!
- 「君が欲しい物 - Best Practice Remix -」がFleurにはあります。
- redux-thunk or redux-saga、reselect、typescript-fsa、Fleurには全部ある!
- Next.jsでも使える!!!
- Bless of Type(型の祝福)を気持ちよく受けながらSPAを作りたい
- コードの書き心地はReduxに頑張って型をつけるよりめちゃくちゃにいいです。これは自信を持って言えます。FleurはTypeScriptの型推論を受けるための本当に最小限のAPIを用意しています。
- ちゃんとテストしたい!
- FleurはOperations / Store / Componentのそれぞれに対してテストのし易いAPIを提供しています。
- 後述しますが、外部通信処理のDIも非常にシンプルに実装されています。
- ここらへんは
@fleur/testing
というライブラリにまとまっています。- やんごとなき事情でStoreにJSONじゃないオブジェクトとか副作用がど〜〜〜しても必要!
- 基本的にはJSONを使えというのはReduxと同じ方針だけど、リアルワールドにおいてはThree.jsのインスタンスや、レンダリングエンジンのインスタンスを状態管理に載せなくちゃならない場面があるんだ
世の中のSPAの8割くらいのケースを満たせる割と薄いFluxライブラリがここにある。少なくとも動画編集ソフトくらいまでなら動かせている。
さあ、いますぐ
yarn add @fleur/fleur @fleur/react
。Fleur - A fully-typed, type inference and testing friendly Flux framework
Fleurは2019年的なAPIで、書きやすく、より型に優しく、テスタブルに構成されたライブラリです。
Fluxible
やRedux
をベースにしていますが、基本的な設計に対して迷いや再実装が生じづらいようにAPIを設計しています。TypeScriptがある時代前提で設計されているので、Reduxで型にお祈りを捧げるときにありがちな
export enum ほへActionType
とかexport type なんとかActionTypes = ReturnType<typeof ほげ>
みたいなのを頑張って書く必要がありません。これが標準で提供されているの圧倒的にアドです。Redux初心者がtypescript-fsa
にたどり着くにはあまりにも超えなければいけない障壁が多すぎます。またNext.jsとの併用にも対応しており、
npx create-fleur-next-app
コマンドでNext.js + FleurによるSPA開発も可能です。(Next.jsなしのSSRでもご利用いただけます、ルーター用意されてます。)Redux devtoolsにもとりあえず対応しているため、デバッグツールにも困らないと思います。
使い方・設計編
それではFleurの使い方、基本的な設計パターンを見ていきましょう。ここでは「VRoid Hub」をFluerで実装するというケースでコードを例示していきます。
Fleurは大まかに↓の4つの要素を必要としています。オススメのディレクトリ構成
Fleurでは、Re-ducks パターン風のディレクトリ構成を推奨しています。Re-ducksパターンは弊社の色々なプロダクトでも割とよく採用されている構成です。
Fleurが推奨する構成は具体的には以下のようになります。
- app - spec -- ここにテスト用のモック関数とかを詰める - mocks.ts - components - domains - Character - operations.ts - actions.ts - selectors.ts - store.ts - model.ts -- フロント側でどうしても必要なビジネスロジックは関数化してここに置く - index.ts -- Re-ducksパターンではあるけど任意。暇なら置いてよい - operations.test.ts - selectors.test.ts - store.test.ts - model.test.ts - User - operations.ts - actions.ts - selectors.ts - store.ts - model.tsRe-ducksパターンでドメインを分けていくと、selectorやmodelがそのドメインに関係していないといけないことを強要出来るので、「汎用的な
utils.ts
にドメインロジックをアーーーーー????」みたいな事態を防げます。もっともこの構成は、あくまで中規模化したプロダクトに対しての推奨で、より小さなプロダクトやプロトタイピングには手間が多いと思います。最小構成で行くなら
domains/{Character,User}.ts
にActionとかOperationsとかをドメイン毎に全部詰めるみたいな構成でもいいでしょう。exportがごちゃごちゃしてなければ最低限の治安は保てるはずです。大規模アプリでどういう構成にしたらいいのかというのは今調べています。みなさんのプロダクトのディレクトリ構成とかフロントエンドアーキテクチャを語る記事を募集しています。
それでは各ファイルの中身を見ていきましょう
Operations
まずはOperation(アプリにおける手続き)を定義します。とりあえずキャラクター情報を取得してみましょうか。キャラクターにはキャラクターの情報(Character entity)とその投稿ユーザーの情報(User entity)があるものとします。
domains/Character/operationsimport { operations } from '@fleur/fleur' import { CharacterActions } from 'domains/Character/actions' import { UserActions } from 'domains/User/actions' import { normalize } from 'domains/normalize' import { AppSelectors } from 'domains/App/selectors' import { API } from 'domains/api' export const CharacterOps = operations({ // 特定のキャラクターの情報を取得する async fetchCharacter(context, characterId: string) { context.dispatch(CharacterActions.fetching.started, { characterId }) // 認証情報取る const credential = AppSelectors.getCredential(context.getStore) try { // APIからデータを取る const response = await context.depend(API.getCharacter)(credential, characterId) // Entityを正規化したりDateに変換したりは`normalize`でやったことにする const { user, character } = normalize(response) // 正規化したデータをStoreに送りつける context.dispatch(CharacterActions.charactersFetched, [ character ]) context.dispatch(UserActions.usersFetched, [ user ]) context.dispatch(CharacterActions.fetching.done, { characterId }) } catch (error) { rethrowIfNotResponseError(error) context.dispatch(CharacterActions.fetching.failed, { characterId, error }) } }, // 他のoperationの定義が続く })Operationで使うAPIと設計のコツを見てみましょう
context.getStore
- Storeのインスタンスを取れます。
context.getStore(CharacterStore)
のような感じで使いますが、selectorに任せてしまうので直接コールする機会はあんまりないかもしれません。context.depend
- 渡されたオブジェクトをそのまま返します。「は?」って感じですね。
これはテスト時にDependency Injectionを行うための仕組みです。後述します。- normalize - エンティティの正規化はOperationでやりましょう。純粋関数として切り出しておくとテストもしやすくて良いです。少なくともStoreで正規化するのはDRYじゃないのであまりおすすめしません…
context.dispatch
- Actionを発行します。normalizeの正規化単位
APIから振ってきたJSONは基本的にはEntity単位で切っていきます。
例えばVRoid HubのCharacter Entityは以下のような構造でAPIから降ってきます。interface SerializedCharacter { character_id: string name: string create_at: string /** 投稿者 */ user: { user_id: string name: string icon: SerializedUserIcon } }このJSONをDB的に分割すると
Character
とUser
とUserIcon
になります。
しかし、UserとUserIconは基本的にセットで使われているので、特に分割する必要がありません。なので分割せず、以下のような2つのEntityに正規化しています。interface Character { character_id: string name: string created_at: Date user_id: string } interface User { user_id: string name: string icon: SerializedUserIcon }Actions
次にActionsの定義です。Fleurにおいてこれはただの識別子と型宣言であり、Reduxと違ってこのActions自体はコールすることは出来ません。アプリケーションでどういうイベントが起きるかを宣言しているのみです。
domains/Character/actions.tsimport { actions, action } from '@fleur/fleur' import { CharacterEntity } from "./types"; export const CharacterActions = actions(/* Redux Devtools用の識別子 = */ 'Characters', { // action名: action<ペイロードの型>() charactersFetched: action<CharacterEntity[]>(), fetching: action.async< { characterId: string }, { characterId: string }, { characterId: string, error: Error } >(), })
fetching
とcharactersFetched
が並んでるのがモニョっとしますね。しかしCharacter Entityが降ってくるのはキャラクターをフェッチしたときだけとは限らないので、あくまでフェッチ状況を伝えるActionと、実際にフェッチされたEntityを伝えるActionを分けています。他のEntityを正規化した時にCharacter Entityが取り出されて、他のドメインから
charactersFetched
が起きたときにCharacterActions.fetching.done
するのが適切か?通信状態も一緒にごまかさないといけなくて設計がちょっと大変じゃない?という感じですね。
Action名は過去形を使うようにしましょう。Redux Style guideや@f_subalさんのスライド でも言及されていますが、Actionを受け取った側がどういう処理をすべきなのかが伝わりやすくなります。
ただ一点、Redux Style Guideで述べられている「一つのActionで全ての関係Reducerが反応するようにすべき」という点には一概に賛同していません。
実はそのような構造にすると、特に大規模なアプリケーションにおいて「あるActionによってアプリケーションで何が発生するのか」が人間的に予測しづらくなり、実は適度な粒度でActionを連投した方が処理の流れが自明になることがあります。(VRoid Hubではエンティティ種毎に
usersFetched
,charactersFetched
のようにactionを連投する形にしています。)特にFleurでどうしても仕方なくStore側で副作用を持っている場合は、どういう副作用を起こすかによってActionを切り分けた方がよさそうです。
またそれが推奨されている理由のもう一つに、ReduxではActionの連投はパフォーマンスに良くないというものがあるそうですが、FleurではStoreからの変更通知は
requestAnimationFrame
でバッファリングされているため、あまり気にしなくてよいです。Store
続いてStoreです。 Fleurにはclass-style StoreとreducerStoreがありますが、基本的に
reducerStore
の利用を推奨しています。こちらは副作用を持てないStoreなので、どうしてもStoreで副作用が必要なときはclass-style Storeを利用します。(class-style Storeの書き方はこちらをご参照ください。)domains/Characters/store.tsimport { reducerStore } from '@fleur/fleur' import { CharacterActions } from './actions' import { CharacterEntity } from '../CharacterEntity/types' interface State { characters: { [characterId: string]: CharacterEntity | void } fetching: { [characterId: string]: { fetching: boolean, error?: Error } } } export const CharacterStore = reducerStore<State>('Character', () => ({ characters: {}, fetching: {}, })) .listen(CharacterActions.charactersFetched, (state, characters) => { characters.forEach(c => state.characters[c.id] = c) }) .listen(CharacterActions.fetching.started, (state, { characterId }) => { state.fetching[characterId] = { fetching: true } }) .listen(CharacterActions.fetching.done, (state, { characterId }) => { state.fetching[characterId] = { fetching: false } }) .listen(CharacterActions.fetching.error, (state, { characterId, error }) => { state.fetching[characterId] = { fetching: false, error } })
reducerStore<State>(storeName, initialStateFactory)
でStoreを宣言します。
storeName
はアプリ内で一意の名前である必要があります。SSR時に吐き出すJSONの名前空間の識別に利用されますが、SSRなしの場合でも必須です。initialStateFactory
はStoreの初期状態を返す関数を渡します。ReducerStore.listen(action, callback)
でactionに対する処理を指定します。
state
を直接変更していますが、これはimmerでラップされたdraftオブジェクトなので、実際のstateはイミュータブルに変更されます。
- これは極めて強くて、特にReact Hooksとの組み合わせにおけるメモ化ではめちゃくちゃ楽にメモ化条件を設定することが出来ます。
- Store内で外部通信などの副作用を起こさないようにしてください。副作用はできるだけOperation層に集めてください。
Component
ではここまで書いてきたものをコンポーネントに繋いでいきます。
pages/character.tsximport React, { useCallback, useState, ChangeEvent } from 'react' import { CharacterSelectors } from 'domains/Characters/selectors' import { useStore, useFleurContext } from '@fleur/react' import { UserSelectors } from 'domains/Users/selectors' import { CharacterOps } from 'domains/Characters/operations' import { API } from 'domains/api' export const CharacterPage = () => { // URLからテキトーにキャラクターIDを取ってくる const characterId = 1 const { executeOperation, depend } = useFleurContext() const character = useStore(getStore => CharacterSelectors.getById(getStore, '1') ) const user = useStore(getStore => character ? UserSelectors.getById(getStore, character.user_id) : null ) const handleChangeName = useCallback( ({ currentTarget }: ChangeEvent<HTMLInputElement>) => { if (!character) return depend(API.putCharacter)({ name: currentTarget!.value }) executeOperation(CharacterOps.fetchCharacter, character.id) }, [character] ) if (!character || !user) { return <div>{/* いい感じのスケルトンを出す */}</div> } return ( <div> <h1> <input type="text" defaultValue={character.name} onChange={handleChangeName} data-testid="input" /> </h1> <h2> <a href={`/users/${user.id}`} data-testid="author"> {user.name} </a> </h2> </div> ) }ここで出てきたAPIを解説します。
useFleurContext
- Operationを実行するためのexecuteOperation()
, DIのためのdepend()
, Storeの値の遅延取得のためのgetStore()
が入ったオブジェクトを返します。
executeOperation(operation, ...args)
- 第一引数に渡されたOperationを実行します。depend(obj)
- objを取得します。テスト時にobjをモックに差し替えることが出来ます。getStore(Store)
- Storeのインスタンスを取得します。基本的にuseStoreで値をとってくるので余り使うことはないと思いますが、表示には関係ないけどStoreから値を取らないといけない場合に使います。コンポーネント内の属性に出てくる
data-testid
は、後述するテストで利用します。Selector
Selectorはこんな感じに用意してあげます。
domains/Characters/selectors.tsimport { selector } from "@fleur/fleur"; import { CharacterStore } from "./store"; export const CharacterSelectors = { getById: selector( (getState, id: string) => getState(CharacterStore).characters[id] ) }
- Component側のuseStoreでは
getStore
でしたが、selector内ではgetState
です
Store#state
を取得してくるためです。- Storeのインスタンス自体にアクセスする必要がある場合、
selector()
の代わりにselectorWithStore()
を使います。Bootstrap
最後にアプリの立ち上げ部分を書きます
app.tsximport React from 'react' import ReactDOM from 'react-dom' import Fleur from '@fleur/fleur' import { CharacterStore } from 'domains/Characters/store' import { UserStore } from 'domains/Users/store' import { FleurContext } from '@fleur/react' const app = new Fleur({ stores: [CharacterStore, UserStore] }) const context = app.createContext() window.addEventListener('DOMContentLoaded', () => { const root = document.getElementById('#root') ReactDOM.render( <FleurContext value={context}> <App /> </FleurContext>, root ) })
new Fleur
でFleurインスタンスを作ります
stores
オプションにアプリ内で利用しているStoreを全て渡します。Fleurでのテスト
ここからは今まで書いてきたOperation, Action, Componentに対してのテストを書いていきます。
Fleurのテストには@fleur/testing
というパッケージを利用します。
yarn add -D @fleur/testing
テストフレームワークにはjest (with ts-jest)を利用する想定をしています。
Contextのモック
まずテストのためのモックContextを作ります。
spec/mock.tsimport { mockFleurContext, mockStore } from "@fleur/testing" import { CharacterStore } from "domains/Characters/store" const baseContext = mockFleurContext({ stores: [ // ここにアプリで使われるStoreをこの形式で突っ込む mockStore(CharacterStore) ] }); export const baseOperationContext = baseContext.mockOperationContext() export const baseComponentContext = baseContext.mockComponentContext()OperationとStoreのテストで利用する
baseOperationContext
、Componentのテストで利用するbaseComponentContext
をexportしておきます。Operationのテスト
はい、ではまずOperationのテストをしていきましょう。
domains/Character/operation.test.tsimport { CharacterOps } from './operations' import { CharacterActions } from './actions' import { UserActions } from 'domains/Users/actions' import { API } from 'domains/api' import { baseOperationContext } from 'spec/mock' import { fakeRawCharacter } from 'spec/fakes/character' describe('CharacterOps', () => { it('キャラクターとユーザーのEntityちゃんと投げた?', async () => { const context = baseOperationContext.derive(({ injectDep }) => { // Storeの特定の状態を設定する場合は `deriveStore` をする // deriveStore(AppStore, { credentialKey: 'mock' }) // API.getCharacterをモックする injectDep(API.getCharacter, async (_, characterId) => fakeRawCharacter()) }) await context.executeOperation(CharacterOps.fetchCharacter, '1011') expect(context.dispatches[1]).toMatchObject({ action: CharacterActions.charactersFetched }) // expect(context.dispatches[1].payload).toMatchInlineSnapshot() expect(context.dispatches[2]).toMatchObject({ action: UserActions.usersFetched }) // expect(context.dispatches[2].payload).toMatchInlineSnapshot() }) })
injectDeps(元のオブジェクト, モックオブジェクト)
によって、Operation内で.depend(...)
しているオブジェクト(関数)をモックすることが出来ますaction.action
が関数なので一発でtoMatchInlineSnapshotしちゃうとちょっと信頼性に欠けます
context.dispatches
に発火されたActionの配列が入っているので、そのpayloadが意図した形になっているかどうかをチェックしていけばStoreのテスト
Storeもテストしていきましょう
domains/Characters/store.test.tsimport { CharacterStore } from './store' import { CharacterActions } from './actions' import { baseOperationContext } from 'spec/mock' import { fakeCharacter } from 'spec/fakes/character' describe('CharacterStore', () => { it('エンティティがちゃんと保存されるか', () => { const context = baseOperationContext.derive(({ deriveStore }) => { deriveStore(CharacterStore, state => { state.characters['10'] = fakeCharacter({ id: '10' }) }) }) const character = fakeCharacter() context.dispatch(CharacterActions.charactersFetched, [character]) expect( context.getStore(CharacterStore).state.characters[character.id] ).toEqual(character) }) })OperationContextからActionを投げて、意図したとおりのstateになっているかを検証します。
ここでは雑に1ケースしか書いてないですが、必要であればこの形式で書き足していきましょう。その際、テストケース毎に
baseOperationContext.derive()
で複製したcontextを使うことを推奨しています。
deriveは#operationのテストで書いたように、Storeの状態を派生させる事ができます。前提状態があるテストを書く場合に利用してください。const context = baseOperationContext.derive(({ injectDep }) => { // オブジェクトはshallow-mergeされる deriveStore(CharacterStore, { characters: { '10': fakeCharacter(); } }) // Deep-mergeしたい時はコールバックを使う deriveStore(CharacterStore, (state) => { state.characters['10'] = fakeCharacter() }) });Componentのテスト
最後にComponentのテストです。 コンポーネントのレンダリングには
@testing-library/react
を利用します。import { render, getByTestId, fireEvent } from '@testing-library/react' import { TestingFleurContext } from '@fleur/testing' import React from 'react' import { baseComponentContext } from 'spec/mock' // ... 略 describe('Character', () => { const mockedContext = baseComponentContext.derive(({ deriveStore }) => { deriveStore(CharacterStore, state => { state.characters['1'] = fakeCharacter({ id: '1', name: 'Haru Sakura', user_id: '2' }) }) deriveStore(UserStore, state => { state.users['2'] = fakeUser({ id: '2', name: 'Hanakla' }) }) }) // Case.1 キャラクターの情報ちゃんと出てる? // Case.2 APIリクエストちゃんと飛ぶ? })
baseComponentContext.derive
によりStoreの状態を設定したContextを生成します。
これを次のテストケースに食わせてあげると、特定の状態下で正しくコンポーネントが動くかをテストできます。Case1it('キャラクターの情報ちゃんと出てる?', async () => { const context = mockedContext.derive() const tree = render( <TestingFleurContext value={context}> {/* <なんとかRouter url='/characters/1'> */} <CharacterPage /> {/* </なんとかRouter> */} </TestingFleurContext> ) expect(tree.getByTestId('input').value).toBe('Haru Sakura') expect(tree.getByTestId('author').getAttribute('href')).toBe('/users/2') expect(tree.getByTestId('author').innerHTML).toBe('Hanakla') })
mockedContext.derive()
でテストケース用のContextを初期化し、TestingFleurContext Component
に与えます。これにより、内部のFleur Contextをモックすることが出来ます。あとは
[data-testid]
を元に要素を探して適切な値が出てるかを調べてるだけですね。
TestingFleurContext
で囲うということだけ覚えておいてください。次にAPIリクエストのテストをします。
Case2it('APIリクエストちゃんと飛ぶ?', async () => { const apiSpy = jest.fn(async () => void 0) const context = mockedContext.derive(({ injectDep }) => { injectDep(API.putCharacter, apiSpy) }) const tree = render( <TestingFleurContext value={context}> <なんとかRouter url='/characters/1'> <CharacterPage /> </なんとかRouter> </TestingFleurContext> ) fireEvent.change(tree.getByTestId('input'), { target: { value: 'Haru' } }) // Wait for request await new Promise(r => setTimeout(r)) expect(apiSpy).toBeCalledWith({ name: 'Haru' }) expect(context.executes[0]).toMatchObject({ op: CharacterOps.fetchCharacter, args: ['1'] }) })
- Operationのテストでも登場した
injectDep
がここでも登場します。 Componentから外部通信を起こすようなパターンの設計をしている場合、ここでdepend
/injectDep
を使うことで関数をモックすることが出来ます。- Componentから発火されたexecuteOperationの内容は`context.
裏話
Fleurの生まれ
Fleurは
Delir
というWeb製映像編集ソフトを開発していく過程で生まれました。当初は
Flux Utils
を使って構築されていましたが、直接的なStoreへの依存や、Action CreatorからStoreを触れないのはComponentが表示以上の責務を負わなければならないなどの問題があり、より適切な移行先を探す中でFluxibleにたどり着きました。Reduxはかなり要件のアンマッチがあり採用しませんでした。
- Storeに副作用が持てない
- JSONを扱うのが基本
- じゃあレンダリングエンジンのインスタンスはどこでもつのか? Component側か?アプリケーション全体のあらゆるイベントを受け付けないといけないのにComponent? React Context使ってインスタンス配っても状態変化の予測がつかない狂気になるだけじゃん?
- Storeに突っ込むのが保守性・予測性が良い。ならReduxは使えない…
- レンダリングエンジンが自身で副作用を発してもComponent側からはただのStoreの変化として扱える、合理的。
- 標準で非同期処理に対応していない
- 型付けのためだけのコードが多くてしんどい
- ActionNamesとActionの型定義が離れてるの果てしなくやりづらい
しかしFluxibleも2016年くらいのときには既につらみを抱えていました。TypeScriptの台頭です。
Fluxibleは上記の要件的には完全に一致しており、コードの読み書きにもほとんど困らないAPIでしたが、こと型をつけるという文脈ではかなり無理のあるAPIになっていました。(pixivFACTORYに所属していた時にFluxibleを採用し型定義を自前で行いましたが、相当しんどいものでした。)そこでFluxibleを元に、より現代的な設計パターンやコードの書きやすいAPIを取り入れたFluxフレームワークを作る、という目的でFleurが誕生しました。
- 型推論フレンドリー
- コードの書き心地を良くする
- React Hooks対応
- 非同期処理対応 (Fluxibleは対応済み)
- immer.js組み込みのStore
- 大体のケースで使ったほうが良い。 まだImmutable.jsが良いとかImmer.jsがES5で使えない思ってる人いないよね?
- パフォーマンス上のボトルネックにならない
- Reactの再レンダリングによって映像レンダリングのFPSが落ちる。UXに如実に影響を与えるのでこれは必要な要件だった。
さらに、本業で得たSSR周りや一般的なWebアプリケーションに対する設計に関する知識を取り込み、通常のWebアプリケーションやSSR環境でも利用可能なフレームワークとしてキメキメにしていきました。
そういう経緯があり、Fleurは2019年までのSPA用ライブラリとしてはn番煎じをキメ込んで非常にまとまりが良く保守性・汎用性の高いフレームワークに仕上がりました。
SSR対応
SSR要件もちゃんと気にかけており、SSRを行うデモアプリを運用して、Apache Benchで現実的じゃない量のアクセスをかけてメモリリークが起きていない(GCでちゃんとメモリが開放されること)を確認したりしてします。
今回のサンプルにも載せられなかったんですが、ルーターも用意されています。
最後に
言い訳させてください! 完全攻略ガイドと言っておきながら体調不良によりNext.jsとかSSRとかに結局触れられませんでした!そこらへんは後日別記事でやるかもしれません(この記事のウケがよかったらね)
本記事で書いたコードはra-gg/advent-calendar-fleurにまとまっています。(実は雰囲気コードなので全部のテストは通ってないんだけどね)
とりあえず自分の中では2019年までのSPA設計のまとめとしてはいい感じのフレームワークになっているかなと思います。残念ながら社内プロダクトでの採用の機会はまだないんですけどね〜! 来年は2020年なのできっとさらに色々良くなったFleurをお見せできるかと思います!
もしよろしければ、「雑にFleur使ってみた」「作者が怠惰だからオレがFleur完全攻略してみた」など、Fleurに対する感想、不満、オレの設計語りなどインターネットに放流してください! Fleur開発に対するモチベーションと設計の洗練度が上がるので! Twitterハッシュタグは #fleurjs GitHub Topic / Qiita tagは fleur-js です!
Q & A
2019年的?
2019年的です。2020年的ではないという意味でもあります。色々情報を集めてはいますがまだよくなれる余地がありそうな気がしています。マイグレーションしやすいことは考えていますが、Breaking changeは入れていくと思います。
Next.jsで使える?
使えます。いますぐ
yarn create fleur-next-app <your-app-name>
しましょう。
本記事で解説したのとほぼ同等の構成のファイル郡でアプリ開発をおっ始める事ができます。そのうちNext.js + Fleurでアプリを作る記事でも書こうかと思います。
画面毎にStoreを分けるのどう思う?
いいと思う。ただその場合この記事で解説したdomain毎の分割だけだと治安が悪くなるので、page毎にStoreを入れるディレクトリを上手く分ける必要はありそう。ただし現バージョンのFleurはあまりそれがやりやすい構造になってないので、それについては今後の課題かなと思っています。
useReducer
を薄〜くラップしたライブラリがあるとそういう「アプリ全体には影響ないけど、ある特定のページ以下でめっちゃグローバル」みたいなやつを切り出せるんだよな〜とも思っています。これも考えてる。既にいくつかそれらしいライブラリがあった気もしている。なんでOperationとActionが別れてるの?
ActionとOperationが1ファイルに混じってごちゃごちゃするのが嫌だったのと、Re-ducksパターンによる影響と、実際ActionとOperationってそんなに綺麗に一対一になるか?という気持ちに決着がつけられていないためです。typescript-fsaはその点すごくシンプルですね。ActionとOperationが透過的に扱えるRedux故の特徴です。 Fleurも何かしら考えたほうがいいよな〜とは思っています。
middlewareない?
ない。middlewareを入れると実装の見通しが悪くなるので、簡単に入れられる仕組みを入れたくない。どうしても必要だったらcontextをラップしてくれ。(Redux DevTools対応はその方式でやってる)
const yourMiddleware = (context: AppContext) => { const executeOperation = context.executeOperation const dispatch = context.dispatch context.executeOperation = (op, ...args) => { // なにがしの処理をする context.dispatch(MiddlewareActions.hoge, {}) return executeOperation(op, ...args) } context.dispatch = (action, payload) => { if (action.name.indexOf('.started')) { // ローディングを出したりする } return dispatch(action, payload) } return context } const app = new Fleur(); const context = yourMiddleware(app.createContext())Fleurのselectorはmemoizeしてる?
してない。 関数一発でmemoizeしてもキャッシュヒット率がたかが知れてるのでしてない。
代替案としてはReactの
useMemo
を使う形に寄せて欲しい。useMemoならコンポーネントの文脈に沿ったよりヒット率の高いmemoizeができる。reselectのコードを読んだことがあればわかると思うけど(ここらへんな)、あるselectorが複数回異なる引数で呼ばれるような場合、reselectのmemoizeはまともに機能しないっぽく見える。ある引数でmemoizeしても次に異なる引数でselectorが呼ばれればキャッシュは破棄される。そして真面目にプロダクトコード書いてたらこれは普通に発生する。
EcmaScriptのRicher keys - compositeKeyが入ればWeakMapで今よりはマシなmemoize機構を作れそうだけど、FleurもReduxも別々の問題でキャッシュヒット率には難がありそうな予感がしています。
なんでComponentからdispatch出来ないの?
dispatch
はStoreに対するかなり低レイヤーな操作です。もしこれをComponentから使えるようにしてしまうと、アプリの構造化が属人的になりやすくなってしまいます。(どこからどういう粒度でdispatchするかを考える余地が生まれてしまう)とはいえ昨今のイケてるフロントエンドを見ているとReact Suspenseとか
useAsync
などを使ってComponent側から通信を起こすケースがありがちなので、そろそろ許しても良いかもしれない…けどアプリが大きくなってもそれを続けられるのか…?小規模アプリだから許されてるのでは…? そもそもその手のアプリはFluxライブラリ使ってないんじゃ…? というところで悩んでいます。俺はVue派なんだが?
Vue版も作ろうか悩んでる、やればVuexより型は強くなる。ただ内部事情として
immer.js
とVueは相性が悪いとかいう次元じゃないのでFleurの書き直しかMutableStoreの対応が必要になる。
あとMobxとかVuexとか色々見ていて「Fleurは本当にこのままの設計でいいのかな〜?」という気持ちもあるのでそこらへん全てに踏ん切りがついたらやるかもね(たぶんVueの人たちVue公式以外のもの使いたがらなさそうという気がするので暇すぎてひまわりになったら?やる)
パフォーマンスどんなもん?
ちょっと極端なケースで計測しているのですがFleur vs Reduxはこんな感じです。(masterの最新コミットでの比較)
Fluxibleも計測してるんですがあまりにも遅いしwarnが多いので省きました。
なんで君のサンプルコード
export default
してないの?
- Named exportしておくとVSCodeのimport候補に出やすくなるから
- 後から「あっこの名前よくなかったわ」って時にVSCodeの自動リファクタリングで一発で名前を変えたいから
(default importされたやつは名前変更で一発で変わってくれない)- default importした時に、人によってimport物にどういう規則で名前つけるか揺れてほしくないから
- import名に対してコーディング規約を考えるのしんどいから最初から適切な名前ついていてホシイ ? ?
- 投稿日:2019-12-24T14:57:18+09:00
2019年時代のFluxフレームワーク “Fleur” 完全攻略ガイド【作者直伝】
こんにちは〜 pixiv(VRoid Hub)のフロントエンドエンジニアでFleur開発者のHanakla(Twitter: @_ragg_) です!
React #2 Advent Calendar 2019 24日目となるこの記事では、モダンで小中規模くらいのフロントエンドのためのReact用Fluxライブラリ「
@fleur/fleur
」について作者としてダイマさせて頂きます!Fleurについては、今年5月にpixiv insideにてご紹介させていただきましたが、この時よりさらに改善されていますので、その点も含めて解説していきたいと思います!
(@ky7ieeeさんのTypescriptとReact HooksでReduxはもうしんどくないという記事が出ているところ恐縮なんですが、
typescript-fsa
… お前3年前に居てほしかったよ……!)話すこと
- VRoid Hubの実際の構成を基にしたFleurでのアプリケーション設計
- Redux設計パターン・Redux Style Guideとのかみ合わせ
(この規則はいいぞ、この規則は現実的じゃないぞなど)こういう時にFleurを使ってくれ
- とりあえず何も考えず最速で堅牢なそこそこスケールするSPAを組みたい!
- 「君が欲しい物 - Best Practice Remix -」がFleurにはあります。
- redux-thunk or redux-saga、reselect、typescript-fsa、Fleurには全部ある!
- Next.jsでも使える!!!
- Bless of Type(型の祝福)を気持ちよく受けながらSPAを作りたい
- コードの書き心地はReduxに頑張って型をつけるよりめちゃくちゃにいいです。これは自信を持って言えます。FleurはTypeScriptの型推論を受けるための本当に最小限のAPIを用意しています。
- ちゃんとテストしたい!
- FleurはOperations / Store / Componentのそれぞれに対してテストのし易いAPIを提供しています。
- 後述しますが、外部通信処理のDIも非常にシンプルに実装されています。
- ここらへんは
@fleur/testing
というライブラリにまとまっています。- やんごとなき事情でStoreにJSONじゃないオブジェクトとか副作用がど〜〜〜しても必要!
- 基本的にはJSONを使えというのはReduxと同じ方針だけど、リアルワールドにおいてはThree.jsのインスタンスや、レンダリングエンジンのインスタンスを状態管理に載せなくちゃならない場面があるんだ
世の中のSPAの8割くらいのケースを満たせる割と薄いFluxライブラリがここにある。少なくとも動画編集ソフトくらいまでなら動かせている。
さあ、いますぐ
yarn add @fleur/fleur @fleur/react
。Fleur - A fully-typed, type inference and testing friendly Flux framework
Fleurは2019年的なAPIで、書きやすく、より型に優しく、テスタブルに構成されたライブラリです。
Fluxible
やRedux
をベースにしていますが、基本的な設計に対して迷いや再実装が生じづらいようにAPIを設計しています。TypeScriptがある時代前提で設計されているので、Reduxで型にお祈りを捧げるときにありがちな
export enum ほへActionType
とかexport type なんとかActionTypes = ReturnType<typeof ほげ>
みたいなのを頑張って書く必要がありません。これが標準で提供されているの圧倒的にアドです。Redux初心者がtypescript-fsa
にたどり着くにはあまりにも超えなければいけない障壁が多すぎます。またNext.jsとの併用にも対応しており、
npx create-fleur-next-app
コマンドでNext.js + FleurによるSPA開発も可能です。(Next.jsなしのSSRでもご利用いただけます、ルーター用意されてます。)Redux devtoolsにもとりあえず対応しているため、デバッグツールにも困らないと思います。
使い方・設計編
それではFleurの使い方、基本的な設計パターンを見ていきましょう。ここでは「VRoid Hub」をFluerで実装するというケースでコードを例示していきます。
Fleurは大まかに↓の4つの要素を必要としています。オススメのディレクトリ構成
Fleurでは、Re-ducks パターン風のディレクトリ構成を推奨しています。Re-ducksパターンは弊社の色々なプロダクトでも割とよく採用されている構成です。
Fleurが推奨する構成は具体的には以下のようになります。
- app - spec -- ここにテスト用のモック関数とかを詰める - mocks.ts - components - domains - Character - operations.ts - actions.ts - selectors.ts - store.ts - model.ts -- フロント側でどうしても必要なビジネスロジックは関数化してここに置く - index.ts -- Re-ducksパターンではあるけど任意。暇なら置いてよい - operations.test.ts - selectors.test.ts - store.test.ts - model.test.ts - User - operations.ts - actions.ts - selectors.ts - store.ts - model.tsRe-ducksパターンでドメインを分けていくと、selectorやmodelがそのドメインに関係していないといけないことを強要出来るので、「汎用的な
utils.ts
にドメインロジックをアーーーーー????」みたいな事態を防げます。もっともこの構成は、あくまで中規模化したプロダクトに対しての推奨で、より小さなプロダクトやプロトタイピングには手間が多いと思います。最小構成で行くなら
domains/{Character,User}.ts
にActionとかOperationsとかをドメイン毎に全部詰めるみたいな構成でもいいでしょう。exportがごちゃごちゃしてなければ最低限の治安は保てるはずです。大規模アプリでどういう構成にしたらいいのかというのは今調べています。みなさんのプロダクトのディレクトリ構成とかフロントエンドアーキテクチャを語る記事を募集しています。
それでは各ファイルの中身を見ていきましょう
Operations
まずはOperation(アプリにおける手続き)を定義します。とりあえずキャラクター情報を取得してみましょうか。キャラクターにはキャラクターの情報(Character entity)とその投稿ユーザーの情報(User entity)があるものとします。
domains/Character/operationsimport { operations } from '@fleur/fleur' import { CharacterActions } from 'domains/Character/actions' import { UserActions } from 'domains/User/actions' import { normalize } from 'domains/normalize' import { AppSelectors } from 'domains/App/selectors' import { API } from 'domains/api' export const CharacterOps = operations({ // 特定のキャラクターの情報を取得する async fetchCharacter(context, characterId: string) { context.dispatch(CharacterActions.fetching.started, { characterId }) // 認証情報取る const credential = AppSelectors.getCredential(context.getStore) try { // APIからデータを取る const response = await context.depend(API.getCharacter)(credential, characterId) // Entityを正規化したりDateに変換したりは`normalize`でやったことにする const { user, character } = normalize(response) // 正規化したデータをStoreに送りつける context.dispatch(CharacterActions.charactersFetched, [ character ]) context.dispatch(UserActions.usersFetched, [ user ]) context.dispatch(CharacterActions.fetching.done, { characterId }) } catch (error) { rethrowIfNotResponseError(error) context.dispatch(CharacterActions.fetching.failed, { characterId, error }) } }, // 他のoperationの定義が続く })Operationで使うAPIと設計のコツを見てみましょう
context.getStore
- Storeのインスタンスを取れます。
context.getStore(CharacterStore)
のような感じで使いますが、selectorに任せてしまうので直接コールする機会はあんまりないかもしれません。context.depend
- 渡されたオブジェクトをそのまま返します。「は?」って感じですね。
これはテスト時にDependency Injectionを行うための仕組みです。後述します。- normalize - エンティティの正規化はOperationでやりましょう。純粋関数として切り出しておくとテストもしやすくて良いです。少なくともStoreで正規化するのはDRYじゃないのであまりおすすめしません…
context.dispatch
- Actionを発行します。normalizeの正規化単位
APIから振ってきたJSONは基本的にはEntity単位で切っていきます。
例えばVRoid HubのCharacter Entityは以下のような構造でAPIから降ってきます。interface SerializedCharacter { character_id: string name: string create_at: string /** 投稿者 */ user: { user_id: string name: string icon: SerializedUserIcon } }このJSONをDB的に分割すると
Character
とUser
とUserIcon
になります。
しかし、UserとUserIconは基本的にセットで使われているので、特に分割する必要がありません。なので分割せず、以下のような2つのEntityに正規化しています。interface Character { character_id: string name: string created_at: Date user_id: string } interface User { user_id: string name: string icon: SerializedUserIcon }Actions
次にActionsの定義です。Fleurにおいてこれはただの識別子と型宣言であり、Reduxと違ってこのActions自体はコールすることは出来ません。アプリケーションでどういうイベントが起きるかを宣言しているのみです。
domains/Character/actions.tsimport { actions, action } from '@fleur/fleur' import { CharacterEntity } from "./types"; export const CharacterActions = actions(/* Redux Devtools用の識別子 = */ 'Characters', { // action名: action<ペイロードの型>() charactersFetched: action<CharacterEntity[]>(), fetching: action.async< { characterId: string }, { characterId: string }, { characterId: string, error: Error } >(), })
fetching
とcharactersFetched
が並んでるのがモニョっとしますね。しかしCharacter Entityが降ってくるのはキャラクターをフェッチしたときだけとは限らないので、あくまでフェッチ状況を伝えるActionと、実際にフェッチされたEntityを伝えるActionを分けています。他のEntityを正規化した時にCharacter Entityが取り出されて、他のドメインから
charactersFetched
が起きたときにCharacterActions.fetching.done
するのが適切か?通信状態も一緒にごまかさないといけなくて設計がちょっと大変じゃない?という感じですね。
Action名は過去形を使うようにしましょう。Redux Style guideや@f_subalさんのスライド でも言及されていますが、Actionを受け取った側がどういう処理をすべきなのかが伝わりやすくなります。
ただ一点、Redux Style Guideで述べられている「一つのActionで全ての関係Reducerが反応するようにすべき」という点には一概に賛同していません。
実はそのような構造にすると、特に大規模なアプリケーションにおいて「あるActionによってアプリケーションで何が発生するのか」が人間的に予測しづらくなり、実は適度な粒度でActionを連投した方が処理の流れが自明になることがあります。(VRoid Hubではエンティティ種毎に
usersFetched
,charactersFetched
のようにactionを連投する形にしています。)特にFleurでどうしても仕方なくStore側で副作用を持っている場合は、どういう副作用を起こすかによってActionを切り分けた方がよさそうです。
またそれが推奨されている理由のもう一つに、ReduxではActionの連投はパフォーマンスに良くないというものがあるそうですが、FleurではStoreからの変更通知は
requestAnimationFrame
でバッファリングされているため、あまり気にしなくてよいです。Store
続いてStoreです。 Fleurにはclass-style StoreとreducerStoreがありますが、基本的に
reducerStore
の利用を推奨しています。こちらは副作用を持てないStoreなので、どうしてもStoreで副作用が必要なときはclass-style Storeを利用します。(class-style Storeの書き方はこちらをご参照ください。)domains/Characters/store.tsimport { reducerStore } from '@fleur/fleur' import { CharacterActions } from './actions' import { CharacterEntity } from '../CharacterEntity/types' interface State { characters: { [characterId: string]: CharacterEntity | void } fetching: { [characterId: string]: { fetching: boolean, error?: Error } } } export const CharacterStore = reducerStore<State>('Character', () => ({ characters: {}, fetching: {}, })) .listen(CharacterActions.charactersFetched, (state, characters) => { characters.forEach(c => state.characters[c.id] = c) }) .listen(CharacterActions.fetching.started, (state, { characterId }) => { state.fetching[characterId] = { fetching: true } }) .listen(CharacterActions.fetching.done, (state, { characterId }) => { state.fetching[characterId] = { fetching: false } }) .listen(CharacterActions.fetching.error, (state, { characterId, error }) => { state.fetching[characterId] = { fetching: false, error } })
reducerStore<State>(storeName, initialStateFactory)
でStoreを宣言します。
storeName
はアプリ内で一意の名前である必要があります。SSR時に吐き出すJSONの名前空間の識別に利用されますが、SSRなしの場合でも必須です。initialStateFactory
はStoreの初期状態を返す関数を渡します。ReducerStore.listen(action, callback)
でactionに対する処理を指定します。
state
を直接変更していますが、これはimmerでラップされたdraftオブジェクトなので、実際のstateはイミュータブルに変更されます。
- これは極めて強くて、特にReact Hooksとの組み合わせにおけるメモ化ではめちゃくちゃ楽にメモ化条件を設定することが出来ます。
- Store内で外部通信などの副作用を起こさないようにしてください。副作用はできるだけOperation層に集めてください。
Component
ではここまで書いてきたものをコンポーネントに繋いでいきます。
pages/character.tsximport React, { useCallback, useState, ChangeEvent } from 'react' import { CharacterSelectors } from 'domains/Characters/selectors' import { useStore, useFleurContext } from '@fleur/react' import { UserSelectors } from 'domains/Users/selectors' import { CharacterOps } from 'domains/Characters/operations' import { API } from 'domains/api' export const CharacterPage = () => { // URLからテキトーにキャラクターIDを取ってくる const characterId = 1 const { executeOperation, depend } = useFleurContext() const character = useStore(getStore => CharacterSelectors.getById(getStore, '1') ) const user = useStore(getStore => character ? UserSelectors.getById(getStore, character.user_id) : null ) const handleChangeName = useCallback( ({ currentTarget }: ChangeEvent<HTMLInputElement>) => { if (!character) return depend(API.putCharacter)({ name: currentTarget!.value }) executeOperation(CharacterOps.fetchCharacter, character.id) }, [character] ) if (!character || !user) { return <div>{/* いい感じのスケルトンを出す */}</div> } return ( <div> <h1> <input type="text" defaultValue={character.name} onChange={handleChangeName} data-testid="input" /> </h1> <h2> <a href={`/users/${user.id}`} data-testid="author"> {user.name} </a> </h2> </div> ) }ここで出てきたAPIを解説します。
useFleurContext
- Operationを実行するためのexecuteOperation()
, DIのためのdepend()
, Storeの値の遅延取得のためのgetStore()
が入ったオブジェクトを返します。
executeOperation(operation, ...args)
- 第一引数に渡されたOperationを実行します。depend(obj)
- objを取得します。テスト時にobjをモックに差し替えることが出来ます。getStore(Store)
- Storeのインスタンスを取得します。基本的にuseStoreで値をとってくるので余り使うことはないと思いますが、表示には関係ないけどStoreから値を取らないといけない場合に使います。コンポーネント内の属性に出てくる
data-testid
は、後述するテストで利用します。Selector
Selectorはこんな感じに用意してあげます。
domains/Characters/selectors.tsimport { selector } from "@fleur/fleur"; import { CharacterStore } from "./store"; export const CharacterSelectors = { getById: selector( (getState, id: string) => getState(CharacterStore).characters[id] ) }
- Component側のuseStoreでは
getStore
でしたが、selector内ではgetState
です
Store#state
を取得してくるためです。- Storeのインスタンス自体にアクセスする必要がある場合、
selector()
の代わりにselectorWithStore()
を使います。Bootstrap
最後にアプリの立ち上げ部分を書きます
app.tsximport React from 'react' import ReactDOM from 'react-dom' import Fleur from '@fleur/fleur' import { CharacterStore } from 'domains/Characters/store' import { UserStore } from 'domains/Users/store' import { FleurContext } from '@fleur/react' const app = new Fleur({ stores: [CharacterStore, UserStore] }) const context = app.createContext() window.addEventListener('DOMContentLoaded', () => { const root = document.getElementById('#root') ReactDOM.render( <FleurContext value={context}> <App /> </FleurContext>, root ) })
new Fleur
でFleurインスタンスを作ります
stores
オプションにアプリ内で利用しているStoreを全て渡します。Fleurでのテスト
ここからは今まで書いてきたOperation, Action, Componentに対してのテストを書いていきます。
Fleurのテストには@fleur/testing
というパッケージを利用します。
yarn add -D @fleur/testing
テストフレームワークにはjest (with ts-jest)を利用する想定をしています。
Contextのモック
まずテストのためのモックContextを作ります。
spec/mock.tsimport { mockFleurContext, mockStore } from "@fleur/testing" import { CharacterStore } from "domains/Characters/store" const baseContext = mockFleurContext({ stores: [ // ここにアプリで使われるStoreをこの形式で突っ込む mockStore(CharacterStore) ] }); export const baseOperationContext = baseContext.mockOperationContext() export const baseComponentContext = baseContext.mockComponentContext()OperationとStoreのテストで利用する
baseOperationContext
、Componentのテストで利用するbaseComponentContext
をexportしておきます。Operationのテスト
はい、ではまずOperationのテストをしていきましょう。
domains/Character/operation.test.tsimport { CharacterOps } from './operations' import { CharacterActions } from './actions' import { UserActions } from 'domains/Users/actions' import { API } from 'domains/api' import { baseOperationContext } from 'spec/mock' import { fakeRawCharacter } from 'spec/fakes/character' describe('CharacterOps', () => { it('キャラクターとユーザーのEntityちゃんと投げた?', async () => { const context = baseOperationContext.derive(({ injectDep }) => { // Storeの特定の状態を設定する場合は `deriveStore` をする // deriveStore(AppStore, { credentialKey: 'mock' }) // API.getCharacterをモックする injectDep(API.getCharacter, async (_, characterId) => fakeRawCharacter()) }) await context.executeOperation(CharacterOps.fetchCharacter, '1011') expect(context.dispatches[1]).toMatchObject({ action: CharacterActions.charactersFetched }) // expect(context.dispatches[1].payload).toMatchInlineSnapshot() expect(context.dispatches[2]).toMatchObject({ action: UserActions.usersFetched }) // expect(context.dispatches[2].payload).toMatchInlineSnapshot() }) })
injectDeps(元のオブジェクト, モックオブジェクト)
によって、Operation内で.depend(...)
しているオブジェクト(関数)をモックすることが出来ますaction.action
が関数なので一発でtoMatchInlineSnapshotしちゃうとちょっと信頼性に欠けます
context.dispatches
に発火されたActionの配列が入っているので、そのpayloadが意図した形になっているかどうかをチェックしていけばStoreのテスト
Storeもテストしていきましょう
domains/Characters/store.test.tsimport { CharacterStore } from './store' import { CharacterActions } from './actions' import { baseOperationContext } from 'spec/mock' import { fakeCharacter } from 'spec/fakes/character' describe('CharacterStore', () => { it('エンティティがちゃんと保存されるか', () => { const context = baseOperationContext.derive(({ deriveStore }) => { deriveStore(CharacterStore, state => { state.characters['10'] = fakeCharacter({ id: '10' }) }) }) const character = fakeCharacter() context.dispatch(CharacterActions.charactersFetched, [character]) expect( context.getStore(CharacterStore).state.characters[character.id] ).toEqual(character) }) })OperationContextからActionを投げて、意図したとおりのstateになっているかを検証します。
ここでは雑に1ケースしか書いてないですが、必要であればこの形式で書き足していきましょう。その際、テストケース毎に
baseOperationContext.derive()
で複製したcontextを使うことを推奨しています。
deriveは#operationのテストで書いたように、Storeの状態を派生させる事ができます。前提状態があるテストを書く場合に利用してください。const context = baseOperationContext.derive(({ injectDep }) => { // オブジェクトはshallow-mergeされる deriveStore(CharacterStore, { characters: { '10': fakeCharacter(); } }) // Deep-mergeしたい時はコールバックを使う deriveStore(CharacterStore, (state) => { state.characters['10'] = fakeCharacter() }) });Componentのテスト
最後にComponentのテストです。 コンポーネントのレンダリングには
@testing-library/react
を利用します。import { render, getByTestId, fireEvent } from '@testing-library/react' import { TestingFleurContext } from '@fleur/testing' import React from 'react' import { baseComponentContext } from 'spec/mock' // ... 略 describe('Character', () => { const mockedContext = baseComponentContext.derive(({ deriveStore }) => { deriveStore(CharacterStore, state => { state.characters['1'] = fakeCharacter({ id: '1', name: 'Haru Sakura', user_id: '2' }) }) deriveStore(UserStore, state => { state.users['2'] = fakeUser({ id: '2', name: 'Hanakla' }) }) }) // Case.1 キャラクターの情報ちゃんと出てる? // Case.2 APIリクエストちゃんと飛ぶ? })
baseComponentContext.derive
によりStoreの状態を設定したContextを生成します。
これを次のテストケースに食わせてあげると、特定の状態下で正しくコンポーネントが動くかをテストできます。Case1it('キャラクターの情報ちゃんと出てる?', async () => { const context = mockedContext.derive() const tree = render( <TestingFleurContext value={context}> {/* <なんとかRouter url='/characters/1'> */} <CharacterPage /> {/* </なんとかRouter> */} </TestingFleurContext> ) expect(tree.getByTestId('input').value).toBe('Haru Sakura') expect(tree.getByTestId('author').getAttribute('href')).toBe('/users/2') expect(tree.getByTestId('author').innerHTML).toBe('Hanakla') })
mockedContext.derive()
でテストケース用のContextを初期化し、TestingFleurContext Component
に与えます。これにより、内部のFleur Contextをモックすることが出来ます。あとは
[data-testid]
を元に要素を探して適切な値が出てるかを調べてるだけですね。
TestingFleurContext
で囲うということだけ覚えておいてください。次にAPIリクエストのテストをします。
Case2it('APIリクエストちゃんと飛ぶ?', async () => { const apiSpy = jest.fn(async () => void 0) const context = mockedContext.derive(({ injectDep }) => { injectDep(API.putCharacter, apiSpy) }) const tree = render( <TestingFleurContext value={context}> <なんとかRouter url='/characters/1'> <CharacterPage /> </なんとかRouter> </TestingFleurContext> ) fireEvent.change(tree.getByTestId('input'), { target: { value: 'Haru' } }) // Wait for request await new Promise(r => setTimeout(r)) expect(apiSpy).toBeCalledWith({ name: 'Haru' }) expect(context.executes[0]).toMatchObject({ op: CharacterOps.fetchCharacter, args: ['1'] }) })
- Operationのテストでも登場した
injectDep
がここでも登場します。 Componentから外部通信を起こすようなパターンの設計をしている場合、ここでdepend
/injectDep
を使うことで関数をモックすることが出来ます。- Componentから発火されたexecuteOperationの内容は`context.
裏話
Fleurの生まれ
Fleurは
Delir
というWeb製映像編集ソフトを開発していく過程で生まれました。当初は
Flux Utils
を使って構築されていましたが、直接的なStoreへの依存や、Action CreatorからStoreを触れないのはComponentが表示以上の責務を負わなければならないなどの問題があり、より適切な移行先を探す中でFluxibleにたどり着きました。Reduxはかなり要件のアンマッチがあり採用しませんでした。
- Storeに副作用が持てない
- JSONを扱うのが基本
- じゃあレンダリングエンジンのインスタンスはどこでもつのか? Component側か?アプリケーション全体のあらゆるイベントを受け付けないといけないのにComponent? React Context使ってインスタンス配っても状態変化の予測がつかない狂気になるだけじゃん?
- Storeに突っ込むのが保守性・予測性が良い。ならReduxは使えない…
- レンダリングエンジンが自身で副作用を発してもComponent側からはただのStoreの変化として扱える、合理的。
- 標準で非同期処理に対応していない
- 型付けのためだけのコードが多くてしんどい
- ActionNamesとActionの型定義が離れてるの果てしなくやりづらい
しかしFluxibleも2016年くらいのときには既につらみを抱えていました。TypeScriptの台頭です。
Fluxibleは上記の要件的には完全に一致しており、コードの読み書きにもほとんど困らないAPIでしたが、こと型をつけるという文脈ではかなり無理のあるAPIになっていました。(pixivFACTORYに所属していた時にFluxibleを採用し型定義を自前で行いましたが、相当しんどいものでした。)そこでFluxibleを元に、より現代的な設計パターンやコードの書きやすいAPIを取り入れたFluxフレームワークを作る、という目的でFleurが誕生しました。
- 型推論フレンドリー
- コードの書き心地を良くする
- React Hooks対応
- 非同期処理対応 (Fluxibleは対応済み)
- immer.js組み込みのStore
- 大体のケースで使ったほうが良い。 まだImmutable.jsが良いとかImmer.jsがES5で使えない思ってる人いないよね?
- パフォーマンス上のボトルネックにならない
- Reactの再レンダリングによって映像レンダリングのFPSが落ちる。UXに如実に影響を与えるのでこれは必要な要件だった。
さらに、本業で得たSSR周りや一般的なWebアプリケーションに対する設計に関する知識を取り込み、通常のWebアプリケーションやSSR環境でも利用可能なフレームワークとしてキメキメにしていきました。
そういう経緯があり、Fleurは2019年までのSPA用ライブラリとしてはn番煎じをキメ込んで非常にまとまりが良く保守性・汎用性の高いフレームワークに仕上がりました。
SSR対応
SSR要件もちゃんと気にかけており、SSRを行うデモアプリを運用して、Apache Benchで現実的じゃない量のアクセスをかけてメモリリークが起きていない(GCでちゃんとメモリが開放されること)を確認したりしてします。
今回のサンプルにも載せられなかったんですが、ルーターも用意されています。
最後に
言い訳させてください! 完全攻略ガイドと言っておきながら体調不良によりNext.jsとかSSRとかに結局触れられませんでした!そこらへんは後日別記事でやるかもしれません(この記事のウケがよかったらね)
本記事で書いたコードはra-gg/advent-calendar-fleurにまとまっています。(実は雰囲気コードなので全部のテストは通ってないんだけどね)
とりあえず自分の中では2019年までのSPA設計のまとめとしてはいい感じのフレームワークになっているかなと思います。残念ながら社内プロダクトでの採用の機会はまだないんですけどね〜! 来年は2020年なのできっとさらに色々良くなったFleurをお見せできるかと思います!
もしよろしければ、「雑にFleur使ってみた」「作者が怠惰だからオレがFleur完全攻略してみた」など、Fleurに対する感想、不満、オレの設計語りなどインターネットに放流してください! Fleur開発に対するモチベーションと設計の洗練度が上がるので! Twitterハッシュタグは #fleurjs GitHub Topic / Qiita tagは fleur-js です!
Q & A
2019年的?
2019年的です。2020年的ではないという意味でもあります。色々情報を集めてはいますがまだよくなれる余地がありそうな気がしています。マイグレーションしやすいことは考えていますが、Breaking changeは入れていくと思います。
Next.jsで使える?
使えます。いますぐ
yarn create fleur-next-app <your-app-name>
しましょう。
本記事で解説したのとほぼ同等の構成のファイル郡でアプリ開発をおっ始める事ができます。そのうちNext.js + Fleurでアプリを作る記事でも書こうかと思います。
画面毎にStoreを分けるのどう思う?
いいと思う。ただその場合この記事で解説したdomain毎の分割だけだと治安が悪くなるので、page毎にStoreを入れるディレクトリを上手く分ける必要はありそう。ただし現バージョンのFleurはあまりそれがやりやすい構造になってないので、それについては今後の課題かなと思っています。
useReducer
を薄〜くラップしたライブラリがあるとそういう「アプリ全体には影響ないけど、ある特定のページ以下でめっちゃグローバル」みたいなやつを切り出せるんだよな〜とも思っています。これも考えてる。既にいくつかそれらしいライブラリがあった気もしている。なんでOperationとActionが分かれてるの?
ActionとOperationが1ファイルに混じってごちゃごちゃするのが嫌だったのと、Re-ducksパターンによる影響と、実際ActionとOperationってそんなに綺麗に一対一になるか?という気持ちに決着がつけられていないためです。typescript-fsaはその点すごくシンプルですね。ActionとOperationが透過的に扱えるRedux故の特徴です。 Fleurも何かしら考えたほうがいいよな〜とは思っています。
middlewareない?
ない。middlewareを入れると実装の見通しが悪くなるので、簡単に入れられる仕組みを入れたくない。どうしても必要だったらcontextをラップしてくれ。(Redux DevTools対応はその方式でやってる)
const yourMiddleware = (context: AppContext) => { const executeOperation = context.executeOperation const dispatch = context.dispatch context.executeOperation = (op, ...args) => { // なにがしの処理をする context.dispatch(MiddlewareActions.hoge, {}) return executeOperation(op, ...args) } context.dispatch = (action, payload) => { if (action.name.indexOf('.started')) { // ローディングを出したりする } return dispatch(action, payload) } return context } const app = new Fleur(); const context = yourMiddleware(app.createContext())Fleurのselectorはmemoizeしてる?
してない。 関数一発でmemoizeしてもキャッシュヒット率がたかが知れてるのでしてない。
代替案としてはReactの
useMemo
を使う形に寄せて欲しい。useMemoならコンポーネントの文脈に沿ったよりヒット率の高いmemoizeができる。reselectのコードを読んだことがあればわかると思うけど(ここらへんな)、あるselectorが複数回異なる引数で呼ばれるような場合、reselectのmemoizeはまともに機能しないっぽく見える。ある引数でmemoizeしても次に異なる引数でselectorが呼ばれればキャッシュは破棄される。そして真面目にプロダクトコード書いてたらこれは普通に発生する。
EcmaScriptのRicher keys - compositeKeyが入ればWeakMapで今よりはマシなmemoize機構を作れそうだけど、FleurもReduxも別々の問題でキャッシュヒット率には難がありそうな予感がしています。
なんでComponentからdispatch出来ないの?
dispatch
はStoreに対するかなり低レイヤーな操作です。もしこれをComponentから使えるようにしてしまうと、アプリの構造化が属人的になりやすくなってしまいます。(どこからどういう粒度でdispatchするかを考える余地が生まれてしまう)とはいえ昨今のイケてるフロントエンドを見ているとReact Suspenseとか
useAsync
などを使ってComponent側から通信を起こすケースがありがちなので、そろそろ許しても良いかもしれない…けどアプリが大きくなってもそれを続けられるのか…?小規模アプリだから許されてるのでは…? そもそもその手のアプリはFluxライブラリ使ってないんじゃ…? というところで悩んでいます。俺はVue派なんだが?
Vue版も作ろうか悩んでる、やればVuexより型は強くなる。ただ内部事情として
immer.js
とVueは相性が悪いとかいう次元じゃないのでFleurの書き直しかMutableStoreの対応が必要になる。
あとMobxとかVuexとか色々見ていて「Fleurは本当にこのままの設計でいいのかな〜?」という気持ちもあるのでそこらへん全てに踏ん切りがついたらやるかもね(たぶんVueの人たちVue公式以外のもの使いたがらなさそうという気がするので暇すぎてひまわりになったら?やる)
パフォーマンスどんなもん?
ちょっと極端なケースで計測しているのですがFleur vs Reduxはこんな感じです。(masterの最新コミットでの比較)
Fluxibleも計測してるんですがあまりにも遅いしwarnが多いので省きました。
なんで君のサンプルコード
export default
してないの?
- Named exportしておくとVSCodeのimport候補に出やすくなるから
- 後から「あっこの名前よくなかったわ」って時にVSCodeの自動リファクタリングで一発で名前を変えたいから
(default importされたやつは名前変更で一発で変わってくれない)- default importした時に、人によってimport物にどういう規則で名前つけるか揺れてほしくないから
- import名に対してコーディング規約を考えるのしんどいから最初から適切な名前ついていてホシイ ? ?
- 投稿日:2019-12-24T14:41:22+09:00
【React】useEffectの第2引数って?
概要
ReactのHooksに関して学び始めた際に
useEffect
の使用方法、特に第2引数部分の意味の理解に苦しんだので、そのまとめです。useEffectとは
ReactのHooksの1つです。
公式では、副作用を実行するフック
のように説明されています。
ざっくり言うと、ライフサイクルメソッドを関数コンポーネントで実現する為に使われたりします。この
useEffect
ですが、2つの引数を持ちます。まず第1引数
デフォルトではRender毎に実行される関数を取ります。
useEffect(() => { console.log("毎回実行"); });そして第2引数
第2引数を与える事で第1引数の関数が実行されるタイミングを自在にコントロールする事ができます。
下記の2つのケースを考えます。(1) 空の配列が渡された場合
第2引数に空の配列が渡された場合、
マウント・アンマウント時
のみ第1引数の関数を実行します。// マウント・アンマウント時のみ第1引数の関数を実行 useEffect(() => { console.log('マウント時のみ実行') }, [])(2)値の配列が渡された場合
第2引数に値の配列が渡された場合、
最初のマウント時と与えられた値に変化があった場合
のみ第1引数の関数を実行します。// 与えられた値に変化があった場合のみ第1引数の関数を実行 useEffect(() => { console.log('変化があった場合の実行') }, [value])まとめ
- 第2変数を指定なし=>
Render毎
に第1引数の関数を実行。- 第2変数に
[]
を指定 =>マウント時とアンマウント時
に第1引数の関数を実行。- 第2変数に値の配列を指定 =>
マウント時と指定された値に変化があった場合
のみに第1引数の関数を実行。参照
本記事では非常に簡単な
useEffect
の説明を行いました。
より詳しく、正確な詳細等は下記を参照してください。
- 投稿日:2019-12-24T14:16:41+09:00
ReactとNginxでリロードしても404しないSPAを作る
はじめに
いなたつアドカレの二十四日目の記事です。
Dockerを使ってReactで作ったアプリケーションをnginx上にポイ投げするためのあれですね。DockerってなにとかnginxってなにReactってなにってのはスルーで行きます。
今回の記事はcreate-react-app(以下CRA)を使用する前提となっています。つかうもの
- react
- react-router
- nginx
- docker
完成系
react-router使ってnginx上でリンクに#がつかないかつリロードしても404にならない構成の作成
ディレクトリ構成
- app
- docker
- react
- Dockerfile
- nginx
- default.conf
- web
- docker-compose.yml
こんなかんじです
dockerディレクトリにdockerで使用するファイルを格納しています。
webディレクトリにCRAで作成したアプリが入りますね。DockerでReact
docker-compose.ymlversion: '3' services: react_app: container_name: react_app build: ./docker/react command: npm start volumes: - ./web:/app ports: - 3000:3000つづいて
docker/react/DockerfileFROM node WORKDIR /appここは基本的になんでも構いません(めんどくさかった)
作成するアプリケーションに合わせてpackage.jsonなどを用意してあげてください。
今回はめんどくさいのでこれでいきます。Dockerで んぎっくす
はい、Nginxいきます
docker-compose.ymlnginx: image: nginx container_name: nginx ports: - 8080:80 volumes: - ./web/build:/var/www - ./docker/nginx/:/etc/nginx/conf.d/ depends_on: - react_appweb(CRAのディレクトリ)の中のbuildをnginxコンテナにマウントしています。
docker/nginx
には設定ファイルですね。default.confserver { listen 80; location / { root /var/www; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }buildをマウントしてそのマウントしたファイルのindex.htmlを表示するぜ〜って感じですね。
Reactのコンポーネント構成
App.jsimport React from 'react'; import { BrowserRouter as Router, Route } from 'react-router-dom' import Hello from './Pages/Hello' import Changed from './Pages/Changed' function App() { return ( <Router> <Route exact path='/' component={Hello} /> <Route exact path='/changed' component={Changed} /> </Router> ); } export default App;
react-router
で2つのページを遷移できるようにします。/Pages/Hello,jsimport React from 'react'; import {Link} from 'react-router-dom' const Hello = () => { return ( <div> <div>Hello</div> <Link to='/changed'>ぺーじせんい</Link> </div> ) } export default Hello;/Pages/Changed,jsimport React from 'react'; const Changed = () => { return ( <div> <div>Changed</div> </div> ) } export default Changed;ページ遷移するだけですね。
うごかしてみる
とりあえずreactのプロジェクトをbuildしましょう。
localhost:8080
にアクセスします。こんな感じですね。
ページ遷移してみます。
遷移できましたね。おっけーです。
ちょっとリロードしたくなってきたわ
あっあっあっ
だめですね。
んぎっくす
さんに怒られちゃいました。ハッシュルーターとかダサいじゃん?
URLに「#」とかつくHashルーターをつかうことでこれを解決することはできます。
localhost:8080/#/changed
だっっっっっっっっっさあああああいやだよ。
解決しよう
nginxの設定を少し変えましょう
```default.conf
server {
listen 80;location / { root /var/www; index index.html index.htm; try_files $uri /index.html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; }}
```
try_files $uri /index.html;
この一行を書くだけですね。ほら、リロードしてみてくださいよ、怒られますか?怒られませんよね。
これで解決ですね。
なぜ解決できたのか
とりま解決でけたらえーねんって人はみなくてよきですえ
nginxにはtry_files ディレクティブというものが存在し、これは、引数を前から順番にファイルが存在するかをtryしまくって行ってくれます。そして見つからなかった場合は
index.html
を返却しましょうねっていう感じです。何も見つからなかったら404を返すよ〜って実装をする場合もありますね。
- 投稿日:2019-12-24T13:33:00+09:00
SCSSとstyled-componentsの勉強と比較のための環境を作った
Make IT Advent Calendar 2019 24日目の記事です。
今年も危うくクリぼっちになりかけましたが、サークルの仲間とクリパすることになりました。
皆様も良いクリスマスをお過ごしください。今年は、マイCSSブーム到来というやつです。5日目にもCSS関連の記事を投稿しました。
ゼミやインターンでSCSSを使っていますが、少し前からstyled-componentsに興味が湧いたので、勉強がてらどっちも書いてみようという旨です。差分が発生する箇所について、コードを書いた感想を述べていくだけですので、実行環境だけ欲しい方は 環境 > リポジトリ(github) をクリックして
git clone
してください。環境
- リポジトリ(github)
- 投稿時現在では、矢印アイコン・メニューボタン・クレジットカードコンポーネント(作り途中)が含まれています。
- 言語・ライブラリ
- typescript(v3.7.2)
- react(v16.12.0)
- storybook/react(v5.2.8)
- scss(v4.13.0)
- styled-components(v4.4.1)
- 実行環境
- macbook pro 2015 catalina
- google chrome
- エディタ
- visual studio code
- vscode plugin
- vscode-styled-components
- テンプレートリテラル内でcssの補完が効きます
- color highlight(他にオススメあればご教授ください)
- テンプレートリテラル内でカラーコードの背景に色が出なかったので導入しました
差分
webpack.config.js
SCSSmodule.exports = { /* 一部省略 */ module: { rules: [ { test: /\.(ts|tsx)?$/, use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/react"] } }, "ts-loader" ] }, { test: /\.css?$/, use: ["style-loader", "css-loader"] }, { test: /\.scss?$/, use: [ "style-loader", { loader: "css-loader", options: { url: false, sourceMap: true, importLoaders: 2, modules: true } }, { loader: "postcss-loader", options: { sourceMap: true, plugins: [require("autoprefixer")({ grid: true })] } }, { loader: "sass-loader", options: { sourceMap: true } } ] } ] }, plugins: [ new TypedCssModulesPlugin({ globPattern: "src/**/*.scss" }) ], /* 一部省略 */ };scssのために4つのloaderを通しています。おかげでconfigが縦に長い!
autoprefixerしか使っていないので良いですが、今後を考えるとpostcss.config.jsとして別ファイルにした方が良いですね。TypedCssModulesPlugin はscss保存時、自動でd.tsを生成します。dropbox製というのが好きです。
私は、フォルダをコンポーネント名にして、中にindex.tsxを作成する方法を使います。
これは、コンポーネントに関係する style.scss, style.d.ts, react custom hooksをまとめてフォルダ内に置けるので気に入っています。styled-componentsmodule.exports = { /* 一部省略 */ module: { rules: [ { test: /\.(ts|tsx)?$/, use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/react"], plugins: ["babel-plugin-styled-components"] } }, "ts-loader" ] } ] }, /* 一部省略 */ };スッキリしました。
autoprefixer・TypedCssModulesPluginの役割は、styled-componentsが全て担ってくれます。
babel-plugin-styled-components について、こちらの記事が参考になりました。reset.css
// for SCSS import 'reset-css'; // for styled-components import React from 'react'; import { Reset } from 'styled-reset'; const App = () => ( <> <Reset /> <div>Hello world!</div> </> );SCSSでは shannonmoeller/reset-css を使用します。
styled-componentsでは zacanger/styled-reset を使用します。ソースコード比較(buttonコンポーネント)
See the Pen menu button scss by haduki1208 (@haduki1208) on CodePen.
マウスホバーでアニメーションするボタンです。
これをstyled-componentsで置き換えます。See the Pen menu button styled by haduki1208 (@haduki1208) on CodePen.
scssがなくなったことでフォルダ内がスッキリしますが、scssの内容がtsxに移動するのでファイルが縦に長くなります。
私はvscodeのエディタを分割してtsxとscssを並べながらコードを書くので、辛さがあります。
↓こんな感じ
解決策として、
1. atomic designを意識してコンポーネントを小さくしていく
2. style.tsx のようなファイルを用意してstyledの要素を宣言する
を考えました。基本的に1番の方法を実践していきたいと思います。おわりに
趣味で何かコーディングするとき、このリポジトリ(github)を使っていこうかなと思っています。
storybookも始めて導入してみたのですが、どんなコンポーネントがあるか一通り見れるので便利です。scss(css)で培ってきた技術がそのままstyled-componentsに活用できるため、
新規プロジェクトはstyled-componentsだったらいいなぁと思っています。
- 投稿日:2019-12-24T13:23:17+09:00
TypeScript で自分だけの React を作る
この記事は 株式会社 ACCESS Advent Calendar 2019 24 日目の記事です。
先週、
@Momijinn(全くの別人でした。失礼しました。) 社内のとあるフロントエンダーに「Build your own React」という記事の存在を教えてもらいました。React の内部実装を把握するためにスクラッチで自分の手を使って実装する手順を紹介している記事です。元記事は JavaScript で記述されていたのですが、そのまま写経するのもつまらないので TypeScript に書き換えながら実装してみました。元記事の部分的な日本語訳や実装しながら得た知見をこの記事で紹介したいと思います。
この記事は実装の最終形を機能ごとに分割して説明しますが、元記事は React を構成する重要な要素を順番に紹介しています。深い理解を得たい方は元記事を読みながら自分の手で実装することがおすすめです。
自分が実装したコードは こちら で公開してます。今回実装した React の API は
- React.createElement
- ReactDom.render
です。動作環境
requestIdleCallback と String.prototype.startsWith が動作するブラウザであることが前提です。JSX (TSX) と DOM
公式 でも言及されていますが、JSX の記述は React.createElement のシンタックスシュガーです。JSX を使用した記述は下記例のように書き換えられます。
JSXconst hello = <div id="hoge">Hello {this.props.toWhat}</div>;JavaScriptconst hello = React.createElement('div', { id: 'hoge' }, `Hello ${this.props.toWhat}`)React.crateElement の引数は下記のようになっています。
JavaScriptReact.createElement( type, // タグ名の文字列 [props], // タグに付与する属性 [...children] // 子要素 )では TypeScript で React.createElement を実装してみます。
TypeScriptconst TEXT_ELEMENT = 'TEXT_ELEMENT' as const type TagType = string // ここの拡張を頑張るとタグの型定義ができる type ElementType = typeof TEXT_ELEMENT | TagType interface Props { nodeValue?: string children: Element[] [key: string]: any // タグに付与する属性が入る } interface Element { type: ElementType props: Props } const createTextElement = (text: string): Element => ({ type: TEXT_ELEMENT, props: { nodeValue: text, children: [] } }) const createElement = (type: ElementType, props: Props, ...children: Element[]): Element => ({ type, props: { ...props, children: children.map(child => (typeof child === 'object' ? child : createTextElement(child))) } })
crateElement
を実行すると、children に対して再起的にcreateElement
を実行します。そして子要素を持たない末端のノードに対してはcreateTextElement
を実行します。下記に実行例を挙げます。
const sample = ( <div id="foo"> <a href="/" target="_blank"> link <span id="baz">hoge</span> </a> </div> )を
const sample = { type: 'div', props: { id: 'foo', children: [ { type: 'a', props: { href: '/', target: '_blank', children: [ { type: 'TEXT_ELEMENT', props: { nodeValue: 'link', children: [] } }, { type: 'span', props: { id: 'baz', children: [{ type: 'TEXT_ELEMENT', props: { nodeValue: 'hoge', children: [] } }] } } ] } } ] } }のような構造に変換してくれる関数が
createElement
です。
おそらくこの Element のツリー構造がいわゆる仮想 DOM に当たるのだと思います。React 開発者から見る・操作することができる DOM ツリーが上記 Object っぽいです。こうして変換された Element を DOM に反映させるための
createDOM
を実装します。TypeScriptconst isEvent = (key: string) => key.startsWith('on') const isProperty = (key: string) => key !== 'children' && !isEvent(key) const toEventType = (key: string) => key.toLocaleLowerCase().substring(2) const createDom = (element: Element) => { if (element.type === TEXT_ELEMENT) { return document.createTextNode(element.props.nodeValue!) } const dom = document.createElement(element.type) Object.keys(element.props) .filter(isEvent) .forEach(name => dom.addEventListener(toEventType(name), element.props[name])) Object.keys(element.props) .filter(isProperty) .forEach(name => dom.setAttribute(name, element.props[name])) return dom }子要素を持たない末端のノードは TextNode として生成します。
末端のノード以外は通常の HTMLElement として生成します。on から始まるイベント属性をaddEventListener
で設定し、その他の属性はsetAttribute
で設定します。JSX (TSX) -> DOM 生成までの繋ぎこみが上記の実装により完了しました。
レンダリング
生成された DOM を実際に描画させます。描画に使用するのが requestIdleCallback です。ブラウザがアイドル状態の時に、既に描画済みの DOM に子として生成された DOM を追加します。
TypeScriptexport type Fiber = | ({ dom: HTMLElement | Text parent?: Fiber child?: Fiber sibling?: Fiber } & Element) | null const requestIdleCallbackFunc = (window as any).requestIdleCallback let nextUnitOfWork: Fiber = null let wipRoot: Fiber = null const commitWork = (fiber: Fiber) => { if (!fiber) { return } const domParent = fiber.parent.dom domParent.appendChild(fiber.dom) commitWork(fiber.child) commitWork(fiber.sibling) } const commitRoot = () => { commitWork(wipRoot.child) wipRoot = null } const workLoop = () => { while (nextUnitOfWork) { // performUnitOfWork() は後述 nextUnitOfWork = performUnitOfWork(nextUnitOfWork) } if (!nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallbackFunc(workLoop) } requestIdleCallbackFunc(workLoop)ここで Fiber なる概念が登場します。上記の Element の要素に加えて、DOM の実体、親・子・兄弟 Fiber への参照を持っています。React の描画処理は Fiber を元に実行されます。
現時点の実装ではブラウザがアイドル状態になった時にworkLoop
を呼びだし、全ての DOM を更新しています。差分検出(Reconciliation)は未実装です。
performUnitOfWork
は差分検出を実装することを見据えて分割して呼び出せるようになっています。TypeScriptconst performUnitOfWork = (fiber: Fiber) => { if (!fiber.dom) { fiber.dom = createDom(fiber) } const elements = fiber.props.children let index = 0 let prevSibling: Fiber = null while (index < elements.length) { const element = elements[index] const newFiber: Fiber = { type: element.type, props: element.props, parent: fiber, dom: null } if (index === 0) { fiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null }
performUnitOfWork
は何をやっているのかというと
- Fiber に DOM の実体がなければ生成
- 次に実行する Fiber を返す
- 子 Element があれば子 Fiber を生成して返す
- 子 Element がなく、兄弟 Fiber があれば返す
- 子 Element・兄弟 Fiber がなく、親の兄弟 Fiber があれば返す
- 以降、親要素の兄弟 Fiber を探し続ける。なければ null を返す
上述したように、Element を Fiber 単位に基づいて DOM の描画処理を行います。
最後に
render
関数を実装します。const render = (element: Element, container: HTMLElement) => { wipRoot = { type: container.tagName, props: { children: [element] }, dom: container } nextUnitOfWork = wipRoot } const sample = ( <div id="foo"> <a href="/" target="_blank"> link <span id="baz">hoge</span> </a> </div> ) const container = document.getElementById('root') render(sample, container)これで描画まで実行されるようになりました。
疲れた
まだ差分検出と Funciton Component を紹介していませんが、ここで一旦終わりにします。「差分検出がなくて何が React なんだ!」との声もあるでしょうが、勘弁してください。年内を目標に続きの記事を書きます。実装自体は完了しているので興味がある方は こちら を見てください。
最後に
TypeScript 最高です。バニラの JS だと写経でも写し間違えに気づかず詰んでいた可能性すらあります。リファクタリングや思考の整理にも、型があるとないとでは捗りに天地の差があると思います。なるべく新規の案件には導入していきましょう!
明日は @naohikowatanabe さんで「要素の表示非表示は visibility:hidden の方が display:none よりも高速」だそうです。お楽しみに。
- 投稿日:2019-12-24T12:16:44+09:00
ReactJS react-routerのサンプルアプリ
非常にシンプルな実装例のメモ!
github: https://github.com/Kohei-Sato-1221/SugarReactRouterreact-routerとは?
Reactでルーティングを実現するためのライブラリ
前提条件
- npmコマンド使えるようにしておく
- create-react-appを使えるようにしておく
実装方法
サンプルのReactプロジェクトを用意
create-react-app router-sampleライブラリをインストール
cd router-sample npm install react-router-domApp.jsを以下の通り書き換える
App.jsimport React from 'react'; import {Route, BrowserRouter, Link} from 'react-router-dom' const App = () => ( <BrowserRouter> <div> <div><Link to='/page1'>Go to Page1</Link></div> <div><Link to='/page2'>Go to Page2</Link></div> <br/> <Route path='/page1' component={Page1} /> <Route path='/page2' component={Page2} /> </div> </BrowserRouter> ) const Page1 = () => ( <div>This is Page1</div> ) const Page2 = () => ( <div>This is Page2</div> ) export default App;以下のコマンドで起動
npm start
- 投稿日:2019-12-24T11:43:35+09:00
GraphCMS から入り、Absintheを利用して作って動かす「チュートリアル」
この記事は、「Elixir Advent Calendar 2019」の24日目になります。
「Elixir Advent Calendar 2019」23日目は、@sanpo_shihoさんのElixirで作るニューラルネットワークを用いた手書き数字認識①でした。
そしてこの記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2019」の1日目から始まった3部作の3つ目の記事で、Advent Calendar NervesJP 6日目の続きになります。
東京だけど fukuoka.ex の YOSUKENAKAO.me です。
普段は合同会社The Waggleで「教育」に関わるサービス作りのお仕事と学習教材の開発や
研修講座の企画開発をしています。この記事の構成
Advent Calendar fukuoka.ex 1日目
https://qiita.com/advent-calendar/2019/fukuokaex
GraphCMS から Absinthe を利用して作る Elixir で体験的に GraphQLSever を作る「ポエム」Advent Calendar NervesJP 6日目
https://qiita.com/advent-calendar/2019/nervesjp
Nerves と GraphQLsever の組み合わせを考える「ポエム」Advent Calendar Elixir 24日目
https://qiita.com/advent-calendar/2019/elixir
GraphCMS から入り、Absintheを利用して作って動かす「チュートリアル」となっています。
対象者はプログラミングを始めたい人で、新しい技術を学びたい人
逆を言えば、このチュートリアルができない人で、プログラミングを学びたい人は
Elixir |> Collegeの対象者です。また、このチュートリアルは完了して、日本語で学びたい人は TheWaggleのオンライン教材の対象者(2020年春頃ローンチに向けて作成中)です。
注意点
このチュートリアルは、最短で何を学ぶべきかと、流れを通してそれぞれの技術的な繋がりを把握してもらう為のものです。
独学ができる人に向けた最初の入り口です。
PHP? Python? Ruby? がやりたいだって?やりたい勉強すりゃいいよ。ただし一言だけ言っておく、Elixirは良いぞ(笑)
- HTML,CSS,JavaScript
- React
- GraphCMS
- Elixir
- Phoenix
- Ecto
- GraphQL(Queryのみ)
- Apollo
- Absinthe
- SQL
- mix
チュートリアルの流れ
- GraphCMSのアカウント登録
- GraphCMSでModelの作成
- Reactの環境構築(Yarnが使える人は飛ばす)
- サンプルプログラムのダウンロードと作成
- サンプルデータの作成
- GraphCMS Endpointの設定
- ElixirでGraphQL Server構築
1.GraphCMSのアカウント登録
下記のサイトへアクセスしてください。
https://graphcms.com/5. 「Asia East」のリージョンを選びます。
6. Pearsonalプランを選択し、Continueを選びます。
7. モデルの作成から順に行って行きます。
2. GraphCMSでModelの作成
8. Schemaの設定
Schemaと書かれたタイトルの下にModelsと書かれていてサイドに[+]のボタンがあります。
9. Create Modelで「Display Name」の欄にAuthorと入力し、Create Modelを押します。
10. 右側にFIELDSと書かれたタブが表示されるので、クリックします。
11. 様々なFiledsのタイプがあります。今回は、Single Textをドラッグしてドロップします。
12. Display Nameに nameと入力します。
13. フィールドタイプが追加されるとこのような形になります。
同じ要領で、Authorのフィールドは以下のように設定します。
14. Authorを設定したら新しく、ModelをPostで作成します。フィールドは以下のようにします。
3.Reactの環境構築(Yarnが使える人は飛ばす)
NodeJSのインストール
LTS版と最新版があります。 LTS版をインストール
インストールの確認
コンソールを再起動します。
node --version次の
v12.13.1
ようにバージョン番号が表示されていたらインストール完了です。npm --version次の
v6.12.1
ようにバージョン番号が表示されていたら成功です。Yarn install
https://yarnpkg.com/ja/docs/install#mac-stable
Windowsの場合は、yarn のインストーラーをダウンロードできるボタンが表示されるので
それをダウンロードしてインストールして下さい。4. サンプルプログラムのダウンロードと作成
https://github.com/GraphCMS/graphcms-examples/tree/master/current/react-apollo-blog
上記のサイトに下記コマンドを実行と記載があるので、下記のコマンドを実行します。
git clone https://github.com/GraphCMS/graphcms-examples.git && cd graphcms-examples/current/react-apollo-blog && yarn && yarn start実行すると、ブラウザでlocalhost:3000で以下のサイトが見れたら成功です。
5. サンプルデータの作成
- 「content」ボタン(三本の横棒マーク)をクリックし、データを入力したい、「Model名」(画像の例は、Author)を選びます。
- 「Create New」をクリックすると、入力フィールドが表示されるので、適当な値を入れて「Published」を選んで「Save」します。
※Postにも忘れずに、同じ要領でサンプルデータを作成してください。
6. GraphCMS Endpointの設定
設定から、Endpointをコピーします。
Endpointをコピーしたら、設定より、PermissionsのScopeを「PROTECTED」から「QUERY」に変更します。
Endpointをプログラムに反映する
react-apollo-blog/src/index.js// Replace this with your project's endpoint const GRAPHCMS_API = 'ここに先ほどコピーしたEndpointのURLを入れる'ここまできたら
yarn start
で無事に自分の入力したデータが表示される事を確認します。ここからは、こちらのブログで書いた手順の5番目以降の説明と同じものになります。
https://qiita.com/Yoosuke/items/2346137ac39715b19cde6. Absinthe を使用して GraphQL のアプリをセットアップします。
mix.exsdefp deps do [ # existing dependencies {:absinthe, "~> 1.4.16"}, {:absinthe_plug, "~> 1.4.0"}, {:absinthe_phoenix, "~> 1.4.0"} ] endmix deps.get7.sampleBlog_web に schema フォルダを作成し、schema.ex ファイルを作成して、ファイルを作成します。
lib/sampleBlog_web/schema/schema.exdefmodule SampleBlogWeb.Schema.Schema do use Absinthe.Schema query do @desc "Get a list of authors" field :authors, list_of(:authors) do resolve &SampleBlogWeb.Resolvers.Blog.authors/3 end @desc "Get a author by its id" field :author, :author do arg :id, non_null(:id) resolve &SampleBlogWeb.Resolvers.Blog.author/3 end end object :author do field :id, non_null(:id) field :name, non_null(:string) field :bibliography, non_null(:string) end end8.リゾルバモジュールファイルを作成します。 sampleBlog_web に resolvers フォルダを作成し、blog.exファイルを作成して、ファイルを作成します。
lib/sampleBlog_web/resolvers/blog.exdefmodule SampleBlog.Resolvers.Blog do alias Getaways.Blog def authors(_, _, _) do {:ok, Blog.list_authors()} end def author(_, %{id: id}, _) do {:ok, Blog.get_author!(id)} end end9.スキーマとリゾルバの準備をしたら、ルーターを設定します。
sampleBlog/lib/sampleBlog_web/router.exdefmodule SampleBlogWeb.Router do defmodule SampleBlogWeb.Router do use SampleBlogWeb, :router pipeline :api do plug :accepts, ["json"] end scope "/" do pipe_through :api forward "/api", Absinthe.Plug, schema: SampleBlogWeb.Schema.Schema forward "/graphiql", Absinthe.Plug.GraphiQL, schema: SampleBlogWeb.Schema.Schema, interface: :simple end end最後にサーバーを起動してみます。
mix phx.serverこれで
localhost:4000/graphiql
にアクセスして以下の画面が出てきたら作成準備完了です。GraphQLのクエリを書く
query { authors{ id name bibliography } }クエリを書いて、無事に成功していれば下記のようなデータが返ってきます。
React のreact-apollo-blog のAbout.jsに上記のクエリを上書きする
src/components/About.jsexport const authors = gql` query authors { authors{ id name bibliography } } `エンドポイントを書き換える
src/index.jsconst GRAPHCMS_API = http://localhost:4000/api/これで、
yarn start
してlocalhost:3000/about
ページにアクセスすると、、、見れません。
エラーを確認するとクロスサイトスクリプティングの問題でデータが取得できてないです。そこで、GraphQL server側に機能を追加します。Cros_plugを追加する
https://hex.pm/packages/cors_plug
mix.exsdefp deps do [ # ~省略 {:absinthe, "~> 1.4.2"}, {:absinthe_plug, "~> 1.4.0"}, {:absinthe_phoenix, "~> 1.4.0"}, {:cors_plug, "~> 2.0"}, #<- 追加 ] end機能を追加する。
$ mix deps.getrouter.exのパイプラインにプラグを追加
lib/sampleBlog_web/router.expipeline :api do plug CORSPlug, origin: "http://localhost:3000" #<-追加 plug :accepts, ["json"] endこれで、
http://localhost:3000/about
にアクセスで以下のようにデータが取得できたら成功です。最後に
いかがだったでしょうか、あまりにも長いのでもしかしたら、抜け漏れがあるかもしれません。
見つけたら遠慮なく、おしらせ下さい。喜んで修正したいと思います。よければ、いいね。よろしくお願いします。励みになります。
- 投稿日:2019-12-24T10:04:06+09:00
React × Firebase でエンジニア向け特化型SNSを開発しています
これは、ひとり開発 Advent Calendar 2019 の23日目の記事です。
はじめに
新卒( 2019年12月で退社 & 転職活動始めます!)でエンジニアをやっている筆者が、友人と一緒にここ数ヶ月開発しているサービスについて書きたいと思います。ちなみに、2020年1月中にα版をリリース予定、3月1日にはこのサービス開発についてまとめた本を技術書展で出版します。みなさん、技術書展の会場でお会いしましょう。
開発中サービスについて
エンジニアのためのプラットフォーム: 「Jeeek (ジーク) 」
サービス名についてですが、色々あってとりあえずこの名前にしています。由来はあるのですが今回の記事では省略します。
概要を最初にざっくり言うと、エンジニアの活動 (学習・転職) を支援するSNSライクのプラットフォームです。詳しくは、以下で述べていきます。
Why?<なぜ作ったのか>
まずは、なぜJeeek(ジーク)を開発・リリースしようと考えたのかを書いていきます。
活動共有、コミュニティ形成の需要があると感じたから
Twitterでは、 「#100DaysOfCode」 や 「#未経験エンジニアと繋がりたい」 といったハッシュタグをよく見かけた時期がありました。これらは、自身の活動の共有を行いたい・共有することで自分にプレッシャーをかけ継続させたい、同じような境遇の人同士で切磋琢磨し合いながら成長していきたい、などの想いがあると思います。また、他分野からIT業界への流入、プログラミング義務教育化など、時代の潮流と共にITエンジニアの増加が想定されます。
しかし、エンジニアが対象とする技術分野は幅が広く、初学者にとっては情報の取捨選択が難しいこともあり、初期は何から勉強をしたら良いのか、また実際に手を動かした後も何が分かっていないのか分からないといった状態が生まれやすいです。経験の浅いエンジニアと経験豊富なエンジニアの間に存在する情報の非対称性を狙って不当に高額なプログラミングスクールなどの情弱ビジネスも横行しています。これでは、たとえITエンジニアの人口が増加したとしても脱初心者にとても時間がかかってしまいます。こういった現状を変え、エンジニアの活動がより身近で楽しいものになったり、相互に刺激し合う環境がオンラインで構築できたり、駆け出しエンジニアの勉強コンサルにもなるようなサービスがあったら面白いのではないかと考えサービス開発を始めました。
How?<どう実現するのか>
次に、「じゃあどうやってそれを実現していこうと考えているの?」ということについてです。基本的に、現在実装中のものはエンジニア向けに特化したSNSというイメージで、今後これに機能を追加しながらエンジニアの活動全体を支援するプラットフォームにまで拡張していく予定です。
ユーザーの活動をSNSライクのタイムラインで共有します
メインとなるのがタイムライン機能です。普段の活動をテンプレートベースのシンプルな投稿で共有します。例えば、『◯◯技術書の1章を読んだ』『◯◯のイベントに参加した』などです。また、エンジニアの活動はインターネットにアウトプットされることが多いことから、外部サービスでの活動を自動収集および自動投稿も可能です。外部サービス (GitHub, Qiita, connpassなど) のAPIを利用して連携させることで、外部サービスでのアクティビティを自動で取得しタイムラインで共有します。例えば、GitHubと連携済みユーザーであれば『新しく1つのcommitをしました』のような投稿を自動で行うことができます。
また、日々の投稿のログ (投稿に設定するタグ) から、ユーザーのスキルスタックを可視化・共有します。そして、それらに基づいて特定の技術でのユーザーランキングも表示します。
What?<ユーザーはどうなるのか>
次に、「Jeeek(ジーク)のユーザーはどういうことが嬉しくて利用するの?」ということについて書いていきます。
他のエンジニアの活動を自身の活動に役立てることができる
タイムラインで活動を共有することで、他のエンジニアのアクティブな活動を知ることができ、切磋琢磨する環境ができたり、良質な記事・文献にアクセスしやすくなります。また初学者にとっては、ロールモデルを発見できる可能性が高まり、自身の勉強の指針にもなります。これに加えて、技術ベースでランキングが確認できるので、自身の立ち位置を客観的に認識することげできます。
また、ユーザーのスキルスタックが公開されるので、スカウトする/されるの機会を作ります。これらのスキルスタックは日々の活動に紐づいて自動生成されるので、定期的に手動でアップデートする必要もありません。
ちなみに、スキルスタック自動生成 + 企業とマッチングについては、LAPRASさんのサービスが有名です。これに対して、Jeeek(ジーク)はより粒度の細かい活動を共有したり、和気あいあいとしたアクティブなコミュニティ形成を実現したいため、SNSの機能に重きを置いています。
開発環境
最後に、Jeeek(ジーク)の開発環境について簡単に紹介します。
技術スタック
使用技術やツールなどは全て上の図に載っている通りです。
インフラにGCP、バックエンドにGo、フロントエンドにTypeScript × Reactを使っています。ちなみに僕はフロントエンドを担当しており、TypeScript, React, Redux, Redux-Saga, Figmaなどを使用しています。
おわりに
プライベート開発では、サービス企画からスプリントプランニング、UI設計、実装、リリーススケジュール、リリース後のアップデートおよびマーケティングまで全て自分たちで考えながら実行・経験できることがとても面白いと感じています。また、それと同時にその大変さも感じ、実際にリリースや運用を行っている全個人開発者のことをリスペクトするようになりました。Jeeek(ジーク)も僕個人もまだまだ道半ばですが、引き続き精進していきます。冒頭でも述べましたが、これに関連した本を技術書展で出版するので、次は3月1日の技術書展でお会いしましょう!
- 投稿日:2019-12-24T08:10:45+09:00
【React】useEffectの第2引数って?
概要
ReactのHooksに関して学び始めた際に
useEffect
の使用方法、特に第2引数部分の意味の理解に苦しんだので、そのまとめです。useEffectとは
ReactのHooksの1つです。
公式では、副作用を実行するフック
のように説明されています。
ざっくり言うと、ライフサイクルメソッドを関数コンポーネントで実現する為に使われたりします。この
useEffect
ですが、2つの引数を持ちます。まず第1引数
デフォルトではRender毎に実行される関数を取ります。
useEffect(() => { console.log("毎回実行"); });そして第2引数
第2引数を与える事で第1引数の関数が実行されるタイミングを自在にコントロールする事ができます。
下記の2つのケースを考えます。(1) 空の配列が渡された場合
第2引数に空の配列が渡された場合、
マウント・アンマウント時
のみ第1引数の関数を実行します。// マウント・アンマウント時のみ第1引数の関数を実行 useEffect(() => { console.log('マウント時のみ実行') }, [])(2)値の配列が渡された場合
第2引数に値の配列が渡された場合、
最初のマウント時と与えられた値に変化があった場合
のみ第1引数の関数を実行します。// 与えられた値に変化があった場合のみ第1引数の関数を実行 useEffect(() => { console.log('変化があった場合の実行') }, [value])まとめ
- 第2変数を指定なし=>
Render毎
に第1引数の関数を実行。- 第2変数に
[]
を指定 =>マウント時とアンマウント時
に第1引数の関数を実行。- 第2変数に値の配列を指定 =>
マウント時と指定された値に変化があった場合
のみに第1引数の関数を実行。参照
本記事では非常に簡単な
useEffect
の説明を行いました。
より詳しく、正確な詳細等は下記を参照してください。
- 投稿日:2019-12-24T01:32:05+09:00
レガシー React Project で実践してきたこと
はじめに
この記事はギルドワークス Advent Calendar 2019の21日目の記事になります。
現在、約3年以上開発を続けているReactのProjectに関わっているので、ここ半年で取り組んだカイゼンの内容についてここにまとめたいと思います。
改善前(半年前)のProjectの状況
以下が半年前のProjectの状況です。
現在と構成が変わっていない部分はあります。
- Rails + Webpacker + Reactの構成
- React.js v16.4.x
- Redux v4.0.0
- ただし状態管理はreduxのstoreを使っていたり、Componentのstateを使っていたり統一感がない状態
- 非同期処理のmiddlewareには redux-saga を使用
- UI補助ライブラリとしてmaterial-uiとbootstrapを併用
- Component単位で依存しているUI補助ライブラリが違っている状態
当時の課題
以下が、当時のふりかえりで上がってきたReact Projectに対する主な課題でした。
- Reactのコードがどんどん増えてきて、状態管理もstate, props, reduxのstoreと使い分けができておらず複雑になってバグが多くなってきた
- テストしようにもComponent間の依存がすごくて難しくなってきた
- なので大胆な変更をしようにも状態管理まわりでバグを生みそうで怖い
- コーティングガイドラインもちゃんと統一されていなかった
- Reactのコードが増えてきて読み込みに時間がかかるようになってきた
こういった課題が顕著にでてきていたので、自然とReactまわりのコードを徐々に改善していく運びになっていきました。
やってきたこと
ではここ半年で実施してきた施策について挙げていきます。
TypeScriptの導入
まずは、大胆な変更を行うためにはTypeScriptによる静的解析が必須であると考えました。
ただいきなりTypeScriptを導入して型を定義していくのはかなり敷居が高いので、以下の順序で導入していきました。
- まず既存のコードをいっきに jsからts, jsxからtsxファイルへ置換する
- TypeScriptを 型チェックを有効にしないで コンパイラのみ有効にした状態にする
const PnpWebpackPlugin = require('pnp-webpack-plugin') module.exports = { test: /\.(ts|tsx)?(\.erb)?$/, use: [ { loader: 'ts-loader', options: PnpWebpackPlugin.tsLoaderOptions({ transpileOnly: true // 型チェックしない }) } ] }
- 警告が出ている箇所はロジック変更をしない形で修正を加えていく
- 最終手段として
@ts-ignore
でエラーを回避- 依存しているライブラリの型定義をインストールしていく (@types/react等)
ここまで実施して、次の項で導入したESLintでTypeScriptの型チェック等の制限を徐々に強くしていって、段階的にコードを修正していく形になります。
参考
当時は以下の記事を参考にして、TypeScriptを軽量に導入する方法の知見を得たりしました。
ESLint + Prettierの導入
TypeScriptが導入された状態で、段階的に型を有効にしていくために、ESLintの
plugin:@typescript-eslint
を有効にして、以下の手順でコードを徐々に修正してく手段をとりました。
- デフォルトの設定だと当然エラーが大量にでてしまうので、地道にエラーとなっている箇所を
warn
に変換して警告がでている状態にする- CI環境では
eslint
がパスする状態にする- 機能追加や修正が入るファイルから徐々に警告の箇所を修正する
- state, props等型定義しやすいところから型定義していく。難しい場合は
any
で回避- 解消した警告から eslintの設定を見直して制限を強くしていく
なお、細かなコーティングガイドラインが無いという課題に対しては、ESLintの設定で
airbnb-base
等一般的なコーディングスタイルをlintに組み込むことができるので、開発メンバーのエディタにlinterとPrettierを有効にしてもらって、自動でコーディングの強制を(ゆるく)実施するようにしました。Prettierは自動でコード整形をしてくれるのでかなり有効です。
参考
既存ComponentからなるべくState管理の排除
せっかくreduxが導入されているにも関わらず、stateで状態を保持しつつ、同じ情報を複数のComponentで引き回すようなコードがいくつかあり、それが複雑性を上げてバグを生みやすくしていました。
具体的なアンチパターンとしては、以下のようなpropsによってstateを変更するようにしているケースです。
this.state.open
はpropsによって更新もされますが、Component内でも変更ができてしまうため、状態管理が複雑になってしまいます。constructor(props) { super(props); this.state = { open: this.props.open, }; } componentWillReceiveProps(nextProps) { this.setState({ open: nextProps.open, }); }こういったコードは、stateを排除してprops参照にする、あるいは reduxのstoreに openの状態をもたせるようにして、状態を一箇所で管理するようにします。
ちなみに LifeCycle関数である
componentWillReceiveProps
は Deprecatedになっているので積極的に排除していっています。参考
React hooksの導入
前項のState管理の排除は既存に対する対処ですが、React Hooksが
React v16.8
から有効になってからは、新規で作成するファイルは積極的にReact.FC
を用いてステートレスな関数Componentを実現してState管理の複雑性を排除していきます。参考
Dynamic Importの導入
Reactのコードが増えてきて、且つ機能が複数ページに跨っていたため、Dynamic Importを利用して機能単位で必要になったファイルをロードするようにして、初期ロード時間の短縮を図りました。
以下のような記述に変更してあげることで実現可能になります。
import React, { lazy, Suspense } from 'react'; const HogeContainer = lazy(() => import(/* webpackChunkName: "HogeContainer" */ '../containers/HogeContainer')); const App = (props) => ( <Suspense fallback={<Loading />}> <HogeContainer /> </Suspense> ); export default App;なお、Dynamic Importは
React v16.8
から有効になっています。参考
今後の導入したいこと
ここからは、まだ導入できていないけど、今後の改善として導入を検討している施策を挙げてみました。
Redux Starter Kitの導入
Reduxは状態管理やロジックを一箇所で管理できるものの、TypeScriptによる型定義が難しかったり、学習コストがかかたっりするところが難点だなと思っています。そこで今年(2019年)の10月に
v1.0
がリリースされたRedux Starter Kit
の導入を検討しています。Reduxのラッパーのようなものなのですが、いくつか恩恵があります。
- TypeScriptの型定義がしやすい
- React Hooksを利用する前提の設計になっている
- Sliceという機能を使ってreducerの記述を簡潔にできる
以下がreducerのサンプルコードです。reducerの関数の呼び出しもかなりシンプルになっています。
import { createSlice } from "redux-starter-kit"; export type TodoItem = { title: string; completed: boolean; key: string; }; const todoSlice = createSlice({ name: "todo", initialState: [] as TodoItem[], reducers: { addTodo: (state, action: { payload: TodoItem }) => { state.push(action.payload); }, removeTodo: (state, action: { payload: string }) => { return state.filter(item => item.key !== action.payload); }, setCompleted: ( state, action: { payload: { completed: boolean; key: string } } ) => { state.forEach(item => { if (item.key === action.payload.key) { item.completed = action.payload.completed; } }); } } }); export default todoSlice;import React from "react"; import { ListGroupItem, Button } from "reactstrap"; import todoSlice, { TodoItem } from "../../reducers/todo"; import { useDispatch } from "react-redux"; type Props = { item: TodoItem; }; const TaskItem: React.FC<Props> = props => { const dispatch = useDispatch(); const { actions: { setCompleted, removeTodo } } = todoSlice; const textStyle = { textDecoration: props.item.completed ? "line-through" : "none" }; const completeTask = () => { dispatch(setCompleted({ completed: true, key: props.item.key })); }; const deleteTask = () => { dispatch(removeTodo(props.item.key)); }; return ( <ListGroupItem> <div className="d-flex"> <span className="flex-fill" style={textStyle}> {props.item.title} </span> <div className="ml-auto"> {props.item.completed ? null : ( <Button color="primary" onClick={completeTask}> Complete </Button> )} <Button color="danger" onClick={deleteTask} className="ml-3"> Delete </Button> </div> </div> </ListGroupItem> ); }; export default TaskItem;参考
カスタムHooksの導入
React hooksの強みは、独自のhooksを作成して、複雑な処理を共通化して流用可能にしつつ、ステートレスな関数Componentを実現できる点です。
例えばシンプルな例ですと、reduxの特定のstore情報の呼び出しをカスタムhooks化して簡潔に呼び出すといったことも可能になります。
import { useSelector } from "react-redux"; import { CombineState } from "../index"; export const useTodoItems = () => { return useSelector((state: CombineState) => state.todo); };import React from "react"; import { ListGroup } from "reactstrap"; import { useTodoItems } from "../../hooks/todo"; import TaskItem from "./TaskItem"; const TaskList: React.FC = () => { const items = useTodoItems(); return ( <ListGroup className="mt-3"> {items.map((item) => { return <TaskItem key={item.key} item={item} />; })} </ListGroup> ); }; export default TaskList;また、外部ライブラリとして多くの便利Hooksが公開されていたりするので、Hooksのエコシステムを上手く活用して綺麗なComponentの記述を実現することもできそうです。
参考
さいごに
かなり長くなってきましたが、ここ半年で取り組んできたレガシーReact Projectに対する取り組みと、今後の改善点の一部を上げてみました。ここには長くなって書けませんが、テストまわりの取り組み(Cypress等)についてもどこかで書ければと思います。
それでは、よりよいReactライフを!