- 投稿日:2020-01-16T22:16:20+09:00
React+Typescript+spring bootで勉強会情報をSlackに通知するサービスを作りました
概要
タイトル通りReact, Typescript, spring bootを用いて個人開発を行いましたので、備忘録としてここに記します。
作ったもの
名前
勉強会情報通知サービス「noroshi」
サービス内容
使い方をnoteに書きました✨
https://note.com/uusu/n/n8441acfd64daconnpassやDoorkeeperの勉強会情報をSlackに通知するサービス。
地域・キーワードで絞り込みができるので、欲しい勉強会情報を取得できます。
また、通知設定は複数登録することができます。
自分はjava勉強会情報チャンネル
とJS勉強会情報チャンネル
それぞれに通知がいくようにnoroshiを使っています。モチベーション
興味があるイベントを知ったときには既に募集が終了していたり、満員で参加できなかった経験からこのサービスを開発しました。
また、新たな技術に挑戦したいという気持ちも強かったので、モチベーション維持のためReactやTypescript、kotlinを採用しました。開発期間
全部合わせて二か月くらいです。
使用技術・ツール
技術
アプリケーション
- React (コンポーネントは全てhooksを用いた関数型コンポーネントで作成しました)
- Typescript
- spring boot
- java
- kotlin (サービス層や一部モデルなどをkotlinで書きました)インフラ
- heroku
- netlifyツール
- intellij
- vscode
postman
illustrator (ロゴやトップ画面イラスト作成に使用)
システム構成図
フロントはreact/tsで書いたものをbuildしてnetlifyでホスティングしています.
バックエンドはspring bootで書いたアプリケーションをherokuで動かしています.
バックエンドはAPIとバッチが共存している構成です.認証周りはJWTを利用しています.
(あまり詳しくないため断言できませんが,JAMstackに近い構成になっているかと思います)
技術的な知見
個人的に詰まった所などを記します。
spring boot
複数の実装があるinterfaceのDI方法
今回、勉強会情報をconnpassさん、doorkeeperさんのAPIから取得しています。
それぞれAPIレスポンスが違いますが、最終的には取得した情報をサービス共通で利用する勉強会モデル詰めて利用しています。
そのため、共通のイベント情報モデルリストを返すメソッドを定義したインターフェースを、connpassイベント取得クラス・doorkeeperイベント取得クラスで実装しておりますそのような複数実装のあるインターフェースをDIする際は、利用する側でList型でラップすると複数の実装クラスがインジェクションされます。
以下のように書きます。
(RequiredArgsConstructorを用いたコンストラクタインジェクションの場合)@Service @RequiredArgsConstructor class StudyGroupService( // StudyGroupSiteがインターフェース val studyGroupService: List<StudyGroupSite>? ・ ・ ・foreachなどで共通メソッドを呼び出しを行えます。
studyGroupService?.forEach { e -> e.インターフェースに定義したメソッド()} }とても簡単なことですが、今まで知りませんでした。
こういった基礎的な知識の見落としに気づけるのは個人開発をするメリットですね。React
hooksでステートを更新すると一部ステートがundefinedになってしまう
言葉で説明するのがめんどくさいので、簡単に再現してみます。
以下二つのファイルをHTMLファイルに張り付けてブラウザで開くと動作します。
classComponent.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ClassComponent</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
a: 0,
b: 0
};
this.handleChangeA = this.handleChangeA.bind(this);
}
handleChangeA(a) {
this.setState({ a: this.state.a + a });
}
render() {
console.log(`a: ${this.state.a}, b: ${this.state.b}`);
return (
<div>
<p>a: {this.state.a}</p>
<p>b: {this.state.b}</p>
<button onClick={() => this.handleChangeA(1)}>a+1</button>
</div>
);
}
}
ReactDOM.render(<ClassComponent />, document.getElementById("root"));
</script>
</body>
</html>
FunctionComponent.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>FuntionComponent</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const FuncComponent = props => {
const [state, setState] = React.useState({ a: 0, b: 0 });
const handleChangeA = a => {
setState({ a: state.a + a });
};
console.log(`a: ${state.a}, b: ${state.b}`);
return (
<div>
<p>a: {state.a}</p>
<p>b: {state.b}</p>
<button onClick={() => handleChangeA(1)}>a+1</button>
</div>
);
};
ReactDOM.render(<FuncComponent />, document.getElementById("root"));
</script>
</body>
</html>
二つは同じ処理をするクラス型コンポーネントと関数型コンポーネントになります。
処理内容はaとbというステートを持ち、a+1というボタンを押すとaのみ1加算されるといったものです。実際に動かしてみると、setStateの内容は同じなのに関数型コンポーネントではbがundefinedになってしまいます。
このステート書き換えでundefinedになってしまう問題のせいで1日潰れました。
しかし,改めて公式ドキュメントを読むと以下のような記述があります。しかしクラスでの this.setState とは異なり、state 変数の更新は、マージではなく必ず古い値を置換します。
hooksでは、古いステートが引き継がれないとしっかり公式のドキュメントで書いていました..
クラスコンポーネントと同じ動作にするためには、ステート更新処理を以下のように修正する必要があります。
const handleChangeA = a => { // スプレッド構文を用いて、古いステートも記述する setState({ ...state, a: state.a + a }); };これで無事hooksを用いた関数型コンポーネントでステート更新を行うことができました。
これが起きてしまった原因は、自分自身Reactを触るのがこれで初で、最初はクラスコンポーネントベースの開発の勉強をしていたため、知識がごちゃごちゃになってしまったことと
単純に公式ドキュメントをしっかり読まなかったことですね。これを反省し、新たなものを学ぶ際はできるだけ公式ドキュメントや公式が用意してるチュートリアルをやろうと思います。。
useEffectで無限にAPIをコールしてしまう
今回関数型コンポーネント+hooksで全て開発しています。
そこで何らかのリストなどをAPIから取得する際は、useEffect内でAPIコールを行っております。
useEffectは、コンポーネントのレンダーが終わるたびに実行されるメソッドです。
詳しくは公式ドキュメントを確認してください最初に以下のようなコードを書きました.
useEffect(() => { APIをコールするメソッド() .then(success => { setState({...state, ステートの更新}) }) })コンポーネントはステートが更新される度に再レンダーされます。
そのため、このコードではレンダー→useEffect内でAPIコール→ステートが変わるので再レンダー→useEffect内でAPIコール→ステートが変わるので再...
といったように無限ループが発生してしまいます。この問題は以下のスタックオーバーフローを発見し解決することができました。
https://stackoverflow.com/questions/53070970/infinite-loop-in-useeffectuseEffect(() => { APIをコールするメソッド() .then(success => { setState({...state, ステートの更新}) }) }, []) // useEffectの第二引数に[]を置くこのように第二引数に[]を指定すると一度だけ実行される副作用となるため、無限ループを回避できます。
このスタックオーバーフローを見つけた後に公式ドキュメントを見ると、同じような事が書いてあり思わず自分を殴りました。
最後に
コードの品質などまだまだ改善できるところがあるので、暇を見つけてちょこちょこと修正していきたいです。
まず、もっとテストをしっかり書きたいですね。バックエンドはもちろんですが、フロントエンドのテストは書いたことがないので挑戦してみたいです。あとは、使用技術の強みを活用できていない点も直したいです。
バックエンドだとkotlinを使用していますが、いまいちkotlinの強みを引き出せていません。
他にもreactでは関数型コンポーネントの「ステートを扱うロジックの再利用性が高い」などの利点をまだ活用しきれていません。開発プロセスや(最低限のUXを考えた)機能設計などの意思決定プロセスなどは別記事にしてまとめようと思います。
もし誤字脱字やQiitaの利用規約に反するような内容あればご指摘をお願いします
- 投稿日:2020-01-16T21:16:56+09:00
[React]@babel/preset-env と @babel/ransform-runtime を core-js@3 で対応する
最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!Babel と core-js の関係のおさらい
Babelが提供する @babel/polyfill や @babel/preset-env などのモジュールを利用すると
built-ins objects(Promise, WeakMap等)
static methods(Object.assign, Array.from等)
instance methods(Array.prototype.includes等)
といった新しい機能を使った実装が可能になりますよね。これらのBabelモジュールは core-js が提供するpolyfillを内部的に読み込んでいます。特に @babel/polyfill は core-js と regenerator-runtime を束ねて提供するpolyfill集ですcore-jsのバージョンを指定して直接読み込むます!
古いブラウザをサポートするため、core-js を利用してポリフィルを含めた React 16 向けの環境を次のように設定React 16 はコレクション型 Map および Set に依存しています。これらの機能をネイティブに提供しない(IE 11 未満など)、または標準非準拠な挙動をする(IE 11 など)古いブラウザやデバイスをサポートする場合は、core-js や babel-polyfill などのような、グローバル環境のポリフィルをバンドルしたアプリケーションに含めることを検討してください。
package.json+ "core-js": "^3.6.4",src/index.js+ import 'core-js/es/set'; + import 'core-js/es/weak-map';これで完了です!
参考記事
JavaScript 環境の要件
Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法
ReferenceError: Can't find variable: WeakMap
@babel/polyfill と core-js
- 投稿日:2020-01-16T20:41:57+09:00
[React]axiosの処理が繰り返し、呼び出される問題
コンポーネント内で定義したstateをaxiosのresponseデータを使って書き換える処理を書きましたが、console.log()で取ってみると何度もresponseが返ってくる事象。。。
const [posts, setPosts] = useState([]); axios .get('/api/posts') .then(response => { setPosts(response.data); console.log(posts) //何度も通信が走っていることを確認 }) .catch(() => { console.log('通信に失敗しました'); });下記で解決しました。
useEffect(() => { getPostsData(); },[]) function getPostsData(){ axios .get('/api/posts') .then(response => { setPosts(response.data); }) .catch(() => { console.log('通信に失敗しました'); }); }axiosの処理を関数化し、第2引数を[]にすることで、空配列が渡ってきたときのみ、処理が走るようにします。
やはり公式、、、
https://ja.reactjs.org/docs/hooks-reference.html
空の配列 [] を渡すと、この副作用がコンポーネント内のどの値にも依存していないということを React に伝えることになります。つまり副作用はマウント時に実行されアンマウント時にクリーンアップされますが、更新時には実行されないようになります。
- 投稿日:2020-01-16T19:44:48+09:00
Next.jsのセットアップ手順
ディレクトリ作成
mkdir sample_app cd sample_appnode_modules初期化
npm init -y
- これでpackage.jsonを作ってくれる
Next.jsインストール
npm install --save next react react-dompackage.jsonのscriptsにコマンドを追加
"dev": "next -p 3001", "build": "next build", "start": "next start"
- ポート番号3000だと他アプリと被りやすいので別ポートに変更
サンプルページ作成
mkdir pages vi page/index.jsexport default () => <div>Welcome to next.js!</div>起動
npm run dev参考記事
- 投稿日:2020-01-16T19:01:04+09:00
経年劣化に耐える ReactComponent の書き方
「経年劣化に耐えるコード」というのは、だれもが目指すものでしょう。「そもそもフロントエンドのコードは、今ある技術で最良のものを書き捨てるべき」という意見も理解できますが「備えあれば憂いなし」ということもありますので、ここにメモを残します。あくまで、私なりのベストプラクティスですのでご了承ください。
5層に別れた SFC
私はレイヤーによる技術の分離で、ReactComponent の経年劣化に備えています。ここでいうSFCとは「Stateless Functional Component」の略称ではありません。Vue.js の文脈にある「Single File Component」を指します。
// (1) import層 import React from 'react' import styled from 'styled-components' // (2) Types層 type ContainerProps = {...} type Props = {...} & ContainerProps // (3) DOM層 const Component: React.FC<Props> = props => (...) // (4) Style層 const StyledComponent = styled(Component)`...` // (5) Container層 const Container: React.FC<ContainerProps> = props => { return <StyledComponent {...props} /> }記述順は「依存関係の上流下流」に従います。import や型定義が上流工程であることは言うまでもないので省略、重要なのは(3)〜(5)を構成するレイヤーです。
技術の分離
なぜこの区分になっているのか、なぜこの書式になっているのか、ひとつずつ解説していきます。
(3) DOM層
const Component: React.FC<Props> = props => ( <div className={props.className}> <button onClick={props.handleClick}> {props.flag ? 'click me' : 'CLICK ME'} </button> </div> )JSX(TSX)は、React のためだけのものではなく、他ライブラリでも利用される技術です。そのため、React に依存する Hooks API などはここから取り除いています。
return
を用いない記法(props => (...)
)にすることで、Hooks API の介入を阻みます。この純粋な TSX にはビジネスロジックが無く、Array.map
や真偽値による出し分け程度です。「ボタンを押下された事で何が発生するのか?」という知識も存在しません。ここは副作用のない、真に Stateless なレイヤーです。このconst Component
だけを抜き出し(export)した場合、テストのしやすさは想像に易いでしょう。(4) Style層
const StyledComponent = styled(Component)` > button {...} `React CSS in JS のメジャーどころとして、styled-components がまず挙がると思いますが、Style層もあくまで CSS の話です。styled-components が解決している名前空間の解決は、BEM(MindBEMding)が解決したことと同じです。テンプレート文字列に記述されたCSSは、BEM にフォールバックしたり、CSS Modules に移行しても成立する記述となっています。私が styled-components の
styled.div
やstyled.button
を敬遠している理由はここにあります。>
による、children への指定漏洩防御も忘れない様にします。BEM へのフォールバックはまれなケースかと思いますが、部分的に React を表示している様な折衷 html では、この切り分けが生きてきます。
(5) Container層
const Container: React.FC<ContainerProps> = props => { const [flag, setFlag] = React.useState(false) const handleClick = React.useCallback(() => { setFlag(!flag) }, [flag]) return ( <StyledComponent {...props} flag={flag} handleClick={handleClick} /> ) }Redux の経験がある方なら、PresentationalComponent / ContainerComponent というワードに馴染みがあるでしょう。Redux のコードベースには、Store に connect するコンポーネントとして、ContainerComponent という区分が明確にあります。これは React Hooks 全盛期のいまでも、踏襲すべきベストプラクティスであると私は考えています。ここは Stateful なレイヤーであり「依存の注入」を行う場所でもあります。
- useState による状態管理が、Redux Store へ移行することになった
- Storybook の為に、モックを注入する層に差し替えたい
- テストの為に、モックを注入する層に差し替えたい
もしこのレイヤーに、
useEffect
を利用した fetch 処理が介入していたとしても、Storybook やテストにおいては、代替 Container を用意すれば良いわけです。(1)〜(4)は、ここの都合による影響を受けることがありません。Hooks か? Redux か? GraphQL か? という配慮も当然不要なものとなります。
(3) DOM層
から知識を剥奪することが重要な理由はこれに起因します。この様に「賢いレイヤー」を分離することで生まれるメリットは、依存注入技術の差し替えだけではなく、ビジネスロジックの移行(純関数の切り出し・Hooks から Redux への状態移譲)も容易にします。「Hooks API が過去のものになる…」という杞憂は当分先の話かと思いますが、将来の変化への備えとしては十分でしょう。
- 投稿日:2020-01-16T15:21:40+09:00
atomic designとは
ページ単位ではなく、コンポーネント単位でデザインを考える手法
・Atoms
・Molecules
・Organisms
・Templates
・Pagesの5つの単位に分けて、ページを作っていきます。
または日本語で、
・原子
・分子
・有機体
・テンプレート
・ページとも言われています。
Atoms
ボタン1つやフォーム1つなど
Molecules
Atomsを組み合わせたもの
Organisms
Atoms,Molecules、他のOrganismsを組み合わせたもの
Templates
Atoms,Molecules、他のOrganismsを組み合わせ、ページ構造を表す
Pages
Templatesに実際の文章や画像が入ったもの
atomic designのメリット
・変更に強い
・再利用性が高い
ではまた!
- 投稿日:2020-01-16T15:19:18+09:00
React TypeScript で Propsに HTMLタグを入れる方法
Propsで渡すデータに改行タグを入れたかったので、
このように設定してみました。import React from 'react'; // 型を定義する type HeadlineType = { en: string | Array<string|JSX.Element>; ja: string; } // 子コンポーネント const TheHeadline: React.FC<HeadlineType> = props => { return ( <h1 className="headline"> <div className="headline__en">{props.en}</div> <div className="headline__ja">{props.ja}</div> </h1> ); }; // 親コンポーネント const App: React.FC = () => { return ( <div id="app"> <TheHeadline en={["Long Long", <br key="Some key" />, "Long Title"]} ja="長いタイトル" /> </div> ); };ポイントは、定義する型は、
Element
ではなく、JSX.Element
ということ。
ここで時間かかってしまった。
[追記]
どうやらprops
で渡す引数にHTMLのタグがある場合、HTMLタグには任意のkey
を指定しないと、
Warning: Each child in a list should have a unique "key" prop.
と警告が出るようです。
これでうまくいきました!
参考:https://stackoverflow.com/questions/33381029/react-how-to-pass-html-tags-in-props
- 投稿日:2020-01-16T12:11:31+09:00
とりあえずreact-hook-form
はじめに
react系のフォームバリデーションライブラリreact-hook-formのざっくり使い方です。
公式のドキュメントでは、
- 超軽量なパッケージ
- 再レンダリングを最小に押さえて、マウントの高速化
- フォームの値がローカル管理される為、他パッケージに依存しない
等々の利点が挙げられている。
formikとの比較
download
https://www.npmtrends.com/redux-form-vs-formik-vs-react-hook-formsize
formik
react-hook-form
API
useForm
useFormでいろいろなapiを受け取れる
useFormのoption(default)const { register } = useForm({ mode: 'onSubmit', // | 'onBlur' | 'onChange' reValidateMode: 'onChange', // onBlur | onSubmit 再度バリデーションされるタイミング defaultValues: {}, validationSchema: {}, // スキーマレベルで Yup を使用してフォームバリデーションルールを適用 validateCriteriaMode: "firstErrorDetected", submitFocusError: true, // エラーのある最初のフィールドがフォーカスされる nativeValidation: false, // ブラウザバリデーションの活用 })register
input/selectのRefとバリデーションルールをreact-hook-formに登録する
<input name="form1_1" defaultValue="test" ref={register} />registerでバリデーションをかけられる。下記のコードではrequired, minLengthを5にせっていしている。
'1_2は必須です', '5桁以上必要です'はエラーメッセージ。<input name="form1_2" ref={ register({ required: '1_2は必須です', minLength : { value: 5, message: '5桁以上必要です' } }) } />フィールドフォームネストして扱うこともできる
name output name="firstName" { firstName: 'value' } name="firstName[0]" { firstName: [ 'value' ] } name="name.firstName" { name: { firstName: 'value' } } name="name.firstName[0]" { name: { firstName: [ 'value' ] } } errors
errorsオブジェクトにはフォーム内の各フィールドのエラーオブジェクト。
react-hook-formにはエラーメッセージ用の表示にErrorMessageコンポーネントも用意されている。import { useForm, ErrorMessage } from 'react-hook-form' // ~~ <ErrorMessage errors={errors} name="form1_2" /> // ~~watch
指定されたnameのinputを監視して、その値を返す。
defaultValue が定義されていない場合、watch の初回のレンダリングは register の前に呼び出されるため undefined を返しますが、 第2引数として defaultValue を設定して値を返すことができます。
ただし、引数として useForm で defaultValues が初期化された場合、 初回のレンダリングは defaultValues で指定された値を返します。
<p className="form1-watch-text">watch output: {watch('form1_1')}</p>handleSubmit
フォームバリデーションを通るとデータを渡す。
// ~~ const onSubmit = (data: Object) => { console.table(data) }; // ~~ <input type="submit" /> // ~~async関数も渡すことができるhandleSubmit(async (data) => await fetchAPI(data))Form1import React from 'react'; import { useForm, ErrorMessage } from 'react-hook-form' export default function Form1() { const { register, handleSubmit, watch, errors } = useForm({ validateCriteriaMode: 'all' }); const onSubmit = (data: Object) => { console.table(data) }; return ( <div className="form form1"> <h1>Form1</h1> <form onSubmit={handleSubmit(onSubmit)}> <div className="form-section"> <span>1_1: </span> <input name="form1_1" defaultValue="test" ref={register} /> <span className="sub-text">*watched</span> </div> <div className="form-section"> <span>1_2: </span> <input name="form1_2" ref={ register({ required: '1_2は必須です', minLength : { value: 5, message: '5桁以上必要です' } }) } /> <span className="sub-text">*required</span> </div> <div className="form1-watch"> <p className="form1-watch-text">watch output: {watch('form1_1')}</p> </div> <div className="errors"> <ErrorMessage errors={errors} name="form1_2" /> </div> <input type="submit" /> </form> </div> ) };controller
Controllerコンポーネント(UIコンポーネントライブラリと併せて使用するコンポーネント)用。
コンポーネント登録するためのメソッドが含まれている。
以下ではmaterial-uiを使用。Form2import React from 'react'; import { useForm, Controller, ErrorMessage } from 'react-hook-form'; import { TextField, Button } from "@material-ui/core"; export default function Form2() { const { handleSubmit, errors, control } = useForm() const onSubmit = (data: Object) => { console.table(data) }; return ( <div className="form form2"> <h1>Form2</h1> <form onSubmit={handleSubmit(onSubmit)}> <Controller as={<TextField />} name="form2_1" control={control} rules={{ required: "必須です" }} defaultValue="" /> <div className="errors"> <ErrorMessage errors={errors} name="form2_1" /> </div> <Controller as={<Button color="primary" ><span>送信</span></Button>} name="submit" control={control} defaultValue="" onClick={handleSubmit(onSubmit)} /> </form> </div> ) }reset
フォーム内のvaluesとerrorsをリセットできる関数。
リセット時に値を渡すとデフォルトの値としてリセットできる。
Form3import React, { useCallback } from 'react'; import { useForm, ErrorMessage } from 'react-hook-form' type Reset = (values?: Record<string, any>) => void; export default function Form3() { const { register, handleSubmit, reset, errors }: ({ register: Function, handleSubmit: Function, reset: Reset, errors: any, }) = useForm(); const onSubmit = (data: Object) => { console.table(data) }; const onReset = useCallback(() => reset(), [reset]) const onDefaultReset = useCallback(() => reset({ first_name: 'ジョン', last_name: '万次郎' }), [reset]) return ( <div className="form form3"> <h1>Form3</h1> <form onSubmit={handleSubmit(onSubmit)}> <span>姓:</span> <input name="last_name" ref={register({ required: '姓は必須です。' })} /> <div className="errors"> <ErrorMessage errors={errors} name="last_name" /> </div> <span>名:</span> <input name="first_name" ref={register({ required: '名は必須です。' })} /> <div className="errors"> <ErrorMessage errors={errors} name="first_name" /> </div> <input type="submit" /> <input type="button" onClick={onReset} value="reset" /> <input type="button" onClick={onDefaultReset} value="reset + default set" /> </form> </div> ) };setError / clearError
inputのエラーを手動で設定したりクリアする。
setValue
値を動的に設定できる。
Form4import React, { useCallback } from 'react'; import { useForm, ErrorMessage } from 'react-hook-form' export default function Form4() { const { register, handleSubmit, setError, clearError, errors, setValue } = useForm(); const onSubmit = (data: any) => { const goodAnswer = 12 * 12; if (parseInt(data.answer, 10) !== goodAnswer) return setError('answer', 'notMatch', '不正解です'); clearError('answer'); console.log('正解'); }; const onSetValue = useCallback(() => { setValue('answer', 12 * 12); }, [setValue]); return ( <div className="form form4"> <h1>Form4</h1> <form onSubmit={handleSubmit(onSubmit)}> <span>12 × 12 = </span> <input name="answer" ref={register({ required: '入力してください' })} /> <div className="errors"> <ErrorMessage errors={errors} name="answer" /> </div> <input type="submit" value="回答" /> <input type="button" value="諦める" onClick={onSetValue} /> </form> </div> ) };getValues
フォーム全体のデータを返す。
<input name="test" ref={register} /> <input name="test1" ref={register} /> <button type="button" onClick={() => { const values = getValues() console.log(values) // e.g: { test: [1, 2], test1: { data: '23' } }} > GetValues </button>triggerValidation
バリデーションのトリガーを手動で設定できる。
<input name="lastName" ref={register({ required: true })} /> <button type="button" onClick={async () => { triggerValidation("lastName"); }} > Trigger </button>unregister
inputにunregisterを適用すると、フォームデータに含まれなくなる。
これは、 useEffect でカスタム登録として input を登録 (register) し、 コンポーネントのアンマウント後に登録を解除する場合に便利です。
らしい。
formState
フォームの状態がオブジェクトで入っている。
name description dirty 入力が行われた後trueになる isSubmitted submitされた後trueになる isSubmitting 送信中はtrue, 送信後falseになる touched 操作されたnameが配列で入る submitCount submitされた回数 isValid errorがない場合true おわりです。込み入ったことは試していないのですが、記述が簡単な印象でした。
- 投稿日:2020-01-16T01:44:55+09:00
redux-sagaのテストライブラリを比較してみた
はじめに
redux-sagaでテストを書くことになりました
[https://redux-saga.js.org/docs/advanced/Testing.html:title]
↑が公式の説明です
ですが、いくつかライブラリがあってどれも評判の差があまりなく、どれがいいのかわからなかったので、実際にコードを書いて比較しましてみました
例のごとくtypescriptです
選択肢
名称 特徴 何も使わない 実際の動作に沿ったstep-by-stepで行える redux-saga-test unitテストが楽になる、step-by-stepで行える redux-saga-testing unitテストが楽になる、step-by-stepで行える redux-saga-test-engine unitテストが楽になる、全effectが配列で記録され、その配列でassertionする redux-saga-test-plan unitテストからintegrationまでサポート redux-saga-tester integrationのテストをサポート それぞれ結構書き味が違ったので感想含めて紹介していきます
サンプル
それぞれのライブラリで以下のコードのテストをしていきます
export function* fetchTodosSaga() { yield put(actions.requestTodos()); try { const todos = yield call(todoApi.getList); yield put(actions.successTodos(todos)); } catch (error) { yield put(actions.failedTodos()); } }シンプルにapiを呼ぶだけのsagaです
何も使わない
import { fetchTodosSaga, actions, todoApi } from "../todo"; import { put, call } from "redux-saga/effects"; import { cloneableGenerator } from "@redux-saga/testing-utils"; describe("step by step", () => { describe("fetchTodosSaga", () => { // sagaのgeneratorをcloneできるようにしておく const gen = cloneableGenerator(fetchTodosSaga)(); // 共通部分をテスト it("should request todo list", () => { expect(gen.next().value).toEqual(put(actions.requestTodos())); expect(gen.next().value).toEqual(call(todoApi.getList)); }); // 共通部分から続けて成功パターンをテスト it("should success", () => { // 共通部分まで進めたgeneratorをcloneすることで、条件分岐のテストができる const clone = gen.clone(); const mockRes = [{ id: 1, title: "test title", content: "test content" }]; expect(clone.next(mockRes).value).toEqual( put(actions.successTodos(mockRes)) ); }); // 共通部分から続けて失敗パターンをテスト it("should failed", () => { const clone = gen.clone(); // throwがoptionalなのでthrowがあるかの確認をしている expect(clone.throw).toBeTruthy(); clone.throw && expect(clone.throw().value).toEqual(put(actions.failedTodos())); }); }); });コードの中に説明がありますが、
@redux-saga/testing-utils
を使うことで条件分岐のテストが簡単になってます何も使わないと言っておいて早速ライブラリを使ってますが、これは公式なのでセーフです
sagaのeffectがオブジェクトを返してくれるのでそれを
toEqual
で比較するとeffectがyieldされてるかを確認できますシンプルにGeneratorのテストをしている形ですね、
expect(gen.next().value)
が長ったらしい感じもしますが、すでに十分わかりやすいですここまでは素のsagaのテストのしやすさが生きてますね、thunkだとこうはいきません
redux-saga-test
import { fetchTodosSaga, actions, todoApi } from "../todo"; import fromGenerator from "redux-saga-test"; import { cloneableGenerator } from "@redux-saga/testing-utils"; describe("redux-saga-test", () => { describe("fetchTodosSaga", () => { const gen = cloneableGenerator(fetchTodosSaga)(); it("should request todo list", () => { // ライブラリからexpectを作成する // Jestのassertを使うようにdeepEqualを作成 const libExpect = fromGenerator( { deepEqual: (a: any, b: any) => expect(a).toEqual(b) }, gen ); // 作成したexpectを使う libExpect.next().put(actions.requestTodos()); libExpect.next().call(todoApi.getList); }); it("should success", () => { const libExpect = fromGenerator( { deepEqual: (a: any, b: any) => expect(a).toEqual(b) }, gen.clone() ); const mockRes = [{ id: 1, title: "test title", content: "test content" }]; libExpect.next(mockRes).put(actions.successTodos(mockRes)); }); it("should failed", () => { const libExpect = fromGenerator( { deepEqual: (a: any, b: any) => expect(a).toEqual(b) }, gen.clone() ); libExpect.throwNext().put(actions.failedTodos()); }); }); });redux-saga-testにはd.tsがなかったので、サンプルリポジトリでは適当にモックしてます
アサーションでchaiか何かを使う前提っぽいのですが、今回はJestで使っているので
deepEqual
を作ってますexpectがsaga仕様になるので、何も使わないよりは書きやすくなってます
redux-saga-testing
import { fetchTodosSaga, todoApi, actions } from "../todo"; import sagaHelper from "redux-saga-testing"; import { put, call } from "redux-saga/effects"; describe("redux-saga-test-plan", () => { describe("fetchTodosSaga", () => { describe("success", () => { // 今度はitを作成する const it = sagaHelper(fetchTodosSaga()); it("should put requestTodos", nextValue => { expect(nextValue).toEqual(put(actions.requestTodos())); }); const mockRes = [{ id: 1, title: "test title", content: "test content" }]; it("should call todoApi", nextValue => { expect(nextValue).toEqual(call(todoApi.getList)); return mockRes; }); it("should select from state", nextValue => { expect(nextValue).toEqual(put(actions.successTodos(mockRes))); }); }); describe("failed", () => { const it = sagaHelper(fetchTodosSaga()); it("should put requestTodos", nextValue => { expect(nextValue).toEqual(put(actions.requestTodos())); }); it("should call todoApi", nextValue => { expect(nextValue).toEqual(call(todoApi.getList)); return new Error(); }); it("should select from state", nextValue => { expect(nextValue).toEqual(put(actions.failedTodos())); }); }); }); });今度はitを作ってますね
1effectごとにテストケースを作るのは、個人的には鬱陶しさを感じます
条件分岐にも弱いです
redux-saga-test-engine
import { fetchTodosSaga, todoApi, actions } from "../todo"; import { put, call } from "redux-saga/effects"; import { createSagaTestEngine, throwError } from "redux-saga-test-engine"; describe("redux-saga-test-engine", () => { describe("fetchTodosSaga", () => { // どんなeffectを記録するか指定する const collectEffects = createSagaTestEngine(["CALL", "PUT"]); it("should fetch todo list", () => { const mockRes = [{ id: 1, title: "test title", content: "test content" }]; // nextで渡したいものは第二引数を使ってモックする // call(todoApi.getList)がyieldされたあとの`next()`で`mockRes`が渡される const actualEffects = collectEffects(fetchTodosSaga, [ [call(todoApi.getList), mockRes] ]); // 配列として`actualEffects`にyieldされたeffectが入る expect(actualEffects[0]).toEqual(put(actions.requestTodos())); expect(actualEffects[1]).toEqual(call(todoApi.getList)); expect(actualEffects[2]).toEqual(put(actions.successTodos(mockRes))); }); it("should noop when failed", () => { const actualEffects = collectEffects(fetchTodosSaga, [ [call(todoApi.getList), throwError()] ]); expect(actualEffects[0]).toEqual(put(actions.requestTodos())); expect(actualEffects[1]).toEqual(call(todoApi.getList)); expect(actualEffects[2]).toEqual(put(actions.failedTodos())); }); }); });これもd.tsなかったです
effectが配列としてactualEffectsで取れるのでそれをテストしてます
いちいちnextする煩わしさはなくなりました
これも条件分岐に弱いです
redux-saga-test-plan
これが一番スター数が多かったです
import { fetchTodosSaga, actions, todoApi } from "../todo"; import { testSaga } from "redux-saga-test-plan"; describe("redux-saga-test-plan", () => { describe("fetchTodosSaga", () => { const labels = { waitApi: "wait api" }; it("should fetch todo list", () => { const mockRes = [{ id: 1, title: "test title", content: "test content" }]; testSaga(fetchTodosSaga) .next() .put(actions.requestTodos()) .next() .call(todoApi.getList) // 共通部分までをsave .save(labels.waitApi) // 成功パターンをテスト .next(mockRes) .put(actions.successTodos(mockRes)) .next() .isDone() // 共通部分までをrestoreして失敗パターンをテスト .restore(labels.waitApi) .throw(new Error()) .put(actions.failedTodos()) .next() .isDone(); }); }); }); });今回は
testSaga
を使ってユニットテストを書いていますが、expectSaga
を使うとreducerを含めたインテグレーションテストもできますまとめて1テストケースで書くと読みにくいですね
testSaga(fetchTodosSaga)
をテストケースの外に出して変数化してあげて、成功・失敗パターンは別のテストケースにしてあげればもっと読みやすくなると思います書き方を考えれば十分効率的に条件分岐のテストが書けます
メソッドチェーンのコード補完でテストを書いていけたので、一番早くテストをかけたと思います
restoreするとsaveポイント消えるので、そこだけは注意です
個人的にはこのライブラリが一番好きです
redux-saga-tester
wixが作ってるライブラリです
import reducer, { fetchTodosSaga, actions, todoApi } from "../todo"; import SagaTester from "redux-saga-tester"; import { Status } from "../types"; describe("redux-saga-tester", () => { const defaultState = reducer(undefined, { type: "noop" }); const sagaTester = new SagaTester(); beforeEach(() => { sagaTester.reset(true); }); describe("fetchTodosSaga", () => { it("should fetch todo list", async () => { const mockRes = [{ id: 1, title: "test title", content: "test content" }]; todoApi.getList = jest.fn().mockImplementation(() => { return Promise.resolve(mockRes); }); sagaTester.start(fetchTodosSaga); await sagaTester.waitFor(actions.successTodos.toString()); expect( sagaTester.wasCalled(actions.requestTodos.toString()) ).toBeTruthy(); expect( sagaTester.wasCalled(actions.successTodos.toString()) ).toBeTruthy(); expect(sagaTester.getState()).toEqual({ list: mockRes, status: Status.success }); }); it("should noop when failed", async () => { todoApi.getList = jest.fn().mockImplementation(() => { return Promise.reject(); }); sagaTester.start(fetchTodosSaga); await sagaTester.waitFor(actions.failedTodos.toString()); expect( sagaTester.wasCalled(actions.requestTodos.toString()) ).toBeTruthy(); expect(sagaTester.wasCalled(actions.failedTodos.toString())).toBeTruthy(); expect(sagaTester.getState()).toEqual({ list: [], status: Status.failed }); }); }); });sagaのテストはeffectの順番が変わるだけでも壊れたりするので、このライブラリではブラックボックステストのアプローチを提供しています
なので
wasCalled
などにより壊れにくいテストが書けるようになっていますまとめ
テストは勉強中なのですが、個人的にはユニットテストはホワイトボックステストをして、インテグレーションテストならブラックボックステストをするものだと考えています
ブラックボックステストはredux-saga-test-planもサポートしてるので、個人的にはやっぱりredux-saga-test-planを使いたいと思います
- 投稿日:2020-01-16T01:00:17+09:00
material-ui-dropzoneでapplication/octet-streamのMime typeが登録できなかった話
内容
ずっと出来ると思っていたcadファイルをアップロードしようと思ってやってみたら、拒否されてしまったのでそれを解決したお話
結論
acceptedFilesのjwwみたいな感じで拡張子を指定してあげればうまく出来たよ
<DropzoneDialog open={open} onSave={handleSave.bind(this)} dialogTitle={"ファイルをアップロードする"} acceptedFiles={['image/*', 'application/*, .jww , .dwg , .dxf , .jwc , .p21']} dropzoneText={"アップロードしたいファイルをドラッグアンドドロップするかクリックして選択ください"} cancelButtonText={"アップロードをやめる"} submitButtonText={"アップロードする"} onDrop={handleCheck.bind(this)} showPreviews={true} maxFileSize={5242880} onClose={handleClose} />
- 投稿日:2020-01-16T00:19:01+09:00
CodePipelineでS3にデプロイしてCloudFrontでコンテンツを配信する
CodeStarでさくさくCI/CD作りもいいのだが、とりあえず一旦はCodeCommitからDeployまでCodePipelineで連携する方法を理解しておこうと思ったので、軽く試してみた。
CloudFrontで配信するところまでやってみる。
やること
- CodePipelineを利用して、CodeCommit, CodeBuildを連携させ、ReactクライアントをS3にアップロードする(デプロイ)。
- S3に配置されたReactクライアントをCloudFrontで配信する。
やる順番
- CodeCommitでリポジトリを作成
- CodeBuildでビルドの設定(テストの設定とかはしない)
- CodePipelineでCommitからDeployまでを一貫して行う(S3へビルドファイルをアップロード)
- CloudFrontでコンテンツを配信(細かい設定はしない)
CodeCommitでリポジトリを作成
AWSコンソールのCodeCommitを開き、リポジトリを作成する。
仮にリポジトリ名testを作成すると以下のような「接続のステップ」が表示される。
ソースコードをプッシュするには、まずこのリポジトリをクローンする必要がある。
右上の「URLのクローン」から「HTTPSのクローン」を選択すると、URLがコピーされるのでローカルでgit cloneする。git cloneの際に尋ねられるユーザー名等はIAMの認証情報から取得する。
IAMのアクセス管理>ユーザーからアカウントを選択して、「AWS CodeCommit の HTTPS Git 認証情報」の「認証情報を生成」から証明書をダウンロードする。中のユーザー名とパスワードを使ってgit cloneできるようになる。
cloneしたディレクトリにソースコードを置いてプッシュすればCodeCommitにソースコードが表示される。
CodeBuildでビルドの設定
AWSコンソールのCodeBuildを開き、ビルドプロジェクトを作成する。
公式の解説ページも参考に。
CodeBuild でビルドプロジェクトを作成する「ビルドプロジェクトを作成」へ入ると、
- プロジェクトの設定
- 送信元
- 環境
- Buildspec
- アーティファクト
- ログ
の設定項目が目につくが、ここでは「送信元」、「環境」、「Buildspec」のみに触れる。
「アーティファクト」と「ログ」の設定は触れずにビルドプロジェクトを作成する。
CodeBuild 送信元設定
ここで指定するソースプロバイダは、CodeCommitの入力アーティファクトを出力するソースコードを指す。
CodePipeline(CodeCommit, CodeBuild, CodeDeployをCI/CD機能)では入力アーティファクトと出力アーティファクトが各フェイズで受け渡される。
アーティファクトとはそれぞれのフェイズの成果物のことで、CodeCommitの出力アーティファクトはコードそのものであり、CodeBuildはそれを入力アーティファクトとして受け取って、ビルドしたファイルを出力アーティファクトとしてCodeDeployへ渡す。CodeBuild 環境設定
ビルド環境設定では、ビルドを実行するためにCodeBuildが使用するオペレーティングシステム、プログラミング言語ランタイム、およびツールの組み合わせを設定することができる。
特にこだわりがないのであれば、
OS、ランタイム、イメージ、イメージのバージョン、環境タイプは上記のように設定すればたいして困らないと思う。サービスロールではCodeBuildの実行に必要なポリシーが組まれたロールが作成される。
すでにある場合は既存のものを使える。CodeBuild のビルド環境リファレンス
CodeBuild に用意されている Docker イメージCodeBuild Buildspec設定
ビルドコマンドの挿入の選択肢もあるが、ここではbuildspecファイルを使ったビルド設定について触れる。
ここまでにCodeBuildがビルドを行うソースコードの設定と、ビルドを行う環境の設定について書いたが、Buildspecではビルドの実行時に実行する細かい処理についての設定を行うことができる。例えば、reactクライアントをCodeCommitへのプッシュをトリガーとしてビルドしたい場合は、以下のようにディレクトリ内にbuildspec.ymlを配置する。
これがこのリポジトリのルートディレクトリだとすると、Buildspecの項目におけるBuildspec名にはファイルそのものを指定すれば良い。
もしリポジトリ内の特定のディレクトリに存在するBuildspecファイルを指定したい場合は、そのパスを書く(client/buildspec.yml
のように)。また、このymlファイルの名前が
buildspec
である必要はない(clientspec.yml
とかでも良い)。buildspec.ymlの例
例ではreactクライアントのビルドを念頭に置いているため、runtime-versionsはnodejs10.xを指定しており、 typescriptをインストールしている。version: 0.2 phases: install: runtime-versions: nodejs: 10 commands: - npm install -g typescript pre_build: # ビルド実行前に実行する処理等の設定 commands: - cd client - npm install build: # ビルド実行処理等の設定 commands: - npm run build # post_build: # commands: # - some command # - some command artifacts: files: - 'client/dist/*' # 出力するビルドファイル discard-paths: yes # 出力するビルドファイルからパスを省く(client/dist/などを省く)CodePipelineでCommitからDeployまでを一貫して行う
AWSコンソールのCodePipelineを開き、パイプラインを作成する。
パイプライン名とサービスロールを設定すると、
- ソースステージ
- ビルドステージ
- デプロイステージ
の設定に入る。
ソースステージの設定
CodeCommitのリポジトリ名とブランチ名を指定する。
これはビルドステージへの出力アーティファクトとなる。ビルドステージの設定
CodeBuildで設定したプロジェクトを指定する。
デプロイステージの設定
CodeDeployはS3のデプロイに対応していないため、S3デプロイを行うためにはCodePipelineを使う必要がある。
デプロイプロバイダーにS3を指定し、デプロイ先のバケットを選択する。
ここでは「デプロイする前にファイルを抽出する」にチェックを入れている(何もしないとzipがバケットに配置されるが、ここでは解凍された状態で配置したいため。デプロイパスはバケット内に展開されるディレクトリの構成を設定できる)。全ての設定を終えて確認画面から「パイプラインを作成する」と、パイプライン一覧に新規パイプラインが表示される。
ソースコードに変更を加え、CodeCommitへプッシュすると、自動的にCodeBuildが起動し、S3へのビルドファイルのデプロイが行われる。デプロイ設定で指定したバケット内にビルドしたファイルが表示されるはず。
CloudFrontでコンテンツを配信
CloudFrontとはオリジンサーバーが直接アクセスに対応する機会を減らし、キャッシュ化されたエッジロケーションのリソースに対してユーザをルーティングする機能のこと。
Amazon CloudFront とはAWSコンソールでCloudFrontを開き、CreateDistributionを選択する。
WebのGet Startedからディストリビューションの設定を行う。
Origin Settingでは以下の2点を設定する。
Origin Domain Name
CodePipelineでビルドファイルをデプロイしたS3バケットを選択する。Restrict Bucket Access
バケットのリソースへのアクセスを、S3のURLを使わずに常にCloudFrontのURLを利用したアクセスのみに絞りたい場合はこの項目をYesに設定する。Default Cache Behavior Settingsは飛ばして、
Distribution Settingsでは以下の項目だけを設定する。
- Default Root Object
index.htmlを指定する。 これはバケット内のインデックスドキュメントの設定。 設定しないと${URL}/index.html
としてアクセスしなければならない。Create Distributionをクリックして一覧に表示されるディストリビューションのStatusがDeployedになるまで待つ。
DeployedとなったらDomain Nameに表示されているURLにアクセスしてページが動いているかを確認する。
完。
まとめ
ここまででCodeCommitに新しい変更をコミットしていくと、S3バケットに自動的にビルドファイルがアップロードされるようになる。
厳密なデプロイはそのあとにCloudFrontのディストリビューションのInvalidate(CloudFrontのエッジロケーションのキャッシュを削除して更新されたコンテンツを再配布する)を行ったときに行われる。
InvalidateはCloudFrontのディストリビューションの一覧画面からディストリビューションを選び、Invalidationタブを選択した画面で行える。
Create InvalidationからInvalidateする項目を指定してInvalidateボタンをクリックするだけ。
バケット内の全てのファイルを指定してInvalidateする場合は*(アスタリスク)を指定すれば良い。今後デプロイの手順としては、
- CodeCommitへコードのプッシュ
- CloudFrontでInvalidateを実行
をするだけでよくなる。
おしまい。
- 投稿日:2020-01-16T00:16:23+09:00
S3に静的HTMLファイルやSPAのスクリプトを配置して公開する
S3はファイルを補完するだけでなく、以下の手順を踏むことでHTTPサーバとしても利用することができます。
EC2などのサーバを立てなくても簡単にHTMLページが公開できます。
また、ReactやVueなどで作ったスクリプトを配置することもできます。費用はファイルサイズやアクセス数をベースに計算され、安価で簡単に情報公開ができるのでおすすめです。S3バケットの作成
バケットを作成していない場合は、Create bucketボタンを押して、S3バケットを作ります。
最初のBucket nameは同じ名前のバケット名を(同じリージョンで)ほかの人が使っているとエラーが出ます。
重複しないバケット名を入れて、Nextを押して次に行きます。②~④は、一旦そのままの設定で構いません
Public access ブロックの解除
次に、Public access Blockの解除を行います。
前の手順で作成したバケットのチェックボックスをチェックした状態でEdit public settingsボタンを押します。
Block all public accessのチェックボックスを外し、Nextを押します。
すると、以下の様にPublic accessを許可してもよいかを確認する画面が出ます。ウインドウ下の入力欄に”confirm”と入力し、Confirmボタンを押します。
私はこの手順を踏むことでデータが公開された!と思ってしまいましたが、この段階ではPublic access blockを外しただけで公開設定をいれたわけではないので、公開はされていません。
次にBucketポリシーを変更して公開設定を行います。Bucketポリシーの設定
次に、対象のバケット名をクリックし、バケットの中身の一覧画面に移動し、上に表示されるPermission>BucketPolicyの順にタブをクリックします。
タブの下にBucketPolicyEditorが表示されますので、以下に示したBucket policyを設定します。
example-bucketとなっている部分については、作成したバケット名に変更してください。"bucketpolicy"{ "Version":"2012-10-17", "Statement":[{ "Sid":"PublicReadGetObject", "Effect":"Allow", "Principal": "*", "Action":["s3:GetObject"], "Resource":["arn:aws:s3:::example-bucket/*" ] } ] }Static website hosting の設定
最後に、Propertiesタブ>Static website hostingをクリックし, index document欄に初期ページとして表示するファイル名を設定します。ファイル名の指定なしにルートディレクトリを参照するURLを表示しようとした場合、ここで設定したファイルが自動的に読み込まれるようになります。
また、Error documentを設定すると、存在しないページなどを表示しようとした場合にここで指定したページが表示されるようになります。
これで設定完了です!
タイトルのすぐ下に表示されているEndpointのURLをクリックすると、S3のルートディレクトリに登録されているindex.htmlを参照できます。参考ページ
https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteAccessPermissionsReqd.html