- 投稿日:2020-08-10T23:08:48+09:00
シンプルなReact用JSONエディター
この記事で作っていたJSON Editorを(だいぶ整えて)npmに公開しました。
Reactでデバッグに使う用のテキストエディタを作ったnpm: https://www.npmjs.com/package/react-plain-json-editor
GitHub: https://github.com/nariakiiwatani/react-plain-json-editor※npmへの公開にあたってはこちらの記事を大いに参考にさせていただきました。
初めてのnpm パッケージ公開インストール
npm i react-plain-json-editor
もしくは
yarn add react-plain-json-editor
できること
Component(
<PlainJsonEditor />
)とhooks(useJsonEditor
)のどちらでも使用可能。
基本的にはコンポーネント版の方が高機能(コンポーネント版は中でhook版を使用している)大雑把に言えば、teaxtarea要素に打ち込んだテキストがJSONにパースした結果を何かに利用するためのライブラリということになる。
使い方はGitHubのreadmeもしくはデモページを参照。
このデモページでは入力されたJSONデータをそのままそのページ自体のCSS Propertiesとして使用している。
デモページReactでシンプル(by Plain Text)なJSON Editorを作ったデモ。
— 岩谷成晃 (@nariakiiwatani) August 10, 2020
入力された文字列をそのページ自体のCSS Propertiesとして使用している。https://t.co/lLXlXGmCzOhttps://t.co/pYLba2zmTS#react #react-hooks #jsoneditor #json #npm pic.twitter.com/mOIawqyiTi他にも、独自のデータ構造を持ったprops受けるコンポーネントへのテストデータの生成や
単にJSON文字列のバリデータとしてなど、使い道はいくつかありそう。
- 投稿日:2020-08-10T22:43:30+09:00
ReduxとReduxToolkitを使用してReact内でデータを管理する
はじめに
前回のReactを使用してWeb画面を作成するでは、Reactの簡単なサンプルと共に説明をまとめました。次はデータを持ちまわすためにReduxとReduxToolkitの簡単なサンプルと説明をまとめようと思います。
最低限の簡単なサンプルなのでアクションは別ファイルにするべきであったり、といったベストプラクティス的なものは他のサイトで調べてください。環境
- node.js: v12.18.2
- webpack: 4.44.1
- React: 16.13.1
- Redux: 7.2.1
環境作成
前回のReactを使用してWeb画面を作成するの続きとなります。
環境構築などはそちらを見てください。Redux
Reactのコンポーネント間でデータを共有するためにReduxというライブラリを使用します。
Reduxのインストール
ReduxのライブラリとReduxをシンプルに記載するためのtoolkitをインストールします。
npm install react-redux npm install @reduxjs/toolkitシンプルなRedux
Reduxのソースの作成
フォルダの作成
Reduxのソースをstore、slice、コンポーネントに分けて作成するので、コンポーネント以外のフォルダを作成してください。コンポーネントは前回作成したsrcの下に入れます。
project_root ├─src // reactのJavaScriptファイルやCSSファイルを格納 ├─store // redux toolkitのstore(reduxのstoreをまとめたもの)ファイルを格納 ├─slice // redux toolkitのslice(reduxのactionとreducerをまとめたもの)sliceファイルの作成
内部的に保持する情報と処理をまとめたものをsliceファイルとして作成しています。
今回は単純にWeb画面と文字のやり取りをするため保持するデータは"mess"
、処理は"hello"をデータに置き換える処理としています。messageSlice.jsimport { createSlice } from '@reduxjs/toolkit'; import axios from 'axios'; export const messageSlice = createSlice({ // slice名 name: 'message', // 内部で保持するデータ(キー:mess, 初期値:メッセージ) initialState: { "mess": "メッセージ" }, // 内部のデータにアクセスするための処理(処理名:sayhello) reducers: { sayhello: state => { state.mess = "hello"; } }, }); // 外からインポートするためにactionとreducerをエクスポートする export const { sayhello } = messageSlice.actions; export default messageSlice.reducer;storeファイルの作成
先ほど作成したsliceのreducerをstoreに登録することで各コンポーネントで情報を共有できるようにします。
store.jsimport { configureStore } from '@reduxjs/toolkit'; import messageReducer from './slice/messageSlice'; export default configureStore({ reducer: { message: messageReducer, }, });storeをコンポーネントに適用させる
コンポーネント間で情報をやり取りするために先ほど作成したstoreをreactのrenderに登録します。
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; // redux用のインポート import { Provider } from 'react-redux' import store from '../store/store' ReactDOM.render( // インポートしたstoreを登録する <Provider store={store}> <App />, </Provider>, document.getElementById('app') );slice経由でstoreを使用する
コンポーネントで情報を処理するためにsliceのaction経由でstoreを操作します。
App.jsimport React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { sayhello } from '../store/slice/messageSlice'; export function Message() { // store内の値を取得 const message = useSelector(state => state.message.mess); // actionを操作するための関数取得 const dispatch = useDispatch(); return ( <div> <div> {/* Sliceで定義したactionをdispatch経由で呼び出す */} <button aria-label="hello" onClick={() => dispatch(sayhello())}> こんにちは </button> {/* 上で呼び出したmessageを表示する */} <span>{message}</span> </div> </div> ); }最終的なフォルダ構成
project_root ├─dict ├─public | Ⅼ-index.html ├─src | ├─App.js | Ⅼ─index.js ├─store | ├─slice | | Ⅼ─messageSlice.js | Ⅼ-store.js ├─.babelrc ├─package.json Ⅼ─webpack.config.jsReactの実行
前回同様にReactを開発用のサーバで起動してブラウザからアクセスしてください。
"./node_modules/.bin/webpack-dev-server"表示されたWeb画面にこんにちはというボタンがあると思うため、それをクリックすると隣の文字がhelloに変わります。
内部の処理としては、ザックリ言うと以下のようなイメージになります。
画面描画時にApp.js
内でuseSelector
を使用することにより、messageSlice.js
で定義したmess
変数を呼び出しています。
その呼び出した変数を<span>{message}</span>
と紐づけて変数が変わったら自動的に変わるように使用しています。
ボタンを押したらmessageSlice.js
で定義したsayhello
を呼び出してmess
変数を更新して、再描画しています。画面から受け取るRedux
先ほどはactionの中で定義した値に更新していましたが、今度はWeb画面に入力した内容を使用してstoreを更新します。
Reduxのソースの作成
先ほどのファイルに処理を追加して機能を実装します。
sliceファイルの作成
messageSlice.jsimport { createSlice } from '@reduxjs/toolkit'; export const messageSlice = createSlice({ name: 'message', ~~~ 上と同じ ~~~ reducers: { ~~~ 上と同じ ~~~ // この処理を追加します。 sayAmount: (state, action) => { state.mess = action.payload; }, }, }); // 追加したsayAmountをエクスポートできるようにする export const { sayhello, sayAmount} = messageSlice.actions; export default messageSlice.reducer;storeファイルの作成
storeファイルは、上のreducerをまとめて登録しているので変更なしです。
storeをコンポーネントに適用させる
storeファイルは、上のreducerをまとめて登録しているので変更なしです。
slice経由でstoreを使用する
内容としては。ほとんど先ほどのものと変わらないです。
8行目のところでuseState
を使用して2つの関数を生成していますが、ザックリ言うとクラス内のstateの宣言などを不要にする物になります。Message.jsimport React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { sayhello, sayAmount } from '../store/slice/messageSlice'; export function Message() { const message = useSelector(state => state.message.mess); const dispatch = useDispatch(); const [messsageAmount, setMesssageAmount] = useState('2'); ~~~ 上と同じ ~~~ {/* テキストボックスとボタンにインポートしたものを適用する */} <input aria-label="set amount" value={messsageAmount} onChange={e => setMesssageAmount(e.target.value)} /> <button onClick={() => dispatch(sayAmount(messsageAmount))}> テキスト変更 </button> </div> </div> ); }Reactの実行
上と同様にReactを開発用のサーバで起動してブラウザからアクセスしてください。
今回追加したテキストボックスとボタンが追加されています。テキストボックスに値を入れてボタンをクリックすると表示されているテキストが変更されます。
基本的に内容としてはほぼ表示のみと変わりません。終わりに
axiosとの連携を書こうと考えていましたが、Reduxの説明が予想以上に長くなったので今回はここまでにします。
次回以降にaxiosのサンプルと簡単な説明を書いていこうと思います。axiosのサンプルと説明はReactのRedux内でaxiosを使用した通信をするにまとめました。
今回は簡単な例なのでメリットがわからないと思いますが、Web画面や保持する情報が増えたら明確にメリットがわかるようになると思います。
- 投稿日:2020-08-10T22:11:49+09:00
React + apollo client + Hexagonal architectureでロジックとビューを分離する
導入
Reactを書く時、外部との通信や状態の更新など、ロジックどこに書くか問題というのは常につきまとうかと思います。1ページ分だけの小さなReact applciationであれば、とりあえず外側のコンポーネントに押し込んでおけばいいかと割り切ることもできると思うのですが、機能が増えたり複雑なロジックを持つようになるとつらみが増し、末端のコンポーネントにロジックが漏れて行ったりします。
以前からどうすればいいかといつも悩んでいたのですが、Hexagonal architectureを導入してみたところ、まま快適になったので、まとめます。サンプルのコードはこちら
https://github.com/eiji03aero/react-hexagonal-sample概要
Hexagonal architectureとは
本家ブログはこちら。
https://alistair.cockburn.us/hexagonal-architecture/Hexagonal architectureでは、applicationをコアとなるロジックの部分(application service以下)とそれ以外の外界とのやりとり(adapters: DBのread/writeやhttp endpointなど)を明確にわけることで、以下のようなメリットを得ることを目的とします。今回は主に
view層へのビジネスロジック流出の阻止
が目的となります。高いテスタビリティ
基本的にplain oldなコードのみで記述されるApplication service以下にビジネスロジックが集約されるため、テストを容易に行うことができます。またApplication serviceが依存するportsを実体のないinterfaceとすることでモックの差し込みも容易です。
view層へのビジネスロジック流出の阻止
application service以下にビジネスロジックを配置し、view層はあくまでapplication serviceが提供するapiをadapterとして利用するだけにする、という形に制限することで、ビジネスロジックがview層に流れることを防ぎます。ここは人力です。
同じ機能を複数の入力に対して提供する
application serviceがinportsを提供することにより、複数のadaptersが同じ機能を利用することができます。たとえば、csvレポートを出力するという機能をapplciation serviceが持っていた場合に、その機能を利用するhttp endpointのadapterとcliのadapterを作成することができます。
sampleについて
sampleはtodoを管理するアプリです。todoの作成や完了更新、tagの作成やtodoへの紐づけ、またtodo一覧でfilter表示などができます。
Apollo clientを状態管理に利用することで、ServiceとReact双方から同じデータを参照できるようにしつつ、Serviceが好きなようにデータの更新を行うことができます。
申し訳程度の六角形ですがお許しください。
- Service
- Application serviceとしての責務を持ちます。adapterとしてのreactが利用する、主にデータ更新系のapiを提供します。
- React
- view層を担います。Serviceのinstanceをcontextに保持し、データの作成や更新の際に必要なapiを呼び出します。
- データ(Todoなど)の読み込みをapollo client cacheから行うことにより、Service(Repositories経由で)が状態を変更した時に自動的にviewに反映します。
- Repositories
- Apollo clientに依存し、データのRead/Writeを担います。
- Apollo client cache
- ローカルの状態管理を担います。
- データの変更があった際に、Reactに変更を反映します。
- ①: ReactはServiceが提供するapiを呼び出してデータの更新などを行います。
- ②: ServiceはRepositoriesが提供するapiを呼び出してデータのRead/Writeを行います。
- ③: Repositoriesはapollo clientに直接依存し、データのRead/Writeを行い、実装の詳細をServiceから隠します。
- ④: ReactはApollo clientの提供するapiを利用し、データの読み込みと、変更が会った際のデータの反映を受け取ります。
sampleのコード
コードを追いながら、実際の処理のフローを見てみます。
ユースケース1: Todoを作成する
Todoのタイトルをユーザーが入力して作成の処理を進め、作成されたデータがviewに反映されるまでです。
1. Serviceのapiを呼び出し
まずcomponentのeventListenerをtriggerに、Service#createTodoを呼び出します。
src/adapters/ui/routes/Todos.tsxexport const Todos: React.FC = () => { ... const handleCreate = React.useCallback((value: string) => { ctx.service.createTodo({ title: value, }); }, [ctx]); ... };2. Service#createTodo
受け取った引数で、todosServiceのメソッドを呼び出して、Todoの作成の処理を進めます。
src/service/Service.tsexport class Service implements types.IService { ... async createTodo (params: { title: string, }): types.PromisedEither<types.ITodo> { const r1 = await this._todosService.create(params); ... } ... }3. TodosService#create
受け取った引数を使って、Todo classのインスタンスを作成します。
さらにインスタンスのvalidationを実行し、問題がなければtodosRepository#saveにインスタンスを渡して永続化をします。src/domain/services/TodosService.tsexport class TodosService implements types.ITodosService { ... async create (params: { title: string; }): types.PromisedEither<types.ITodo> { const todo = new Todo({ title: params.title, done: false, }); const r1 = todo.validate(); if (E.isLeft(r1)) { return r1; } const r2 = await this._todosRepository.save(todo); if (E.isLeft(r2)) { return r2; } return E.right(todo); } ... }4. TodosService#save
Todo classのインスタンスを受け取り、apollo client cacheに書き込みます。
src/adapters/repositories/TodosRepository.tsexport class TodosRepository implements types. ITodosRepository { ... async save (todo: types.ITodo): types.PromisedEither<null> { const r1 = await this._getSerialized({}); if (E.isLeft(r1)) { return r1; } const stodos = r1.right; const stodo = todo.serialize(); this._apolloClient.writeQuery({ query: local.GetTodosDocument, data: { todos: [stodo, ...stodos] } }); return E.right(null); } ... }5. 追加されたデータのviewへの反映
前項目で追加されたデータはuseQueryを通して反映されます。
src/adapters/ui/routes/Todos.tsxexport const Todos: React.FC = () => { ... const todosResult = useQuery(local.GetTodosDocument, { variables: { keyword: state.keyword, tagIds: state.tagIds, sort: state.sort, } }); ... };ユースケース2: 無効なTagを作成しようとしたエラーの通知をviewで購読する
無効な入力でTagを作成しようとして、Tag classのvalidationに引っ掛かり、エラ〜メッセージがEventEmitter経由でviewに通知されるまでです。
1. 無効な値でServiceのapiを呼び出し
空文字を名前としてTagを作成するためのapiを呼び出します。
src/adapters/ui/routes/Tags.tsxexport const Tags:React.FC = () => { ... const handleCreate = React.useCallback((value: string) => { ctx.service.createTag({ name: value, }); }, [ctx]); ... };2. Service#createTag
受け取った引数でTagsService#createを呼び出します。
src/service/Service.tsexport class Service implements types.IService { ... async createTag (params: { name: string, color?: string, }): types.PromisedEither<types.ITag> { const r1 = await this._tagsService.create(params); ... } ... }3. TagsService#create
受け取った引数でTag classのインスタンスを作成し、validateメソッドを呼び出します。
src/domain/services/TagsService.tsexport class TagsService implements types.ITagsService { ... async create (params: { name: string, color?: string, }): types.PromisedEither<types.ITag> { const tag = new Tag({ name: params.name, color: params.color || colors.random(), }); const r1 = tag.validate(); ... } ... }4. Tag#validate
このインスタンスはname propertyに空文字を持っているため、Errorを返却します。
src/domain/entities/Tag.tsexport class Tag extends BaseEntity implements types.ITag { ... validate () { ... if (!this.name) { return E.left(new Error("name cannot be empty")); } ... } ... }5. Service#createTag-2
前項目で返却されたエラーはService#createTagまでもどってきます。
Service#notificateを呼び出し、Eventをpublishします。src/service/Service.tsexport class Service implements types.IService { ... async createTag (params: { name: string, color?: string, }): types.PromisedEither<types.ITag> { const r1 = await this._tagsService.create(params); if (E.isLeft(r1)) { await this.notificate({ type: "error", message: `Failed to create tag: ${r1.left.message}`, }); return r1; } ... } ... }6. 通知の購読
前項目でpublishされた通知は、通知のためのコンポーネントによって購読され、ユーザーへfeedbackされます。
src/adapters/ui/containers/NotificationsContainer.tsxexport const NotificationsContainer: React.FC = () => { ... React.useEffect(() => { const handler = ((sn: types.SNotification) => { setState({ open: true, currentNotification: sn, }); }); ctx.service.onNotification(handler); return () => { ctx.service.offNotification(handler); }; }, [setState]); ... return ( <Snackbar open={state.open} autoHideDuration={6000} anchorOrigin={{ vertical: "bottom", horizontal: "right", }} onClose={handleClose} > <Alert elevation={6} variant="filled" severity={severity} onClose={handleClose} children={message} /> </Snackbar> ); };所感
結局ビジネスロジックがview層に漏れでていないかどうかは開発者が人力でレビューするしかないのですが、明確にcomponentを定義してこの中に収まるように書きましょう、と規約を設定できるのはわかりやすくていいと思います。以下pros cons。
良いところ
- Service以下にロジックを集約できる。Reactからはあくまで必要な引数を作ってServiceのapiに渡すだけにとどめる。きれいになります。
- Apollo clientのおかげで、Service - React間の繋ぎ込みが容易です。
- テストが書きやすい。
- 今回のsampleではテストは書いていないのですが、やはりビジネスロジックが混入しないよう努力されているため、書きやすいはずであります。
悪いところ
- ボイラープレートが多い
- Service class内に直接IO処理(apollo client cacheへのRead/Write)を書かないためにRepositoryというadapterを作ることにしているが、ServiceとRepositoryのapiで似たようなsignatureになりやすいので、冗長な記述が増えてしまった。一つ一つtypeのaliasを定義するのもいかがなものか。。。
- 投稿日:2020-08-10T21:30:56+09:00
Firestoreのsetメソッド基本の使い方
1. addメソッドとは
- データベースに追加するときに使う
- firesroreのaddメソッドは自動でIDを採番してくれるので単純にデータを渡せば良い
const itemRef = db.collection('items') const data = { name: name, price: parseInt(price, 10) } return itemRef.add(data) .then(() => { dispatch(push('/')) })2. setメソッドとは
- idを指定して登録できる
- 何も指定がないと自動で設定される(addと同じ)
return itemRef.doc().set(data)
- 事前に割り振られるIDを取得できる
const ref = itemRef.doc(); const id = ref.id; data.id = id return itemRef.doc(id).set(data)
- 変更部分のみmargeできる
return item.Ref.doc(id).set(data, {marge: true})
- 投稿日:2020-08-10T20:46:36+09:00
Next.js & material UIで[Prop className` did not match`]が発生する
問題
Next.jsアプリでmaterial-UIを使用した際に、
[Prop className' did not match' Server 'xx' Client 'xx']
が発生する詳細
makeStyles
で作成したスタイルの読み込み時に発生- 初回レンダリング時には発生しない
- 変更を加えたり、ページ更新を行うと発生する
Server 'xx' Client 'xx'
の内容は、Server "makeStyles-mainContainer-1" Client makeStyles-mainContainer-2
のように、クラス名に違いが生じている解決方法
参照:
Material UI公式:サーバーサイドレンダリングについて
上記ページからのリンク:Next.jsのサンプルリポジトリサンプルリポジトリの通り、
_app.js
と_document.js
を変更することで解決pages/_app.jsimport React from 'react'; import PropTypes from 'prop-types'; import Head from 'next/head'; import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; import theme from '../src/theme'; export default function MyApp(props) { const { Component, pageProps } = props; React.useEffect(() => { // Remove the server-side injected CSS. const jssStyles = document.querySelector('#jss-server-side'); if (jssStyles) { jssStyles.parentElement.removeChild(jssStyles); } }, []); return ( <React.Fragment> <Head> <title>My page</title> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> </Head> <ThemeProvider theme={theme}> {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} <CssBaseline /> <Component {...pageProps} /> </ThemeProvider> </React.Fragment> ); } MyApp.propTypes = { Component: PropTypes.elementType.isRequired, pageProps: PropTypes.object.isRequired, };_document.jsimport React from 'react'; import Document, { Html, Head, Main, NextScript } from 'next/document'; import { ServerStyleSheets } from '@material-ui/core/styles'; import theme from '../src/theme'; export default class MyDocument extends Document { render() { return ( <Html lang="en"> <Head> {/* PWA primary color */} <meta name="theme-color" content={theme.palette.primary.main} /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } } // `getInitialProps` belongs to `_document` (instead of `_app`), // it's compatible with server-side generation (SSG). MyDocument.getInitialProps = async (ctx) => { // Render app and page and get the context of the page with collected side effects. const sheets = new ServerStyleSheets(); const originalRenderPage = ctx.renderPage; ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheets.collect(<App {...props} />), }); const initialProps = await Document.getInitialProps(ctx); return { ...initialProps, // Styles fragment is rendered after the app and page rendering finish. styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()], }; };
- 投稿日:2020-08-10T15:45:50+09:00
Firebaseでユーザー認証を行うときのメソッド
Firebese Authのメソッド
1. createUserWithEmailAndPassword()
文字通りメールアドレスとパスワード認証でユーザーを作成するメソッド
引数にpassword
を受け取る
戻り値はPromis.then(result => { const user = result.userとすると
user.uid
などで取得しやすいfirestoreに登録する場合
if (user) { const uid = user.uid const timestamp = FirebaseTimestamp.now() const userInitialDate = { created_at: timestamp, email: email uid: uid, updated_at: timestamp, username: username } db.collection('users').doc(uid).set(userInitialDate) .then(() => { dispatch(push('/')) }) }とすることでusersコレクションに登録される。ちなみに
db
の中身はfirebase.firestore()
になっていて、処理が完了するとルートパスに戻る。2.signInWithEmailAndPassword()
上記で作成したアカウントにログインするメソッド
引数にemail, passwordを受け取る
上記同様に実行結果を定数に入れることで扱いやすくなる
firebase Authでサインインした場合アプリ側にもstateを更新する必要があるので、以下のようにするif (user) { const uid = user.uid db.collection("users").doc(uid).get() .then(snapshot => { const data =snapshot.data() dispatch(signInAction({ isSignedIn: true, uid: uid, username: data.username })) dispatch(push("/")) }) }データベースからuidが一致する情報を引っ張り、
signInAction
を実行しreduxのstateを変更している(reduxではactionsは変更を伝える役割で実際にはreducersがどう変更するか決めている)
- 投稿日:2020-08-10T15:43:58+09:00
[ApexCharts] React / Angular などでテスト(jest)を実行すると 'TypeError: r.node.getBBox is not a function' 発生
概要(症状)
私の場合
React
ですが、以下のようなエラーが出ました。エラー状況(node:3677) UnhandledPromiseRejectionWarning: TypeError: Caught error after test environment was torn down Cannot read property 'body' of null (node:3677) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 23) FAIL src/App.test.js (28.739s) ● renders hoge huga TypeError: r.node.getBBox is not a function ...以下略テスト対象(※Reactのソースです)
エラーが発生するテスト対象はこんな感じ
なるべく削ぎ落としてます。App.jsimport React, { Component } from 'react'; import ReactApexChart from 'react-apexcharts' class App extends Component { constructor(props) { super(props); this.state = { options: { chart: { type: 'candlestick', height: 350 }, title: { text: 'CandleStick Chart', align: 'left' }, xaxis: { type: 'datetime' }, yaxis: { tooltip: { enabled: true } } }, series: [{ name: 'series-1', data: [{ x: new Date(1538778600000), y: [6629.81, 6650.5, 6623.04, 6633.33] }, { x: new Date(1538780400000), y: [6632.01, 6643.59, 6620, 6630.11] }, { x: new Date(1538782200000), y: [6630.71, 6648.95, 6623.34, 6635.65] }, { x: new Date(1538784000000), y: [6635.65, 6651, 6629.67, 6638.24] }, { x: new Date(1538785800000), y: [6638.24, 6640, 6620, 6624.47] }, { x: new Date(1538787600000), y: [6624.53, 6636.03, 6621.68, 6624.31] }, { x: new Date(1538789400000), y: [6624.61, 6632.2, 6617, 6626.02] }, { x: new Date(1538791200000), y: [6627, 6627.62, 6584.22, 6603.02] }, { x: new Date(1538793000000), y: [6605, 6608.03, 6598.95, 6604.01] }, { x: new Date(1538794800000), y: [6604.5, 6614.4, 6602.26, 6608.02] }, { x: new Date(1538796600000), y: [6608.02, 6610.68, 6601.99, 6608.91] }, ] }] } } render() { return ( <> <div id="chart"> <ReactApexChart options={this.state.options} series={this.state.series} type="candlestick" width={800} height={350} /> </div> <span>test hoge huga !</span> </> ); } } export default App;対処
github の issue が既にいくつか上がっており、mockを2つ作成することでこのエラーが回避できました。
英語だと読解に時間がかかったので(´;ω;`)ブワッ、日本語化しておこうと思った次第です
ソースコード
こんな感じでOKでした。
App.test.jsimport React from 'react'; import { render } from '@testing-library/react'; import App from './App'; // この2つの jest.mock を追加しただけ ↓ jest.mock("react-apexcharts", () => jest.fn(() => { return null; }) ); jest.mock("apexcharts", () => ( { exec: jest.fn(() => { return new Promise( (resolve, reject) => { resolve("uri"); } ); }) } )); test('renders hoge huga', () => { const { getByText } = render(<App />); const linkElement = getByText(/hoge huga/i); expect(linkElement).toBeInTheDocument(); }); // result => PASS src/App.test.js (15.903s)参考記事
- 投稿日:2020-08-10T15:39:50+09:00
Reactを使用してWeb画面を作成する
はじめに
reactとreact-reduxを調べたときに概念や考え方など難しい話から入っているサイトやちょっと難しい(カッコいい)Web画面をサンプルに使用していて理解が難しいなと感じました。
そのため、単純なサンプルを使用して最低限の説明のみをしようと思います。環境
- node.js: v12.18.2
- webpack: 4.44.1
- React: 16.13.1
環境作成
node.jsのインストール
公式のサイトに従ってインストールしてください
公式サイト: https://nodejs.org/ja/プロジェクト用のファイルを作成
node.jsのプロジェクトではプロジェクトの設定やインストールしたパッケージなどをpackage.jsonに記載します。
次のコマンドでpackage.jsonを作成するといくつかの入力項目がありますが基本的にすべてデフォルトで問題ないです。npm initBabel
環境やブラウザのバージョンによって使用できるJavaScriptの仕様が異なります。その仕様の差分を埋めるために、Babelを使用して作成したJavaScriptを対応可能なものに変換します。
Babelのインストール
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react @babel/registerBabelの設定作成
Babelの設定はプロジェクト直下の
.babelrc
に記載します。そのため、このファイルを作成して以下の内容を記載します。{ "presets": ["@babel/env", "@babel/preset-react"] }webpack
Web画面からJavaScriptを読みだす際にJavaScriptのファイルが多いと無駄な時間や処理が発生します。webpackを使用すると複数のファイルを一つにまとめていい感じにしてくれます。
webpackのインストール
webpackに必要なライブラリの他にもローカルでサーバを起動するために
webpack-dev-server
をインストールします。npm install --save-dev webpack webpack-cli webpack-dev-server style-loader css-loader babel-loaderwebpackの設定作成
webpackの設定はプロジェクト直下の
webpack.config.js
に記載します。このファイルを作成して以下の内容を記載します。webpack.config.jsconst path = require("path"); const webpack = require("webpack"); module.exports = { entry: "./src/index.js", mode: "development", module: { // ファイルをどのように変換すればよいのかのルールを設定。 // testで入力するファイルの条件、excludeで除外する条件、 //loaderで外部ライブラリのルールを参照する rules: [ { test: /\.(js|jsx)$/, exclude: /(node_modules|bower_components)/, loader: "babel-loader", }, { test: /\.css$/, use: ["style-loader", "css-loader"] } ] }, // ビルドの順番を設定 resolve: { extensions: ["*", ".js", ".jsx"] }, // ビルド後の設定 // pathは、ビルド後のファイルを吐き出すフォルダ、 // filenameはビルド後のファイル名を設定 output: { path: path.resolve(__dirname, "dist/"), filename: "bundle.js" }, // ローカルで起動するサーバの設定 // contentBaseでブラウザからアクセスしたときのルート、 // portはブラウザからアクセスするときのポート番号、 // hotOnlyはファイルを更新したときに自動読み込みをする設定 devServer: { contentBase: path.join(__dirname, "public/"), port: 8080, hotOnly: true }, plugins: [new webpack.HotModuleReplacementPlugin()] };react
reactのインストール
npm install react react-domWeb画面のソースの作成
フォルダの作成
プロジェクトルート直下にsrcとpublicとdistのフォルダを作成してください。
※上のwebpackの設定を変えたときはここも変えてください。project_root ├─dist // ビルド後のファイルを格納 ├─public // htmlを格納 ├─src // reactのJavaScriptファイルやCSSファイルを格納 ├─.babelrc ├─package.json ├─webpack.config.jshtmlファイルの作成
ブラウザからアクセスした際に一番最初にアクセスされるhtmlファイルを作成します。
※webpackのビルド後のファイルをインポートするのを忘れないでください。index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>React Sample</title> </head> <body> <div id="app"></div> <script src="./bundle.js"></script> </body> </html>reactのrendarファイルの作成
reactの機能をつかってレンダリングするJavaScriptファイルを作成します。
ReactDOM.render()
にコンポーネントファイルとdocument.getElementById(置き換えるhtmlのid)を指定してあげます。
上のindex.htmlの<div id="app"></div>
とAppコンポーネントを置き換えたいのでReactDOM.render()
に<App />
とdocument.getElementById('app')
を指定します。index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <App />, document.getElementById('app') );reactのコンポーネントファイルの作成
実際にWeb画面に表示するための情報を書いたコンポーネントのJavaScriptファイルを作成します。
基本的にはコンポーネントファイルを増やして、Web画面を増やしたりWeb画面の要素を増やしたりします。
※上のindex.jsのimport App from './App';
でこのファイルをインポートしています。最後の行のexportを忘れないでください。App.jsimport React, { Component} from "react"; class App extends Component{ render(){ return( <div className="App"> <h1> Hello, World! </h1> </div> ); } } export default App;Web画面の起動
ビルド
開発用のサーバを起動するときに同時にビルドが走るので特に必須ではないですが、webpackの設定や作成したファイルが間違っていないかをチェックするために一旦ビルドします。
プロジェクトのルートで次のコマンドを実行するとビルドが走ります。Windowsの場合はダブルクォーテーションで囲まないとうまくいかないです。"./node_modules/.bin/webpack"ビルドが成功するとdictフォルダ内にJavaScriptファイルが一つできるはずです。
開発用サーバの起動
プロジェクトのルートで次のコマンドを実行するとビルドとサーバの起動が走ります。Windowsの場合はダブルクォーテーションで囲まないとうまくいかないです。
"./node_modules/.bin/webpack-dev-server"サーバが起動したらブラウザからlocalhost:8080にアクセスするとHello, World!が表示されます。
終わりに
Reduxやaxiosとの連携を書こうと考えていましたが、予想以上に長くなったので今回はここまでにします。
次回以降にReduxとaxiosのサンプルと簡単な説明を書いていこうと思います。
それぞれまとめました。
Redux:ReduxとReduxToolkitを使用してReact内でデータを管理する
axios:ReactのRedux内でaxiosを使用した通信をする
- 投稿日:2020-08-10T13:55:39+09:00
ReactサーバをプロキシにしてCORSに引っかからないようにする
create-react-appでreact環境を作りました。
次にAPIを叩きたいのですが、今のreactサーバからやってもいいけど負荷が集中するからあまり現実的じゃない。
(→役割分担させる。webサーバは基本的にページを返す。APIサーバは基本的に裏でAPI叩いてデータのやり取りを行う。)そこでreactサーバとネットの間にもう一つサーバ(Flask)をかますことにしました。
ただ、このまま使ってもCORSに引っかかってうまくAPIを叩くことができません。CORS
セキュリティ的な問題でデフォルトで設定されている。
ブラウザから入力したものが意図しないリソースにアクセスされないように。
参考
https://dev.classmethod.jp/articles/cors-cross-origin-resource-sharing-cross-domain/create-react-appのプロキシ設定
それ用のmiddlewareがあるからそれを使えば簡単に設定できる。
窓口は一つのポート番号にして、そこから裏で動いている別サーバにアクセすることが可能
参考
https://applingo.tokyo/article/1568注意点
直接URLにFlaskのエンドポイントを入力しても画面は真っ白のまま、、、
一方で、コンポーネント内でaxiosを使ってgetするとうまくいく、、、
これは、create-react-appの仕様上の問題で、access headerがtext/htmlじゃないもののみプロキシに渡すようになっているらしい。
だから、直接URLに入力しても何も帰ってこないのか、納得。
詳しくは公式ドキュメントを参考にしてください。
https://create-react-app.dev/docs/proxying-api-requests-in-development/
- 投稿日:2020-08-10T12:48:31+09:00
Reduxのデバックに必須!Redux DevToolsの使い方
react reduxを使用して開発しているときに必須のデバックツールだと思います。
Google Choromeのエクステンションでstateの偏移や、アクションの実行の履歴などを確認できます。Redux DevToolsをインストールする
chrome ウェブストアでインストールしましょう。
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja
そしたらRedux DevToolsのアイコンが右上にでると思います。
そしたらreactのプロジェクトのindex.jsを書き換えましょうindex.jsimport React from 'react'; import { ReactDOM } from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, compose } from 'redux'; import App from './App'; import reducer from './reducers'; const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )redux-thunkなどのミドルウェアを使用するならこうなります。
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware, compose } from 'redux'; import reduxThunk from 'redux-thunk'; import App from './App'; import reducers from './reducers'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk))); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.querySelector('#root') );これだけで準備は完了です。
Redux DevTools を使用する
あとはローカルサーバーを起動したときにアイコンをクリックすればウィンドウがでます。
右側にあるActionを押せばなんのActionが実行されたか、Stateを押したらStoreの状況を見ることができます。他にも機能があるので色々試してみてください。
以上です。便利なデバックツールを活用して開発を行っていきましょう。
- 投稿日:2020-08-10T12:02:22+09:00
React初心者が、React.Componentのライフサイクルを理解する
React.Componentには、
- Mounting
- Updating
- Unmountingがあります。それぞれで呼び出される関数があり、それらを実装すると、コンポーネント自体のライフサイクルを操作できる。
Mounting
- constructor(props)
- static getDerivedStateFromProps(nextProps, prevState)
- componentWillMount() / UNSAFE_componentWillMount()
- render()
- componentDidMount()
それぞれの関数の説明
constructor()
コンポーネっとのクラス自体のコンストラクタです。
super()を実行することが必須で、受け取ったpropsを処理する
stateの初期化もこの段階で行う。static getDerivedStateFromProps(nextProps, prevState)
propsとstateを確認して、propsによってstateを書き換えるかどうかを決定する。
書き換えが必要であれば、書き換え後のstateを作成し、その値を返す。
書き換えが不要であれば、nullを返す。
この関数を通すため、コンストラクタがstateは初期化しておくべき!componentWillMount() / UNSAFE_componentWillMount()
React0.17ではいしされる。
安全でないので使わないrender()
描画を扱う関数。
componentDidMount()
コンポーネントのMountionの処理が全て完了した際に呼び出される関数で、コンポーネントで一度だけ呼び出される。
一般には、コンストラクタでstateの箱を作り、componentDidMount()から必要なデータを取得しにいくような処理を記述。Updating
- componentWillReceiveProps(props) / UNSAFE_componentWillReceiveProps()
- static getDerivedStateFromProps(nextProps, preState)
- shouldComponentUpdate(nextProps, nextState)
- componentWIllUpdate() / UNSAFE_componentWillUpdate()
- render()
- getSnapshotBeforeUpdate(prevProps, prevState)
- componentDidUpdate(prevProps, prevState,snapshot)
それぞれの関数の説明
shouldComponentUpdate(nextProps, nextState)
コンポーネントのアップデータが必要かどうかを判断するための関数。
getSnapshotBeforeUpdate(prevProps, prevState)
UPdatingの処理でrender()のあとに呼び出され、この関数でreturnされる値がcomponentDIdUpdateのsnapshotとして呼び出される。
Uumounting
- componentWIllUnmount()
それぞれの関数の説明
componentWIllUnmount()
メモリリークを抑えるために参照の削除などを実装
参照
- 投稿日:2020-08-10T11:37:59+09:00
VS CodeでESlint、Prettierを使用したReact環境を構築する
Reactの環境構築はcreate-react-appでとても簡単になりました。ここにコードチェック、整形ツールであるESlint、Prettier を導入してみます。
ESlint、Prettierを使用した環境構築方法やルールはプロジェクトによって違うと思うので、基本的な設定だけしています。プロジェクトに合わせて編集してください。
完成品はGitHubにアップしています。create-react-appで生成されるReactのアイコンなどは削除しています。
https://github.com/nineharker/react-vscode-eslint-prettier
環境構築
それでは環境構築していきましょう!パッケージ管理にyarnを使っていきますが、npmを使用している人は便宜読み替えてください。
VS CodeにESlintとPrettierの拡張機能を追加する
VS Codeの拡張機能としてESlintをインストールしましょう。
Prettierもインストールします。
これで必要な拡張機能はインストールできました。create-react-appでプロジェクトを作成する
Reactプロジェクトを作成しましょう。
create-react-app sample必要なパッケージをインストールする
create-react-appで作成された雛形では、すでにESLintに関するパッケージが導入されています。
create-react-appで作成したプロジェクトの場合、eslintとbabel-eslint、eslint-loaderをインストールしたらエラーが発生するのでインストールしないください。
yarn add --dev prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react
eslint、prettierの設定ファイルを生成する
プロジェクトルートに.eslintrc.jsと.prettierrc配置してルールを書いていきます。 基本的な設定だけを書いています。
.eslintrc.jsmodule.exports = { "env": { "es6": true, "node": true }, "parser": "babel-eslint", "plugins": [ "react", "prettier" ], "parserOptions": { "version": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended", "prettier/react" ], "rules": { "prettier/prettier": "error" } }.prettierrc{ "printWidth": 120, "useTabs": false, "semi": true, "singleQuote": true, "trailingComma": "es5", "bracketSpacing": true, "jsxBracketSameLine": false }VS Codeの設定でセーブ時に整形するようにする
セーブ時に整形するようにVS Codeの設定を変更しましょう。VS Codeのデフォルトのフォーマット機能をオフにしています。
{ "javascript.format.enable": false, "eslint.autoFixOnSave": true }おわり
お疲れ様でした!これで設定が完了です。App.jsなどのコンポーネントの拡張子はjsxに変更しましょう。
ESlint、Prettierの設定は大変ですが、その後の開発が圧倒的に楽になるのでぜひお試しください。
- 投稿日:2020-08-10T01:02:49+09:00
Gatsbyjs + Netlify + Contentful で Blogを作ってみた。
Gatsbyjs + Netlify + Contentful で Blogを作成
以前にgatsby-starter-blogでブログを作成したのですがCMSを利用したいと思い、
新たにgatsby-starter-gcnでブログを作成したので備忘録を残します。利用したサービス
- Gatsbyjs
GatsbyjsはReactを利用したモダンなサイトを高速に作成できるオープンソースフレームワークです。
Gatsbyjsでなにか作りたい方はテンプレートが豊富に用意されているので、そちらを利用するといいと思います。
- Netlify
Netlifyは静的なサイトを高速で提供できるホスティングWebサービスです。
フロントエンドのビルド、デプロイ、ホスティングの全てを高速に行ってくれます。
また、NetlifyはGitHubリポジトリとリンクして、GitHubリポジトリにプッシュがある度にビルド・デプロイをしてくれます。
- Contentful
ContentfulはAPIファーストなHeadless CMS(コンテンツ管理システム)です。コンテンツとはブログ記事などのことです。
WordPressなどのCMSとは異なり、開発者はREST API経由で記事(コンテンツ)を取得し、アプリケーションやデバイスのUIに反映させるというものです。
特徴はなんといってもAPIベースでコンテンツを管理・取得できるので、表示(フロントエンド)側での制約がなくなり、管理しているコンテンツをWebからでも、モバイルからでも取得し、表示することができる、つまり、フロントエンドとバックエンド切り離すことができるということです。
今回はその管理の分離を考えて、HeadlessCMSを利用することにしました。ブログ作成手順
こちらからブログ作成の手順を残しておきます。
Gatsbyプロジェクトの作成
まずはこちらのリンクからGatsbyjsのテーマを選びます。
今回はGatsbyjs + contentful + Netlifyという構成にしたかったので、
gatsby-starter-gcnを選びました。上記リンク先のSourceからGithubにとべるので、そちらのREADMEにしたがって作成していきます。
まずはプロジェクトを作成します。
$ gatsby new gatsby-starter-gcn(任意のプロジェクト名) https://github.com/ryanwiemer/gatsby-starter-gcn.git上記コマンドでGatsbyのプロジェクトを作成します。(gatsby-cliが必要です。)
自分の場合は以下のエラーが出ましたが、現在の最新のnodejsのバージョン(14.7.0)に上げたらエラーがなくなりプロジェクトを作成できました。(エラーが確認できたnodejsのバージョンは13.2.0でした。)
/usr/local/lib/node_modules/gatsby-cli/node_modules/yoga-layout-prebuilt/yoga-layout/build/Release/nbind.js:53 throw ex; ^ Error: No valid exports main found for '/usr/local/lib/node_modules/gatsby-cli/node_modules/@urql/core' at resolveExportsTarget (internal/modules/cjs/loader.js:611:9) at applyExports (internal/modules/cjs/loader.js:492:14) at resolveExports (internal/modules/cjs/loader.js:541:12) at Function.Module._findPath (internal/modules/cjs/loader.js:643:22) at Function.Module._resolveFilename (internal/modules/cjs/loader.js:941:27) at Function.Module._load (internal/modules/cjs/loader.js:847:27) at Module.require (internal/modules/cjs/loader.js:1016:19) at require (internal/modules/cjs/helpers.js:69:18) at Object.<anonymous> (/usr/local/lib/node_modules/gatsby-cli/node_modules/urql/dist/urql.js:206:12) at Module._compile (internal/modules/cjs/loader.js:1121:30) { code: 'MODULE_NOT_FOUND' }無事プロジェクトが作成できたので次にいきます。
ContentfulのSetup
次にこちらにしたがってContetfulの設定をしていきます。
まずcontentfulのアカウントを作成し、空ページを作ります。
最初はチュートリアルページがあるかと思うので、そちらを削除し、画面左上の
+ Create space
を押して空ページを作成します。ヘッダーのSpace Homeを押した際に以下のページが表示されていると思います。
次にContentfulのAPIKEYの設定をします。
$ npm run setup上記のコマンドを叩くと
- SPACE ID
- Content Delivery API access token
- Content Preview API access token
- Content Management API access token
を聞かれるのでそれぞれContentfulのSpace Settings → API keysから取得して入力します。
Content delivery / preview tokensとContent management tokensのタブの情報が両方必要なので注意してください(tokenはなければ作成してください)。これでContentfulの内容が反映されるようになったので、
$ gatsby developでローカルプロジェクトを立ち上げて http://localhost:8000/ でローカルでコンテンツの確認をすることができるようになりました。
あとはNetlifyを使ってBuild・Deployの設定をします。
NetlifyのSetup
Netlifyのアカウントを作成したらこちらからNetlifyとgitプロジェクトの紐付けをしていきます。
リンク先の画面でGithubを選択し、該当のGithubリポジトリを選択します。
次の画面でDeploy Siteを押し、サイトのDeployを開始します。Deployが開始されましたがそのままではビルドがこけてしまうので環境変数を設定します。
NetlifyトップページのSettings → Build & Deploy → Build Environment Variables.から
READMEにならってSPACE_IDとACCESS_TOKENを設定します。その後Deploysから手動でDeployを実行します。
すると今度はDeployが成功していると思います。これでプロジェクトのmasterブランチにpushすれば本番環境にbuild・deployされるようになりました。
最後にContentfulでWebhookの設定をしておきます。
ContentfulでWebhookの設定
このままだとContentfulで記事を投稿しても手動でdeployしないといけないので、
Contentfulで記事が投稿、削除されたらNetlifyに通知し、Deployされるようにしたいと思います。例によってこちらもREADMEどおりに進めます。
Netlifyの Settings → Build & Deploy → Build hooksからbuildhookを新たに作成します。
こちらで作成したBuildhookのURLをContentfulのWebhooksに設定します。
ContentfulでSettings→Webhooksを選択し、画面右側のNetlifyのテンプレートを選択します。
以下のNetlifyのAddを選択。
TriggersにはPublish Unpublish Deleteを選択しておきます。
これでWebhooksの設定も完了したのでContentful上で記事の投稿・削除をすればNetlifyに通知され
自動でDeployされるようになりました![FYI]Contact Form
BlogのContactで送られたものはNetlifyのFormsで確認できますが、こちらの通知も設定できますので通知してほしいという方はREADMEにならって設定してください。
ブログの作成手順は以上になります。
なににおいても継続することが成功への近道だと思うので、
しっかりアウトプットしていきたいと思います!参考
- https://quantum-native.com/gatsby-netlify-contentful-web/ ← 大いに参考にさせていただきましたm(_ _)m ありがとうございます!