- 投稿日:2021-02-20T23:42:55+09:00
React*Material-UIのCreateStylesで擬似クラスを扱う
概要
何故かMaterial-UIのCreateStyles環境下における擬似クラスの取り扱いの記述が公式にあまりなかった為、どうやって適用すれば良いのか当初詰まりました。
最終的にこうやれば良いのだと発見した為、確認できた対応方法を記載致します
記載方法
sample.tsxconst useStyle = makeStyles(theme => createStyles({ box1: { color: 'black', '&:focus': { '& + $box2': { color: 'red', }, }, }, box2: { color: 'black', }, }), ); export function InputStandard() { const clsx = useStyle(); return ( <div> <input className={clsx.box1} /> <div className={clsx.box2}>box2</div> </div> ); }CreateStylesの場合は通常のCSSの記載方法が取れず、例えば隣接要素へfocus時のCSSを適用したい場合は、focus対象のクラス内でネストさせて、そこで $class名とする必要がありました。
なおこの例では$class名としておりますが、$要素名でも行けます。
そもそもStateで管理してJS側で管理するパターンの方が多いかもしれませんが、どなたかの参考になれば幸いです
- 投稿日:2021-02-20T23:23:17+09:00
Reactについての知見まとめ
useState
の状態を変更したときは再レンダリングが行われますが、useRef
の状態を変更したときは行われません。const App: React.FC<{}> = () => { const [count, setCount] = useState<number>(0); console.log(count); // 0 と 1 が出力されます useEffect(() => { setCount(1); }, []); return <></>; };const App: React.FC<{}> = () => { const count = useRef<number>(0); console.log(count.current); // 0 だけが出力されます useEffect(() => { count.current = 1; }, []); return <></>; };
useState
で今の状態から次の状態を作るとき、次のように実装すると意図した結果が得られません。const App: React.FC<{}> = () => { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); setCount(count + 1); } return <> {/* 1 クリックにつき count は 1 しか増えません */} <button onClick={handleClick}>Click me!</button> {count} </>; };意図した結果を得るためには、
setCount
に関数を渡します。この関数の第一引数には今の状態が渡ります。関数の戻り値が次の状態になります。const App: React.FC<{}> = () => { const [count, setCount] = useState(0); function handleClick() { setCount(count => count + 1); setCount(count => count + 1); } return <> {/* 1 クリックにつき count は 2 増える */} <button onClick={handleClick}>Click me!</button> {count} </>; };
useEffect
に渡した関数が実行されるタイミングは、レンダリングが完了した後です。よって、次のコードは必ずレンダリング後の要素の幅と高さを出力します。const App: React.FC<{}> = () => { const ref = useRef<HTMLDivElement>(null); useEffect(() => { if (ref.current === null) return; const style = window.getComputedStyle(ref.current); console.log(style.width, style.height); //=> 1440px 18px }, []); return <div ref={ref}>test</div>; };
useRef
のcurrent
の変更を検知したいことがあります。しかし、useRef
にはcurrent
の値の変更を検知する方法は用意されていません。変更を検知したいときは、代わりに「コールバックref」を使います。コールバックrefの具体例としては、canvas
要素が存在するときに限りコンテキストを取得したい、というケースがあります。次のコードは、
canvas
要素が存在すればsetContext
を実行し、存在しなければ何もしないコードです。const App: React.FC<{}> = () => { const [show, setShow] = useState(true); const [, setContext] = useState<CanvasRenderingContext2D | null>(null); useEffect(() => { // 1秒毎にcanvas要素の存在の有無を切り替えます const id = window.setInterval(() => setShow(show => !show), 1000); return () => window.clearInterval(id); }, []); // useCallbackに渡した関数は、nodeが変わったときに呼ばれます const callbackRef = useCallback((node: HTMLCanvasElement | null) => { if (node === null) return; setContext(node.getContext('2d')); }, []); return <> {show && <canvas ref={callbackRef}></canvas>} </>; };同等のことをコールバックrefなしで実現しようとすると、おそらく複雑なコードになります。
useState
のsetXXX
を実行した後のレンダリングのタイミングは遅延されることがあります。たとえば次のコードは、setCount(1)
実行後に即座にレンダリングされず、setCount(2)
実行後にレンダリングされます。よってコンソールには0
と2
が出力されます。const Child: React.FC<{ count: number }> = ({ count }) => { console.log(count); return <></>; }; const App: React.FC<{}> = () => { const [count, setCount] = useState(0); useEffect(() => { (async () => { // await null; setCount(1); setCount(2); })(); }, []); return <Child count={count} />; };しかし、
// await null;
のコメントアウトを外すだけで、コンソールには0
、1
、2
が出力されるようになります。これは、setCount(1)
実行後に即座にレンダリングされることを意味します。このように、
setXXX
実行後にレンダリングされることは保障されていますが、実行後のどのタイミングでレンダリングされるかについては保障されていません。したがって、タイミングへの依存はなくす必要があります。
- 投稿日:2021-02-20T23:14:28+09:00
Twitter APIで取得できるツイートのIDの丸め誤差問題を解決する
現在React Native + Expo + Twitter APIで個人開発をしているのですが、Twitter APIに苦戦したので書きます。
コードは基本的にReact/React Native上で動くものになります。idが大きすぎる
Twitter APIでは一度で取得できるツイート数に制限があります。
import Client from '../hoge'; const getTweets = () => { Client.get('favorites/list.json',{ count: 200, // 最大200まで tweet_mode: 'extended', }) };なので、自動スクロールなどで追加取得する際にパラメーターにmax_idかsince_idを追加する必要があります。
import Client from '../hoge'; const getTweets = () => { Client.get('favorites/list.json', { count: 200, max_id, // 追加 tweet_mode: 'extended', }) };この時、最後に取得したツイートのidをそのまま渡すと、渡されたID以下のツイートが取得されることになるので、最後のツイートが重複してしまいます。なので、idに-1をしてから渡さないといけません(古い順で取得してる時は+1)。
ここで問題が発生します。これは実際の自分のツイートのレスポンスです。
{ created_at: "Sat Feb 20 13:38:04 +0000 2021", id: 1363120740321558500, id_str: "1363120740321558529", full_text: "明日中にアプリの申請絶対だすぞ", ... }number型のidとstring型のid_strが返ってくるのですが、number型の方は値が大きすぎて丸まっています。さすがTwitterです。
実際に計算をしてみても、結果は以下のようになります。
console.log(tweet.id, tweet.id - 1); // 1363120740321558500 1363120740321558500正確な値であるid_strの方を使うことになるのですが、文字列なので数値に変換しないといけません。しかしparseIntなどで変換しても上記の計算と同じ結果になってしまうので、Big Intとして計算しないといけません。
解決策
簡易的なコードの例ですが、自分の解決策を載せます。bignumber.jsというライブラリを使用しました。
yarn add bignumber.jsimport React, { useState } from "react"; import { BigNumber } from "bignumber.js"; import Client from '../hoge'; export const App = () => { const [tweets,setTweets] = useState([]); // 既に取得しているツイートが入ります ... const getMoreTweets = () => { const max_id = new BigNumber(tweets.slice(-1)[0].id_str).minus(1).c?.join(''); // 最後に取得したツイートのidを-1 Client.get('favorites/list.json', { count:200, max_id, tweet_mode: 'extended' }).then((res:any) => setTweets([...tweets,...res])); } ... }
- 投稿日:2021-02-20T22:51:57+09:00
framer-motionを使って遷移時にアニメーションを追加する
お題
framer-motionライブラリの使い方
・framerが提供しているコンポーネントにアニメーションを付与することができる。
・タグでJSXを包むことでページ遷移にもアニメーションをつけることができる。
導入方法
①npm install framer-motionでインストールする
②AppRouterページでswitchタグをタグとタグで囲む
包んだswitchにはlocationとkeyを設定する。locationはどこのページにいるかframer-motionに教える役割を持っており、これが設定されていないとアニメーションが機能しない。keyにはlocation.pathnameを設定。これはパス名。ここではloginやtitleが該当する。
HashRouterを使っている場合はRoute render={({ location }) => ()}で取得することができる。
AppRouter.jsimport { AnimatePresence, motion } from "framer-motion" <Router> <Route render={({ location }) => ( <AnimatePresence exitBeforeEnter initial={false}> <motion.div> <Switch location={location} key={location.pathname}> <Route exact path="/" render={() => <Redirect to="/top" />} /> <Route exact path="/login" component={LoginPage} /> <Route exact path="/title" component={TitlePage} /> </Switch> </motion.div> </AnimatePresence> )} /> </Router>これで下準備は完了。後はアニメーションを設定したいページにmotion.divを入れこみます。
ログインページもといサンプルページで実装するとこんな感じになります。
Login.jsimport { motion } from "framer-motion"; import { animationService } from '../../services/AnimationService'; class LoginPage extends Component { constructor(props) { super(props); const errors = { mail: "", password: "", }; this.state = { animation: animationService.getAnimationMotions() } } render() { return ( <Page> <LoginHeader title="ログイン" /> <div id="login-page"> <motion.div animate={{x: 0}} initial={{x: 100}} exit={{x: -100}} transition={{duration: 0.2}}> <Button modifier="large" onClick={this.submit} disabled={this.state.isBtnDisabled}>ログイン</Button> </motion.div> </div> </Page> ); } } export default withRouter(LoginPage);motion.divのプロパティにそれぞれanimate,initial,exit,transitionと設定しています。遷移アニメーションで重要なのは始発点(initial)と終着点(exit)であり、これをinitialとexitでそれぞれx地点のどこからアニメーションが始まるかまたは終了するかを設定する必要があります。
transitionにはアニメーションが動作する時間を設定します。
以上で基本的な実装は完了。
他にもコンポーネントそのものにアニメーションを加えたりページによって遷移アニメーションを分けることもできるので組み合わせ次第でよりリッチに見えるアプリを作ることができます。
initialとかexitの値をアプリで固定して使いたい場合は別ファイルでまとめて管理すると楽になります。
まとめ
react.jsでアニメーションを描画するのってめんどくさそうと思っていましたがこのライブラリはAppRouterを多少いじることに目を瞑ればめちゃめちゃ便利ですね。
- 投稿日:2021-02-20T20:47:03+09:00
入力文字の行数で縦幅が可変するtextareaのReactコンポーネント
概要
次の記事を参考に入力文字の行数で縦幅が可変するReactコンポーネントを作成しました
react hook formがすぐに使いたかったのでforwordRefを使用して実装しています
- 【参考記事】内容に応じてサイズが可変する を素敵に実装する
- (非常にわかりやすく助かりました。ありがとうございます)
TypeScriptのコード
import { ComponentProps, forwardRef, useRef } from "react" export const FlexTextArea = forwardRef<HTMLTextAreaElement, ComponentProps<"textarea">>( (props, ref) => { const dummyRef = useRef<HTMLDivElement>(null) return ( <div className={"flex_text_area"}> <div className={"flex_text_area_dummy"} aria-hidden={true} ref={dummyRef} /> <textarea {...props} ref={ref} onChange={(e) => { if (dummyRef.current) { dummyRef.current.textContent = e.target.value + "\u200b" } if (props.onChange) { props.onChange(e) } }} /> </div> ) }, )スタイル(less)
.flex_text_area { position: relative; .flex_text_area_dummy { overflow: hidden; visibility: hidden; box-sizing: border-box; padding: 5px 15px; min-height: 120px; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; border: 1px solid; } & > textarea { position: absolute; top: 0; left: 0; box-sizing: border-box; padding: 5px 15px; width: 100%; height: 100%; background-color: transparent; border: 1px solid #b6c3c6; border-radius: 4px; color: inherit; font: inherit; letter-spacing: inherit; resize: none; } }
- 投稿日:2021-02-20T20:23:08+09:00
学習週次報告 #2
お疲れ様です。
この記事は、すごく個人的な学習週次報告です。
自分自身の「目標の明確化」「学んだこと・わからないことの整理」「成長記録」のために書いています。今回は、”今週で主にやったこと”から、
”今週で学んだこと”、”つまづいたけど7、8割くらい理解できたこと”、
”つまづいて、今でもよくわかっていないこと”を整理して、
”次週何をやるべきか”を書いていきます。
今週で主にやったこと
- React、ReacrRauter、Redux、ReactRedux、ReduxThunkの復習
- React:簡易的な掲示板の作成
- ReacrRauter:APIを使ったクイズアプリの作成
- Redux:TODOを管理できるツールの作成
- ReactRedux:Reduxで作ったツールを元にTODOアプリの作成
- ReduxThunk:ReacrRauterで作ったクイズアプリのデータをStore管理できるようにする
- ”リーダブルコード”を読んだ
- ”自己紹介サイト”の制作(まだ構成を形にした段階で、完成には至っていない)
今週で学んだこと
- ”アトミックデザイン”について学んだ
- ページ構成の最小単位(ボタンやフォームなど)からデザインを決めていき、最終段階のページ制作を効率化させるやり方のこと
- 従来のデザインと違って、コーディング目線から産まれた手法
- メリット1:更新変更に強い
- メリット2:特に大規模プロジェクトでは、メンバーで共有しやすいため、一貫性を持たせられる
つまづいたけど7、8割くらい理解できたこと
- React Reduxでのコンポーネントのコンテナー化はまだつまずくが、
進めているうちに、Storeの情報をmapStateToPropsで取得してコンポーネントに持ってきたり、
コンポーネントサイドで行ったStore情報の更新をmapDispatchToPropsで送ったりしてるのかなあ、と
なんとなく感覚が掴めてきている感じがする。- サイト制作にあたってyarnインストールしてみた
(理由:どうやらnpmより速いらしく、いろんなネット記事もyarnが多かった)
が、なぜかエラー
→ sudoで解決した(参考記事:https://qiita.com/tsumita7/items/a40a367088018b5bbe33)つまづいて、今でもよくわかっていないこと
- ”リーダブルコード ”を一応全部読んでみたが、
後半になるにつれて話は高度になっていき、自分のレベルではあまり理解できず。
しかし、序盤は初心者にもすごくわかりやすく、綺麗なコードをかく意義とテクニックが書いてあり、
最後の章には具体的な機能実装の手順があって、エンジニア思考のプロセスが追えて勉強になった。
ただ、今の自分にとって緊急性のある内容ではないように感じたので、
また、コーディングに慣れてきた頃に読み返そうと思う。次週何をやるべきか
- 感覚を定着させるためにまたReact、ReacrRauter、Redux、ReactRedux、ReduxThunkの復習
- もう来週には完成をめどに、”自己紹介サイト”制作を進める
内容は以上です。
みなさんお互いに頑張りましょう。little by little.
- 投稿日:2021-02-20T16:41:11+09:00
[AWSxAmplifyxCognito] Amplifyでグローバルサインアウトがグローバルサインアウトしてくれない問題を解決する
はじめに
こんにちは!
認証機能を実装する場合、別のデバイスでサインアウトしたらユーザーがログインしている他の全てのデバイスでもログアウトして欲しいときがありますよね?Amplifyではそんな要望に応えるために、
singOut
関数にglobalオプションがあり、signOut({global:true})
のようにするとサインアウトした際に、ユーザーが持つ全トークンが無効化され、全てのデバイスからサインアウトできます。
以下ドキュメントから抜粋
と、いうのがドキュメントには書かれており、実際ユーザーが認証後に取得する3種類のトークンのうち、アクセストークンとリフレッシュトークンは即座に無効になるのですが、
実は、signOut({global:true})
をしてもIDトークンは無効にならないんです!
加えて、セッションを確認するための関数たち(currentSession
,userSession
)はCognitoにアクセストークンやリフレッシュトークンが有効なのか問い合わせにいったりはせず、ローカルストレージにあるIdトークンの有効期限を確認するだけなんです。つまり、
currentSession
,userSession
関数では、1つのデバイスでグローバルサインアウトが行われていても、他のデバイスではIdトークンが有効期限切れにならない限り、ユーザーは"セッション切れ"とは判断されないんです。対応策
currentSession
,userSession
関数では、アクセストークン、リフレッシュトークンが有効かどうか見てくれない。
なら、それらが有効かどうかを問い合わせる関数を使えばいいのですが、その確認のためだけの関数がAmplifyリファレンスに見当たらないんです。なので、実行時にアクセストークンを使う関数を利用して、その有効・無効を判断します。
例えば、currentUserInfo
関数がそれにあたります。この関数は,
- サインイン状態のとき、以下のようなusername,attributes(cognitoで設定したユーザーの属性)を含むオブジェクトが返ります。
{ "UserAttributes": [ { "Name": "sub", "Value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, { "Name": "email_verified", "Value": "true" }, { "Name": "email", "Value": "xxxxxxxxx@xxxxxxxxx.com" } ], "Username": "xxxxxxxxxxxxxxxxxxxxxxxxxx" }
- そのデバイスでサインアウトして、デバイスにトークンがないときは
null
- 別のデバイスでサインアウトして、そのデバイスに各種トークンはあるが、アクセストークンが無効なときは、空オブジェクト
{}
が返りますしたがって、返り値が null または 空オブジェクト のときユーザーはサインアウト状態であるといえます。
以下はサインイン画面での実装例です。
サインイン画面側の実装例
サインインしてる時のみ、処理を行いたいので
サインアウト状態(null or 空オブジェクト
)の否定であるnullでない かつ 空オブジェクトでない
でサインイン状態かどうかを見ています。(空オブジェクトでない はプロパティがundefinedかどうかで見ています。)const getCurrentUserInfo = async () => { const currentUserInfo = await Auth.currentUserInfo(); return currentUserInfo; } getCurrentUserInfo() .then(currentUserInfo =>{ //以下の場合はサインアウト状態 //1.currentUserInfo === null:そのデバイスでサインアウトして、デバイスにトークンがないとき //2.currentUserInfo.username(などユーザーに関するプロパティ) === undefined: 別のデバイスでサインアウトして、そのデバイスにトークンはあるが、アクセストークンが無効なとき if(currentUserInfo !== null && currentUserInfo.username !== undefined){ //サインインしているときにしたい処理を書く。ログイン画面に飛ばすとか。 } }) .catch(err=> { console.log(err) })サインイン後の画面の実装例
サインアウトしてる時のみ、処理を行いたいので
サインアウト状態(null or 空オブジェクト
)でサインアウト状態かどうかを見ています。const getCurrentUserInfo = async () => { const currentUserInfo = await Auth.currentUserInfo(); return currentUserInfo } getCurrentUserInfo() .then(currentUserInfo =>{ // 以下の場合はサインアウト状態なのでログイン画面に飛ばす。 //1.currentUserInfo === null:そのデバイスでサインアウトして、デバイスにトークンがないとき //2.currentUserInfo.username(などユーザーに関するプロパティ) === undefined: 別のデバイスでサインアウトして、そのデバイスにトークンはあるが、アクセストークンが無効なとき if(currentUserInfo === null || currentUserInfo.username === undefined){ //ログイン画面に飛ばす処理 } }) .catch(err=> { console.log(err) //currentUserInfoが取得できないときはログイン画面に飛ばす。 //ログイン画面に飛ばす処理 })以上です。
上記の処理をページの描画ごとに行えば、
Idトークンが有効であっても、
そのデバイスでサインアウトしている場合だけでなく、別のデバイスでサインアウトしているかどうかも判定ができます。
自分の方法が唯一解ではないと思うので、別案・改善案あればぜひ共有していただければ!終わりに
公式の見解
実は、
signOut({global:true})
がドキュメント通りの挙動でない問題は現在openなissueとしてあがっています。
cognito.user.signOut() does not invalidate tokensこのissueは2019年6月にopenされて現在もまだcloseされていません。このissue以外にも同様の件に関するissueは何件か見られました。
issueにコメントしているユーザーの1人がコメントで個人的にAWSに問い合わせした際の返事を転記していて、それを信じるなら、AWS側はこの問題を認知していて現在取り組み中だそうです。。
以下のどちらかがいつか実装されるといいなと思います。できれば前者。
- グローバルサインアウトでidトークンが即時無効になり、かつ session関数がローカルストレージのトークン有効期限ではなくcognitoに有効かどうかを確認しに行く
- アクセストークンが有効かどうかをCognitoに問い合わせる関数が生える
- 投稿日:2021-02-20T16:12:01+09:00
React Redux の Storeを空にする
React, Redux, ReduxToolKitにかまけていたら、storeキャッシュの方法がsliceの範囲外だったため、それの備忘録
TL;DR
root reducerを設置して、storeにundefinedを設定する、
action.tsimport { createAction } from "@reduxjs/toolkit"; export const clearCacheBase = createAction('[CORE/STIORE] Clear Store Cache') export const clearCache = () => { return clearCacheBase() }reducer.tsximport { combineReducers, configureStore } from "@reduxjs/toolkit" import * as fromTodo from "./todo" import { clearCacheBase } from "./core/action" const combinedReducer = combineReducers({ [fromTodo.featureName]: fromTodo.reducer }) export const reducers = (state, action) => { if(action.type === clearCacheBase.toString()) { state = undefined // undefinedを追加するとstoreが初期値に全て戻る // return combinedReducer(undefined, action)でも動く こっちの方が再代入なく直感的かも } return combinedReducer(state, action) } export type RootReducer = typeof reducerscomponent.tsximport React from 'react'; import { useDispatch } from "react-redux" import { clearCache } from "../store/core/action" /* eslint-disable-next-line */ export interface ButtonProps {} export function Button(props: ButtonProps) { const dispatch = useDispatch() return ( <div> <button onClick={() => dispatch(clearCache())}>clear cache</button> </div> ); } export default Button;
- 投稿日:2021-02-20T15:50:40+09:00
領域に収まらない子要素をflexで折り返すには?
背景
バックエンドからデータを取得し、そのデータをもとにカードを並べたい。
しかし、通常並べるだけでは以下のように無限に横並びになってしまう。解決策
折り返したい子要素を包む親要素に以下のCSSを指定する。
display: flex; flex-wrapper: wrap;解決画面
以下の通り、親領域に収まらない時は折り返されるようになった。
参考コード
// 折り返したい子要素を包む親要素 const CardsWrapper = styled.div` display: flex; justify-content: center; margin-top: 12px; flex-wrap: wrap; // flexでwrap指定する。 `; // 折り返したい子要素 const CardHolder = styled.div` margin-right: 6px; `; const App = () => { // ... // 取得したデータ数だけカードをレンダリングする関数。 const alignCards = (data) => { return ( <CardsWrapper> {data.map((_data, i) => { return ( <CardHolder><SampleCard name={data[i].title}/> </ CardHolder> ) })} </CardsWrapper> ) } // ... return ( <> {alignCards(movieDataArraysObj)} </> ) } export default App;
- 投稿日:2021-02-20T15:50:40+09:00
領域に収まらない子要素をflexで折り返す
背景
バックエンドからデータを取得し、そのデータをもとにカードを並べたい。
しかし、通常並べるだけでは以下のように無限に横並びになってしまう。解決策
折り返したい子要素を包む親要素に以下のCSSを指定する。
display: flex; flex-wrapper: wrap;解決画面
以下の通り、親領域に収まらない時は折り返されるようになった。
参考コード
// 折り返したい子要素を包む親要素 const CardsWrapper = styled.div` display: flex; justify-content: center; margin-top: 12px; flex-wrap: wrap; // flexでwrap指定する。 `; // 折り返したい子要素 const CardHolder = styled.div` margin-right: 6px; `; const App = () => { // ... // 取得したデータ数だけカードをレンダリングする関数。 const alignCards = (data) => { return ( <CardsWrapper> {data.map((_data, i) => { return ( <CardHolder><SampleCard name={data[i].title}/> </ CardHolder> ) })} </CardsWrapper> ) } // ... return ( <> {alignCards(movieDataArraysObj)} </> ) } export default App;
- 投稿日:2021-02-20T15:34:59+09:00
React Typescriptでcontainer単体テストでつまったはなし
概要
React Typescript, jestで単体テストの簡単な作り方。
Tl;DR
redux-mock-storeでmock-storeを作成。
jest.requireaActualでProvider呼び出し、useDispacthとuseSelectorをmock化する。想定ファイル
todo.tsximport { createDraftSafeSelector } from '@reduxjs/toolkit'; import React from 'react'; import { selectAllTodo } from '../store/todo/selector'; import { useselector } from "react-redux" const containerSelector = createDraftSafeSelector( selectAllTodo, todos => ({ todos }) ) /* eslint-disable-next-line */ export interface TodoListContainerProps {} export function TodoListContainer(props: TodoListContainerProps) { const { todos } = useselector(containerSelector) return ( <div> {todos.map(t => (<div key={t.title}>{t.title}</div>))} </div> ); } export default TodoListContainer;テストファイル
todo.spec.tsximport React from 'react'; import { render } from '@testing-library/react'; import TodoListContainer from './todo-list-container'; describe('TodoListContainer', () => { it('should render successfully', () => { const { baseElement } = render(<TodoListContainer />); expect(baseElement).toBeTruthy(); }); });解決策として、redux-mock-storeとjestでモック化する
todo.spec.tsximport React from 'react'; import { cleanup, render } from '@testing-library/react'; import configureStore from "redux-mock-store" // reudux mock storeでデータを読み込む import TodoListContainer from './todo-list-container'; const mockStore = configureStore() // mockStore Functionを作成 // jest.mock('react-redux') で'react-redux'全体がMock化されるため、ProviderをActualで呼び出す。 const { Provider } = jest.requireActual("react-redux") // mock化されたdispatcherとselectorを作成 // selectorの実装を変えたときについては、jestのmock functionを書き換える。 jest.mock("react-redux", () => ({ useselector: jest.fn().mockReturnValue({ todos: [] }), useDispatch: jest.fn().mockReturnValue(jest.fn()) })) describe('TodoListContainer', () => { afterEach(() => { cleanup() }) it('should render successfully', () => { const { baseElement } = render( <Provider store={mockStore()}> <TodoListContainer /> </Provider> ); expect(baseElement).toBeTruthy(); }); });以上で、React Typescriptでcontainer単体テストをとりあえず実装できます。
Unit/Integrationを利用する場合は比較的つかえるかなとおもいますが、作り込む場合はjestの学習コストがあがるのできっと、人柱が必要になります。
- 投稿日:2021-02-20T14:48:54+09:00
React Material の初期開発で<Dialog>いっぱい書きたくない話
概要
dialogやsnackbarをmaterialで記載する時にcontextを利用すると再利用生があがるので、初期開発の速度が上がる可能性がある。
経緯
個人的にも昔のProjectで、[ダイアログ空いてる?、] = useState()てきなことを大量にかいていた戒め。
TL;DR
ReactのClasss Componentを利用してContext Providerを作成。
component側からは、DialogのReactElementを渡す。
ClassComponentを使わない場合については、hooksが利用できないので(hooks in hooksでエラー)死んだ話。流れ
- Reactのプロジェクトを作成
- Materialを追加
- Class Componentを利用してContextを作成
- Dialogの中身を作成(適当に)
- Functional Componen側からDialogContextを呼ぶ
1. Reactのプロジェクトを作成
Nxまたはcreate-react-appを利用してアプリケーションを作成
create-react-appを利用する場合
$ create-react-app my-appnxを利用する場合
terminal
$ npx create-nx-workspace@latest --preset=react
プロジェクトが作成されたら、Materialの追加
2. Materialを追加
$ yarn add @material-ui/coreここで下準備終了。
3. Class Componentを利用してContextを作成
stateにDialogに必要なopenのflag, elementを利用、
contextValueのcreate, closeのfuncitonを持たせる。
dialogにoptionを追加した場合は、default のoptionを内部で持ちcreateの時にoverrideするといろいろ楽に対応できます。contexts/dialog-context.tsximport { Dialog } from '@material-ui/core'; import React, { createContext } from 'react'; // ?はdefaultValueの時に渡せないため、?を利用 export interface DialogContextValue { createDialog?: (elm: JSX.Element) => void, closeDialog?: () => void, open: boolean } /* eslint-disable-next-line */ export interface DialogContexttProps {} interface DialogContextState { open: boolean, elm: JSX.Element } export const DialogContext = createContext<DialogContextValue>({ open: false }) export class DialogcontextProvider extends React.Component<DialogContexttProps, DialogContextState> { constructor(props) { super(props) this.state = { open: false, elm: null } } createDialog = (elm: JSX.Element) => { this.setState(state => ({ ...state, open: true, elm })) } closeDialog = () => { this.setState(state => ({ ...state, open: false, elm: null })) } render() { return ( <DialogContext.Provider value={{ open: this.state.open, createDialog: this.createDialog, closeDialog: this.closeDialog }}> { this.props.children } <Dialog open={this.state.open}> { this.state.elm } </Dialog> </DialogContext.Provider> ) } } export default DialogcontextProvider;次にcontextを利用するため、Appに追記します。
適宜利用してください。app.tsximport React from 'react'; import DialogcontextProvider from './contexts/dialog-contextt'; const App = () => ( <div> <DialogcontextProvider> <div>sample</div> </DialogcontextProvider> </div> ) export default App4. Dialogの中身を作成(適当に)
これは、適当でいいですがhooksを内部で利用するとclass Componentじゃないといけないことがわかると思います。
useForm, useDispatch, useSelectorなど結構メジャーどころを使ってcontextProvider ClassComponentじゃない時に、hook in hookのエラーが表示されます。components/dialog-content.tsximport React from 'react'; /* eslint-disable-next-line */ export interface DialogContentProps {} export function DialogContent(props: DialogContentProps) { return ( <div> <h1>Welcome to dialog-content!</h1> </div> ); } export default DialogContent;5. Functional Componen側からDialogContextを呼ぶ
最後に呼び出し元のcomponentを作成して、dialog呼べるか確認していきます。
components/button.tsximport { DialogContent } from './dialog-content'; import React, { useContext } from 'react'; import { DialogContext } from '../contexts/dialog-contextt'; /* eslint-disable-next-line */ export interface ButtonProps {} export function Button(props: ButtonProps) { const { createDialog } = useContext(DialogContext) return ( <div> <button onClick={() => createDialog(<DialogContent />)}>click to open</button> </div> ); } export default Button;app.tsxに呼び出し元を追加。
app.tsximport React from 'react'; import Button from './components/button'; import DialogcontextProvider from './contexts/dialog-contextt'; const App = () => ( <div> <DialogcontextProvider> <div>sample</div> <Button /> </DialogcontextProvider> </div> ) export default App表示されたbuttonをクリックした時にelementが表示されます。
地味に利用手段が増えるので、snackbarとかも、入れておくと、
Sample.tsxfunction Sample () { useEffect(() => { createSnackBar({ type: 'error', message: "Api Failっぽいよ" }) }, [errors]) return ( ) }の様にcomponent側から呼び出すことが可能になり、Componentは一つのため、開発速度は上がります。
- 投稿日:2021-02-20T13:32:10+09:00
React 17 で Recoil を使う
概要
まだ React 17 対応版の Recoil が npm に公開されていないため、
npm install recoil
で入れるとエラーが出ます。
Nightly Build をインストールすることで React 17 環境でもインストールできます。インストール
npm install https://github.com/facebookexperimental/Recoil.git#nightly --savehttps://recoiljs.org/docs/introduction/installation/#nightly-builds
- 投稿日:2021-02-20T12:37:39+09:00
simpackerでRails + React + Typescript環境構築
はじめに
simpackerとは
webpackerを使わず、シンプルなwebpackでRailsを開発するgem。
クックパッド技術部の方が作られたgemです。
https://github.com/hokaccha/simpacker
Simpacker: Rails と webpack をもっとシンプルにインテグレーションしたいのです
Githubと、このクックパッドの開発者ブログを見れば、simpackerが何なのかほとんどわかると思います。なぜsimpacker?
Rails6以降、webpackerが標準搭載になっていますが、webpackerは独自のDSLになっており、webpack本来の設定などが見えなくなっています。簡単にwebpackを扱えますが、設定のカスタマイズには独自DSLを学ぶコストがついてきます。またwebpackの細かい設定などは調整しにくかったりできないこともあるので、純粋なwebpackを使いたいならsimpackerの導入をおすすめします。
またwebpackerの独自DSLを学ぶなら、webpackそのものを学んだ方が今後の開発に活かせる気がします。私は初めて現場で触れたのがsimpackerの方で、webpackerのことはよくわかってなかったりします。笑Rails + React + TypeScriptで環境構築
バックエンドはRails、フロントエンドはReact + TypeScriptで構成された開発手順をメモしたいと思います。
開発環境
ruby 3.0.0
rails 6.1.1
yarn 1.22.10
tsc 4.1.3手順
グローバルを汚さないRails環境構築やMySQLのコンテナ化は別記事に解説してますので、よければ参考にしてください。
Dockerでコンテナ化したMySQLを使用してRails環境構築
Railsの環境構築(グローバル環境を汚さずに)railsプロジェクト立ち上げ
bundle exec rails new my_app --skip-javascript bundle exec rails db:createwebpackerを入れたくないので、
--skip-javascript
を付けます。simpackerをGemfileに追加
Gemfilegem 'simpacker'gemインストール
bundle installsimpacker初期化コマンド実行
bundle exec rails simpacker:installReact, TypeScript、必要なパッケージをインストール
yarn add -D react react-dom yarn add -D typescript ts-loaderyarnを使っていますが、npmでも問題ありません。
ts-loaderはtypescriptをjavascriptにトランスパイルするためのパッケージです。webpack.config.jsを作成
ここにwebpackの設定を書いていきます。
純粋なwebpackと全く同じ書き方です。webpack.config.jsconst path = require("path"); const WebpackAssetsManifest = require("webpack-assets-manifest"); const { NODE_ENV } = process.env; const isProd = NODE_ENV === "production"; module.exports = { mode: isProd ? "production" : "development", devtool: "source-map", entry: { application: path.resolve(__dirname, "app/frontend/js/packs/application.tsx"), }, output: { path: path.resolve(__dirname, "public/packs"), publicPath: "/packs/", filename: isProd ? "[name]-[hash].js" : "[name].js", }, resolve: { extensions: [".js", ".ts", ".jsx", "tsx"], }, module: { rules: [ { test: /\.(js|ts|jsx|tsx)$/, exclude: /node_modules/, use: [ {loader: "ts-loader"} ] } ] }, plugins: [ new WebpackAssetsManifest({ publicPath: true, output: "manifest.json", }), ], };
const path = require("path");
node.jsのpathモジュールを読んでます。
const WebpackAssetsManifest = require("webpack-assets-manifest");
を生成してくれるパッケージです。
manifest.json
const { NODE_ENV } = process.env;
こちらで任意の環境変数を読み込んでいます。
mode:
modeによって出力ファイルの形式が変わります。productionだと圧縮され、develomentだとみやすく整形されて出力されます。
devtool:
ソースマップを指定できます。ソースマップを有効にすると、ブラウザコンソールでエラーを確認するときに、エラー箇所を特定できるため必須だと思います。
entry:
webpackに読み込ませるエントリポイントを指定します。
path.resolve(__dirname, "")
と言う表記は環境に依存しない絶対pathを取得できるっぽいです。
output:
ファイルに出力先を指定します。
publicPath
は本番環境での解決pathを指定しています。
railsではデフォルトでpublic配下がドキュメントルートなので、/packs/
を指定しています。
extensions:
importするファイルの拡張子を省略できます。同じファイル名の異なる拡張子ファイルが存在した場合、配列の先頭のものが読み込まれます。import File from '../path/to/file';
rules:
トランスパイルするローダーの設定を書きます。
test:
で対象ファイルを指定します。
use
で使用するローダーをしてします。複数書いた場合は後ろから実行されます。
WebpackAssetsManifest
:
manifest.jsonを作成するwebpack用ライブラリです。webpackの設定はやはり公式を参照すべきだと思います。
https://webpack.js.org/concepts/
こちらも充実しています。
webpack 4 入門 - Qiitatsconfig.jsonを作成
typescriptの設定を書いていきます。
tsconfig.json{ "compilerOptions": { "target": "es5", "module": "es2015", "jsx": "react", "allowJs": true, "moduleResolution": "node", "sourceMap": true, "strict": true, "noImplicitAny": false, }, "include": [ "app/frontend/js/**/*" ], "exclude": [ "**/*.(spec|test).ts", "**/setup.jest.ts", ] }
target:
出力するjsのバージョンを指定します。
module:
使用するモジュールを指定します。targetやmoduleについてまだ詳しく理解していないので、今後勉強していきたいと思ってます。
jsx:
reactを使用する場合、reactを指定します。
allowJs:
trueでjsファイルもトランスパイルしてくれます。
moduleResolution:
とりあえずnodeにしておけばいい?
strict:
全ての型チェックを有効にします。tsconfig.jsonについては、
公式にコンパクトにまとまっています。
https://typescript-jp.gitbook.io/deep-dive/project/compilation-context/tsconfig
日本語だと、こちらの記事でかなり詳細に解説してくださっています。
tsconfig.jsonの全オプションを理解する(随時追加中) - QiitaReactコンポーネントを作成
エントリーポイントのファイルを作成していきます。
app/frontend/js/packs/application.tsximport * as React from 'react'; import * as ReactDOM from 'react-dom'; import Index from '../pages/Index'; const appElement = document.getElementById('app'); if (appElement) { ReactDOM.render(( <Index /> ), appElement); }レンダーするコンポーネント
app/frontend/js/pages/Index.tsximport * as React from 'react'; interface Props {} const Index: React.FC<Props> = () => { return( <div>Hello React</div> ) }; export default Index;これをwebpackでビルドしてjsファイルを出力します
yarn webpack
/public/packs/
配下にapplication.js
,application.js.map
,manifest.json
が作成されていると思います。これらをRails側で読み込みましょう。
bundle exec rails g controller Topapp/controllers/top.rbclass TopController < ApplicationController def index; end endapp/views/top/index.html.erb<div id="app"></div>app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title>Sample</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> </head> <body> <%= yield %> <%= javascript_pack_tag 'application' %> </body> </html>simpackerが良い感じに
<%= javascript_pack_tag 'application' %>
を解釈して読み込んでくれます。
http://localhost:3000/index
にアクセスしてHello Reactが表示されれば成功です。終わりに
simpackerはシンプルにwebpackを扱えるようにしてくれます。
ただ、今のRailsのフロント周りのgemはwebpackerを前提に作られているものが多いので、そういうgemを利用する際には、かなり苦労することもありますので、少なからずデメリットも存在します。
- 投稿日:2021-02-20T03:20:26+09:00
GatsbyブログのSEO対策するべきこと一覧
はじめに
Gatsbyを用いて技術ブログを作ったのは良いものの、数ヶ月の間SEO対策はほったらかしにしていました。最近このブログに色々SEO対策を施したので、その方法を紹介したいと思います。
ちなみにこのブログのstarterはgatsy-starter-hello-worldです。
SEOとは
SEOとはSearch Engine Optimizationの略で日本語にすると「検索エンジン最適化」となります。GoogleやYahooなどの検索エンジンで記事が上位に表示されるようにする対策することをいいます。
Gatsbyは静的なHTMLサイトのSEO対策とは違ったアプローチが必要です。それ故に「SPAはSEOに弱い」などと言われていたのですが、GatsbyのSEO系のプラグインは充実していて、それらを使えば簡単に実装することができます。
Sitemapの生成
Sitemapは検索エンジンのクローラーがサイト構造を読み取るのを助けます。
プラグインにgatsby-plugin-sitemapが用意されているので、これをインストールします。
$ npm i gatsby-plugin-sitemap
gatsby-config.jsにてサイトのURLをなければ追加し、plugins配列に先ほどインストールしたものを追加します。
title=gastby-config.jssiteMetadata: { siteUrl: `https://www.example.com`, }, plugins: [`gatsby-plugin-sitemap`]きちんとSitemapが作成されたかどうかは
https://example.com/sitemap.xml
にアクセスすることで確認できます。またSitemapはSearch ConsoleでGoogleに提出しましょう。
headのmetaタグの設定
React-Helmetを用いて行います。Starterによってはもともと入っているものもあると思いますがなければインストールしてください。React-Helmetはhead内のmetaタグを設定するのに使います。
このブログの場合は
Head
というmeta情報を管理するコンポーネントを作成してそこで色々設定しています。今回はこのコンポーネントで
1. meta設定 2. Twitterカードの設定 3. 言語を日本語に設定を行ます。
完成したコードはこちらになります。下でさらに詳しい解説をします。
title=head.jsconst Head = ({ title, description, lang, meta }) => { const data = useStaticQuery(graphql` query { site { siteMetadata { title siteUrl description } } } `) return ( <Helmet htmlAttributes={{ lang, }} title={`${title} | ${data.site.siteMetadata.title}`} meta={[ { name: `description`, content: `${data.site.siteMetadata.description}`, }, { name: `twitter:card`, content: `summary`, }, { name: `twitter:creator`, content: data.site.siteMetadata.author, }, { property: `og:image`, content: `${data.site.siteMetadata.siteUrl}/images/tube.png`, }, { property: `og:title`, content: title, }, { property: `og:description`, content: `${data.site.siteMetadata.description}`, }, { property: `og:type`, content: `website`, }, { name: `thumbnail`, content: `${data.site.siteMetadata.siteUrl}/images/tube.png`, }, { name: `twitter:title`, content: title, }, { name: `twitter:description`, content: `${data.site.siteMetadata.description}`, }, { property: `og:type`, content: `website`, }, ]} /> ) }サイトの言語の設定
サイトの言語を日本語に設定します。Helmetに
htmlAttribute=lang
を設定して、defaultPropsをja
にします。
```js:title=head.js
//..省略
<Helmet
htmlAttributes={{
lang,
}}//..省略
Head.defaultProps = { lang: `ja`, meta: [], description: ``,}
```Metaの設定
autherやsiteTitleなどの情報は
gatsby-config.js
に入れておいて、そこから取り出せるようにしておきます。title=gatsby-config.jsmodule.exports = { siteMetadata: { title: "k-log", author: "Kebeb", description: `Kebebの技術ブログ。主にMERN stackの学習の記録`, siteUrl: `https://jujekebab.com/`, },
head
コンポーネントのgraphQLからアクセスして設定します。title=gatsby-config.js{ property: `og:description`, content: `${data.site.siteMetadata.description}`, },Twitterカードの設定
TwitterなどのSNSで共有した時に、いい感じのカードが表示されるようにします。
og:image
,twitter:title
,twitter:creator
,twitter:card
さえ設定しておけば最低限の見た目にはなります。作成したカードはTwitter Card Validatorを用いて下見することができます。
Urlの正規化
Canonical属性を指定します。これもプラグインで簡単に実装できます。
- gatsby-plugin-canonical-urls
npm i gatsby-plugin-canonical-urls
して、gatsby-config.jsに追加するだけです。オプションでサイトのURLを追加しましょう。title=gatsby-config.jsplugins: [ { resolve: `gatsby-plugin-canonical-urls`, options: { siteUrl: `https://jujekebab.com`, stripQueryString: true, }, }, ]robot.txt の設置
クローラーに読み取ってほしいページと読み取ってほしくないページを伝えます。
こちらもgatsby-plugin-robots-txtというプラグインが用意されているので、これを使います。
これもインストールして
gatsby-config.js
に追加するだけです。Google Search コンソールでの登録
Google Analyticsを登録しているなら、そこからGoogle search Consoleへリンクすると簡単だと思います。
LightHouseでも高得点
LighHouseでもSEOは100点でした。あとはコンテンツの質だけだ!
まとめ
こういう目に見えない細かい作業をすると、はてなブログなどの無料ブログサイトの有り難みがわかります。もしGatsbyを使っていなかったらと思うとどれだけの作業量になるのか…。
構造化データの追加だけはやっていなかったので次回はそれについて紹介したいと思います。
参考
WPからGatsbyへ移行時に気を付けたいSEO対策一覧と導入方法
- 投稿日:2021-02-20T01:38:53+09:00
Reactの重要キーワード
- 投稿日:2021-02-20T00:07:51+09:00
パイソンのイラスト生成WebアプリをReactで作った
パイソンについて
うすうす感づいているかもしれませんが、この記事にはPython言語の話は出てきません。それでもこうしてこの記事に出会えたのも何かのご縁、ちょっとだけでも覗いていっていただけませんか。
普段Python言語を使っているソフトウェアエンジニアの方でも本物のパイソンを見たり触ったりしたことのある人は少ないのではないでしょうか。今日はそんなパイソンの魅力だけでもおぼえて帰っていただければと思います。
パイソンは日本語でニシキヘビと呼ばれていますが日本の野生では生息していません。美しい模様と、いかつい顔立ちが特徴のヘビです。
毒を持たず、鳴き声やにおいもなく、アレルギー源となる体毛もなく、餌は週に1回、寿命も10〜20年と長く飼いやすいため、犬や猫を飼えない事情がある人でも飼育できるペットとして近年人気が出ています。
なかでもボールパイソンは性格もおとなしく、大きくなりすぎないためおすすめです。
モルフとは
模様や色で分類される品種をモルフと呼びます。ボールパイソンは非常に多彩なモルフが特徴で、希少で人気のあるモルフは高額で取引されます。飼育下でのヘビの繁殖はそれほど難しくなく、好みのモルフを掛け合わせてあらたなモルフを作出する愛好家も多いです。
そのためここ数年で膨大な種類のモルフが生み出されてきました。これらのモルフの特徴をおぼえるのが大変なので作ったのが、今回紹介するウェブアプリBall Python Virtual Morph Makerです。
Ball Python Virtual Morph Maker
https://smallpinkmouse.github.io/virtualmorph/
https://github.com/smallpinkmouse/virtualmorph
色や模様の特徴をGUIで設定することができます。望むパターンができたら、Renderingボタンを押すとトグロを巻いたイラストが生成されます。
フレームワークはReactを使用、グラフィックの描画はp5.js、スライダーはrc-slider、カラーピッカーはreact-colorを使っています。
設定したパラメータをjson形式でローカルに保存して、後から再現できるようにしています。
モルフの特徴
ノーマルモルフのボールパイソンは逆三角形の斑紋(Blotch)とその中の2つの点が特徴で、宇宙人の顔に見えるためAlien Headと呼ばれています。
モハベモルフの場合、斑紋の黄色味が強くなり中の点の数もひとつになります。Keyholeとも呼ばれます。
スパイダーモルフの場合は、斑紋が大きくなり隙間が蜘蛛の糸のように細くなります。中の点は消失します。
生成したイラスト
レンダリングしたイラストはこのようになります。
野生下ではニュースになるくらい希少なアルビノも、爬虫類ブリーダーの間では遺伝を管理されているため、入手しやすいポピュラーなモルフとなっています。黄色の色素は残るため体色は完全に白にはならず、また目は赤くなります。
パイドとかパイボールと呼ばれる、印刷ミスのように模様が白く抜けるモルフも人気があります。
スパイダーは前述のように蜘蛛の巣のような模様が特徴です。頭部や虹彩も少し色が抜けて、猫目になる個体が多いようです。
横の斑紋が完全に消失して、背中の線だけが高速道路のセンターラインのように残るフリーウェイという品種です。
最後に
ボールパイソンがいかに多彩で美しい模様を持つかが伝わったでしょうか。
苦手な人もいるかと思うので、実物の写真はここまで出しませんでしたが、もし興味がわいたなら世界最大のボールパイソン情報サイトWorld of Ball Pythonsを見てみてください。
http://www.worldofballpythons.com/最後に我が家で飼っているナムパイ君をごらんください。パステルクラウンというパステルとクラウン両方の特徴を引き継いだモルフになります。