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

【React】particles.js で動く背景を作る

概要 particles.jsを使うと、背景が動くちょっと面白いサイトを作ることができます。 公式デモサイトで、パラメーターをいじりながらアニメーションを試すことができます。 環境 React - 17.0.2 TypeScript - 4.4.2 react-particles-js - 3.5.3 npm - 7.23.0 インストール プロジェクトフォルダを作成して、Create React Appでプロジェクトを作成します。 cmd npx create-react-app . --template typescript react-particles-jsをインストールします。 cmd npm i react-particles-js react-particles-jsがReact(17.x)に対応していない場合、--forceオプションを付けてインストールできます。 実装 コーディング コーディングはとても簡単です。 スタイリングは、CSS-in-JSとしてemotionを使っています。 import React, { VFC } from 'react'; import Particles from 'react-particles-js'; import { IOptions, RecursivePartial } from 'tsparticles'; import { css } from '@emotion/css'; import ParticlesParams from '../assets/particles-default.json'; export const CustomParticles: VFC = () => { return ( <Particles className={styles.particles} params={ParticlesParams as RecursivePartial<IOptions>} /> ) } const styles = { particles: css` position: absolute; width: 100%; height: 100%; ` } 設定 paramsとして、インポートしたjsonファイルを指定しています。 このjsonファイルで、色やノードの大きさ、マウス操作時のアニメーションなどの設定を行います。 これらの設定は、公式デモで試しながら調整することができます。また、その設定をjsonファイルとしてエクスポートすることができます。 設定のテンプレート 公式デモにはいくつか設定のテンプレートが用意されています。 例えば、[Snow]を選択すると、雪が降っているようなアニメーションになります。 まとめ アニメーションはいざ実装しようとすると複雑になりがちなので、particles.jsのような簡単に使うことのできるパッケージは本当にありがたいです。 何より使ってて楽しいです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chakra UIテーマのカスタマイズ方法

Chakra UIとは Chakra UIとは、React用のコンポーネントライブラリです。 React用のコンポーネントライブラリの中で、多分一番使われていると思われるのがGoogoleが提唱する「Material Design」というガイドに沿って作成っされたMaterial-UIでは無いかと思います。 今回は、Chakra UIを利用させて頂きました。選んだ理由としては、Githubのスター数、コンポーネントの豊富さ、コンポーネントに手を入れなくても比較的そのままで使えるか、ドキュメントが整ってるかなどです。 しかし、いざChakra UIを導入して、テーマのを変更するのにかなり苦戦したので、同じ所でハマっている方がいれば参考にしていただければ幸いです。 themeファイルでテーマのをカスタマイズ Chakra UIはドキュメントがしっかりしているので、問題なく利用できると思いますが、コンポーネントをオーバーライドする場合の説明があまりなく、苦戦しました。 こちらのページにコンポーネントのオーバーライド方法が記載されてはいるのですが、ボタン以外のコンポーネントで期待通りに動かない結果になりました。 記載されているButtonコンポーネントを参考に、他コンポーネントも設定しましたが、全然適用されませんでした。調べていくとどうやら、コンポーネント事に微妙に記述の違いがある様です。 結果的に動いたファイルは下記 // theme.js import { extendTheme } from "@chakra-ui/react" const theme = extendTheme({ styles: { global: { body: { fontFamily: "'游ゴシック体', YuGothic, '游ゴシック', 'Yu Gothic', 'メイリオ', 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', sans-serif", fontSize: "0.8125rem", lineHeight: "1.5", }, }, }, components: { Heading: { baseStyle: { fontWeight: 'normal', }, sizes: { sm: { fontSize: '1.125fem', }, md: { fontSize: '1.375fem', }, lg: { fontSize: '1.625fem', }, xl: { fontSize: '1.875fem', }, }, }, Text: { sizes: { xs: { fontSize: '0.625rem', }, sm: { fontSize: '0.8125rem', }, md: { fontSize: '0.8125rem', }, lg: { fontSize: '0.625rem', }, xl: { fontSize: '1.125rem', }, }, }, Table: { baseStyle: { table: { color: 'gray.400', }, th: { fontWeight: 'normal', bgColor: 'gray.100', }, }, }, }, }) export default theme 上記のthemeファイルをappファイルに読み込めば、適用されました。 components→コンポーネント名→baseStyleで動くコンポーネントもあれば、components→コンポーネント名→baseStyle→要素名で動く場合もあり、そこが苦戦しました。 是非上記を参考にしていただければ幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

日本語版Redditを作ってみた

きっかけ 私はこれまでRedditを使いエンジニアリングや投資に関しての情報を得ていたが、主に米国で起きていることしか議論の対象になっていない。。日本語で”色んな人による議論”が起きている場所といえば5chだが、まとまってなく中傷的な発言が目立つ。Quoraは質問オンリー、Noteやブログは一方向の発進媒体で双方向ディスカッションができない。Twitterはタイムラインが流れるので過去の情報にアクセスしづらい。日本語でも投票制かつ多数の人が双方向コミュニケーションに参加でき、人々のマインドをコミュニティを通じ繋げれば良いなと思った。 TL;DR リリース版がキドクです。なんでも質問受け付けてます。 https://kidok.app 要件&スタック スタックを選ぶ前にいくつかの要件の洗出しと仮説を立ててみた コミュニティーを作ること、投稿できること、良いコメントを上位にあげること、中傷的なコメントを排除できることなど基本機能を実装する。 リリースまでのスピードを早くする。リリース後のバグ修正や改善などを早く行えるプロセスにする。 コードに対しテストを書く。動かない機能をたくさん作るより、必要で確実に動く機能をリリースする。 コードが増えるとメンテナンスも増えるのでSaaSで置き換えられるところはお金で買う。 コメント数が飛び上がる、アクセスが増えるなど、負荷がかかってもスケールができ、可用性を維持する。 クエリが今後変わるのでフレキシブルなモデルにする。クエリの柔軟性も重視する。 システムの状態の監視は常に行う。 固定費を抑える。ユーザーが増えればコストが上がっても問題なし。 セキュリティやデータロスが行われないよう重視する。 DBやフロントエンドなどデザインは後から変えれる。 フロントエンド フロントエンドは主にReactJS + TypeScript。Create React AppのTypeScriptテンプレートを使い、ejectするとwebpackのメンテナンスが増えるのでそれをしないように頑張る。 VueやAngularも使えたがReactJSが一番慣れているだけで、早く作れるのが分かっていたので選んだ。 TypeScriptを選んだ理由はランタイムエラーを防止するためでモデルを定義できるから。モデルが今後変わっていくので、その度にビルドでエラーを吐いて欲しい。(もちろんTypeScriptでanyなどは使わない。) その他使って良かったライブラリー react-query : 非同期処理(APIコールなど)をラップするHookで、キャッシュやステート管理を簡易に行ってくれる。 zustand : これはReduxをHook化したglobal store。Redux + sagaを使うよりコードがかなり短縮され(かつテストも書きやすい)、immmerjsが内部で使われるのでobjectはimmutableにされる。 react-testing-library : enzymeはハッキーな回避策が多すぎて苦痛した。react-testing-libraryはユーザーイベントなど問題なく動くのでスムーズにテストが書ける。 データベース データベースはMySQL/PostgreSQL、DynamoDB やFireStoreなどが使えたがMySQL/PostgreSQLは自ら立ち上げな上、データマイグレーションやスクリプトの管理が必要なので選ばなかった。AWS Aurora(SQLのクラウドマネージ版)で立ち上げてコストを抑えたり、[AWS]によってスケールを管理してもらうこともできるが、コラムのIndexの追加など時間かかるので現時点では見送りする。DynamoDBはクエリの柔軟性が無いので、FireStoreを選んだ。 DB セキュリティ ACID 可用性 スケール MySQL系 サーバーが必要でパスワード/VPCの設定が必要。 ACID 自己責任 自らShardingなどが必要。(Auroraだと自動) DynamoDB サーバーが必要でパスワード/VPCの設定が必要。 CD APIの可用性は自己責任。DynamoDBは99.99%のSLAが適用される。 Capacity UnitsをUIで増やせばスケール可能。 Firebase Firebase Auth + Firestore Securityで認証や権限などの設定が可能。 ACID 99.95%でGoogleが対応 自動でスケール DB クエリの柔軟性 モデルの柔軟性 コスト MySQL系 インデックスを追加すれば柔軟に対応。インデックスのスケール(write)は困難。 毎回スクリプトを作りその管理が必要。 ユーザーが少なくても固定費がかかる。ユーザーが増えれば安くなる。 DynamoDB PKとSKのみ。複数のコラムでイコールフィルタするのも不可。 NoSQLなのでPK/SK以外は簡単に変えれる。 データのRead/Writeで課金。安い。 Firebase クエリは柔軟で NoSQLなので柔軟。データモデルの変更はアプリ側で対応が必要。 無料枠は大きいが、それ以降はコスパ高め。データのRead/Writeで課金。 Firestoreの欠点は二つ: 範囲operatorを二つのコラムに対してクエリを実行できない。(ドキュメントには複合クエリにおいて、範囲(<、<=、>、>=)と不等値(!=、not-in)の比較は、すべて同一のフィールドに対するフィルタである必要がありますある。と書いてある) 例えば、.where('createdAt', '>=', oneWeekAgo).where('upvote', '>', 50)は使用不可。この場合は、.where('createdAt', '>=', oneWeekAgo).where('upvoteTier', 'in', ['high', 'med'])と指定する必要があり、サーバー側でupvoteTierを定期的にインデックスする必要がある。 Writeの制限が1ドキュメント1write/秒がsoft limitなので、ユーザーアクションが増えるとFirestoreから移行する必要がある。 サーバー サーバーはDigital Oceanを使う。FireStore DBを使うのでAPIの管理は必要なく、サーバー側アプリはCron系のプロセスとJSバンドルを配信するぐらい。Herokuなども検討したが使用量の予測ができないので固定費が安いDOにした。ドメイン、SSL、dockerをセットアップして設定完了。 スケールが必要になると、CloudFlareを挟んでグローバルキャッシュ対応とDDOS対策が完了。Rate limitなどで荒らしユーザーの対処法にもなる。 サーバーアプリ フロントエンドのバンドルをサーバーから配信するのみなのでDockerで実行できる。 サーバーアプリはCron系のプロセスを回していて、主にupvoteなどのデータのインデックスを行うためである。 このアプリはGo言語で作成する。 認証 認証はFirebase AuthenticationやAuth0などを検討した。 コスト 第三者ログイン ロックインリスク メンテナンス Firebase Auth 無料 対応 認証データ以外export可能 無し Auth0 ユーザー数に比例し、かなり高い 対応 認証データ以外export可能 無し 自作 データベースとAPIが必要 全て自分でセットアップ 無し バグや機能など開発が必要 メンテナンスの方がコストが高いので自前で作るのは辞め、Firebase Authenticationを選んだ。ロックインリスクがあるが徐々にユーザーを移行すれば回避できるので、あまり問題視していない。 デザイン デザインはミニマムのこれを使っている。tailwind、bootstrap、material-uiなども選択できたが、CSSとHTMLをいじるだけでデザインができちゃうのでメンテナンスが少ない。SASSを使い変数をいじれば色やフォントの設定も綺麗にできる。もちろんresponsive対応。 ロゴの色はデザイナーの友達にアドバイスをもらい、それを元に他にアウトソースした。 CI/CD 使い慣れていたTravisCIをセットアップしたが、無料枠を使い切ると「もうクレジット無料枠はチャージされないよー」と出てきたので、毎週無料クレジットが与えられるCircle CIを導入。キャッシュなどが簡易にできるのでビルド時間がかなり短縮される。Codecovも導入して、GitHubのプルリクに反映させてる。 プロジェクト管理 GitHub Issueでラベル(p0, p1, p2, p3)を優先度順に設定する。Issueを作る際はテンプレートを用意してちゃんと答える。これにより客観的に重要性を判断できる。 **これを完了できない場合のインパクト** **完了の合否基準** PRを上げる場合は以下のテンプレートを使い、人為的ミスを減らす。 **Issueへのリンク** - [ ] テストが書かれているか - [ ] データ移行などはあるか - [ ] Firestoreの移行は必要か モニタリング Sentry: アプリケーションエラーを通知してくれる。エラーの量が少なく、かっこいい事しなければ無料枠で収まる。 Mixpanel: ユーザーのイベント可視化ツール。お願いすれば最初の2年は無料。 UptimeRobot: URLエンドポイントを定期的に呼び、ダウンしたら通知してくれる。(無料) フィードバックはcrisp.chatが一番コスパが良いサポートツールだが、設定が少し大変。 ウェブの右下にこのボタンを設置して、ユーザーの声を意見を聞く。 完成品 完成品が以下の画像です。ここからアクセス可能です。 まとめ 今のテクノロジーとツールで早く進めることができたので数週間でリリースが完了した。 これからもユーザーのフィードバックを元に改善を行っていき、それに応じてスタックを見直す必要があれば行う。 今は開発スピードを重視できるセットアップができたので目的は達成したと考えている。 興味ある方、加わって行きたい方はコメントかTwitterでDMください。 応援したい方はウェブサイトで色々投稿するか、QiitaとTwitterのフォローお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

基礎から学ぶReact/React Hooks学習メモ 5-2 useEffect

React Hooksを基礎から理解する 5-2 useEffect 参考 - 基礎から学ぶReact/React Hooks useEffect useEffect()に渡された関数の実行タイミングを、コンポーネントのマウント後、アンマウント後、または更新後まで遅らせることができるフック。 副作用とは、Reactの管理外であるDOMを更新する処理やAPIとの非同期通信等のデータ処理、購読(subscription)の設定など、UI構築以外の処理を指す。 useEffect()により副作用の実行タイミングが遅延され「UI構築後に行われる処理」と考えていい。 useEffect()に渡されたコールバック関数は、描画完了のタイミングで実行されるので、正しく構築されたDOMを参照することができる。無限ループに陥ることなく、安全にDOMを参照できる。 インポート import React, {useEffect} from "react" 基本構文 第2引数を省略すると、コンポーネントがレンダリングされるたびに副作用関数が実行されるため、無限ループに陥りやすく、第2引数を省略することはほとんどない。 useEffect(副作用関数, [依存する変数の配列]) 初回レンダリング時のみ副作用関数を実行させる useEffect(() => console.log("副作用関数が実行されました"), []); 依存関数の要素が変化した場合のみ副作用関数を実行させる useEffect(() => document.title = `${userName} ${frameworkName}`, [userName, frameworkName]); クリーンアップ関数 useEffect()の副作用関数から返された関数を、クリーンアップ関数という。 クリーンアップとは、タイマーのキャンセル処理、イベントリスナの削除など。 レンダリングするたびにイベントが重複してしまうので、マウント時に実行した処理をアンマウント時に削除する処理が必要。 useEffect(() => { console.log("副作用関数が実行されました!"); return () => { alert("クリーンアップ関数が実行されました!"); } }, []) useEffectの利用例 クリーンアップを必要としないコード例 ボタンをクリックするとカウンターが加算、リセットされ、ブラウザのタイトルも同時に変更される import React, { useState, useEffect } from "react"; import "./styles.css"; const INITIAL_COUNT = 0; const SampleComponent = () => { const [count, setCount] = useState(INITIAL_COUNT); // countが変更されたときに、タイトルを変える useEffect(() => { document.title = `${count} 回クリックされました`; }, [count]); const countIncrement = () => { setCount((prevCount) => prevCount + 1); }; const countReset = () => { setCount(INITIAL_COUNT); }; return ( <div className="App"> <p>現在のカウント数:{count}</p> <button onClick={countIncrement}>+1ボタン</button> <button onClick={countReset}>リセット</button> </div> ); }; export default function App() { return <SampleComponent />; } クリーンアップを必要とするコード例 アンマウントまたは副作用が再実行されたときに、クリーンアップ関数が実行される 「タイマーを表示」ボタンをクリックするとタイマーが表示され、useEffect()の副作用関数が実行される 「タイマーを非表示」ボタンをクリックするとタイマーが非表示になり、useEffect()の副作用関数のクリーンアップ関数が実行される import React, { useState, useEffect } from "react"; import "./styles.css"; const INITIAL_COUNT = 0; const Timer = () => { const [count, setCount] = useState(INITIAL_COUNT); const countReset = () => { setCount(INITIAL_COUNT); }; const countIncrement = () => { setCount((prevCount) => prevCount + 1); }; // 副作用関数が初回レンダリング時に実行される useEffect(() => { console.log("副作用関数が実行されました!"); const timer = setInterval(countIncrement, 1000); return () => { // アンマウント時に実行される console.log("timerが削除されました!"); clearInterval(timer); }; }, []); return ( <div className="App"> <p>現在のカウント数:{count}</p> <button onClick={countReset}>RESET</button> </div> ); }; export default function App() { const [display, toggleDisplay] = useState(false); // ボタンによって、Timerコンポーネントの表示、非表示が行われる const handleToggleDisplay = () => { toggleDisplay(!display); }; return ( <> <button onClick={handleToggleDisplay}> {display ? "タイマーを非表示" : "タイマーを表示"} </button> {display && <Timer />} </> ); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apollo v3のページネーションについて

公式ドキュメント 何についての記事か? Aoikki v3でのページネーション(特に、カーソルページネーション)についての記事になります。 主に、公式ドキュメントだとわかりにくかった部分の補足を書きます。 基本的な部分については公式ドキュメントを参考にしてください。 v3と前バージョンの大きな違いは何か? updateQueryがdeprecatedになり、Cache APIにマージの処理を書くようになったことです。 より効率的にキャッシュを使うための施策のようです。 どのように書くか?(具体例) クエリ実行の時、今までは updateQuery でマージの処理を書いていましたが、その記述は不要になります。 代わりに、CacheAPIでマージ処理を書きます。 export const useFeedsQuery = (variables: FeedsQueryVariables) => { // クエリの実行 const { data, fetchMore, loading } = useQuery(FEED_QUERY, { variables: { id: "XXX" collectionInput: { limit: 10, cursor: "YYY", } },  fetchPolicy: 'cache-and-network',  nextFetchPolicy: 'cache-first', notifyOnNetworkStatusChange: true });  // 次ページ取得関数 const handleMore = useCallback(() => { fetchMore({ variables: { collectionInput: { ...variables.collectionInput, cursor: data?.feeds?.pageInfo.endCursor, }, }, }); }, [variables.collectionInput, data?.feeds?.pageInfo.endCursor, fetchMore]); return { feeds: data?.feeds?.edges || [], hasMore: data?.feeds?.pageInfo.hasNextPage || false, handleMore, loading, }; }; // CacheAPIのマージ部分 type FeedQueryResult = Pick<FeedQuery['feed'], 'pageInfo' | 'edges'>; type QueryResultItem = { id: string }; const cache = new InMemoryCache({ typePolicies: { Query: { fields: { feeds: { keyArgs: ['id'], merge( existing: FeedQueryResult, incoming: FeedQueryResult, { args, readField } ): FeedQueryResult { const merged = { pageInfo: incoming.pageInfo, edges: [...(existing?.edges ?? [])], }; let offset = offsetFromCursor(merged.edges, args?.collectionInput?.cursor, readField); if (offset < 0) offset = merged.edges.length; merged.edges = [...merged.edges.slice(0, offset), ...incoming?.edges]; return merged; }, } } } } }) const offsetFromCursor = (items: QueryResultItem[], cursor: string, readField: ReadFieldFunction): number => { if (!cursor) return 0; for (let i = items.length - 1; i >= 0; --i) { const item = items[i]; const id = readField('id', item); if (id === cursor) { return i + 1; } } return -1; }; 上記の書き方について以降で解説します。 ここで解説していない基本的な部分などについては、公式ドキュメントを参考にしてください。 具体例の解説(useQuery) fetchPolicy useQueryを実行したとき、データをどのように返すかを制御するものです。 次の種類があります。 fetchPolicy 説明 cache-first(デフォルトの動作) キャッシュがあればそれを返す、なければサーバーから取ってくる。 cache-and-network キャッシュがあればそれを返すが、同時にサーバーから取ってきてキャッシュを更新する。 network-only キャッシュは利用せず、サーバーから取ってきたものを返す(この場合もキャッシュは更新される)。 no-cache network-onlyのキャッシュを更新しない版。 cache-only cache-firstのサーバーからとってこない版(キャッシュになければ、エラーになる)。 standby nextFetchPolicy 指定できるものはfetchPolicyと同じです。 useQueryを実行した後、ネットワーク状態の更新などでコンポーネントも更新されることがあり、useQueryも複数回実行される可能性がありますが、この時の振る舞いを決めるものとなります(おそらく)。 何も設定しないと、fetchPolicyと同じ値になります。 notifyOnNetworkStatusChange これは通信状況の監視を行うかのフラグで、trueにしないとloadingは常にfalseになります。 具体例の解説(fetchMore) variables ここでのvariablesは変更になったものだけを書いてやれば良いです。 useQueryに渡したものとマージされます。 ただし、マージされるのは直下のみで、ネストしている場合は自分でマージする必要があります。 具体例の解説(CacheAPI) typePolicies GraphQLクエリタイプの_typenameの値がtype名やfield名になります。 例えば上記の具体例だと、以下のようなクエリタイプがある想定です。 // GraphQLクエリタイプ export type FeedsQuery = { __typename?: 'Query' } & { feeds: { __typename?: 'FeedPageInfo' } & { pageInfo: { __typename?: 'CollectionPageInfo' } & Pick< Types.CollectionPageInfo, 'startCursor' | 'endCursor' | 'hasNextPage' >; edges: Array<{ __typename?: 'Feed' } & FeedsFragment>; }; }; 上記の具体例は次のように分割することも可能です(この分割の仕方はあまり良くないですが)。 詳細は、こちらを確認してください。 export const newCache = (): InMemoryCache => { return new InMemoryCache({ typePolicies: { Query: { fields: { feeds: { keyArgs: false, merge: true, }, }, }, TopicPageInfo: { fields: { pageInfo: { merge(existing: CollectionInput, incoming: CollectionInput) { ... }, }, edges: { merge(existing: Array<FeedFragment>, incoming: Array<FeedFragment>) { ... }, }, }, }, } }); }; 分割する場合は単一要素(サービス内でユニークな要素)のみにした方が良さそうです。 上の例のように複数要素が入るようなfieldを分割すると、予期しないところからキャッシュが更新されてしまう可能性があります。 keyArgs args内のどのフィールド単位でキャッシュを分けるかを指定できます(複数指定可能)。 これを使わず、merge内でargsのフィールドを元にしたkeyを作りmapで管理する方法もあります(この場合、取り出す時にクエリのレスポンスの形にする必要があるのでreadの処理を書く必要があります)。 ただ、責任を分けるためにもkeyArgsを使うのが無難です。 // キャッシュを自分で分割する方法の例(具体例とは対応していません) feeds: { read(existing = {}, { args: { groupId, offset, limit }}) { return existing[groupId] && existing[groupId].slice(offset, offset + limit); }, merge(existing = {}, incoming, { args: { groupId }}) { const merged = existing[groupId] ? existing[groupId].slice(0) : []; for (let i = 0; i < incoming.length; ++i) { merged[offset + i] = incoming[i]; } existing[groupId] = merged; return existing; }, }, merge キャッシュのデータと新しくとってきたデータをどのようにマージするか指定します。 第三引数にどういう条件でクエリが呼ばれたかを示すargs、readFieldやobjectMergeといったヘルパー関数が含まれます。 次のように書くだけだとうまくいかないケースも多いので気をつけてください。 return [...existing, ...incoming]; read 具体例のように何も指定がない時はmergeの返り値をそのまま返します。 取り出すときに何か手を加えたいときはこれに処理を書きます(例えば、一部だけ返したいときなど)。 トラブルシューティング loadingを使った処理がうまく動かない [原因: notifyOnNetworkStatusChange: trueの設定がない] notifyOnNetworkStatusChange: true が useQueryのオプションに設定されていない可能性があります。 次ページを取得しようとすると無限ループになる [原因: nextFetchPolicyがnetwork系になっている] nextFetchPolicyがnetwork系になっていると、例えば次のようなことが発生します。 前提: 画面下に来ると次ページを取得する 1. fetchMoreを実行して次のページを取得する 2. キャッシュのリストが更新されるタイミングで再レンダリング、useQueryも実行されるが、その時もう一度最初のリクエスト(cursorなし)が飛ぶ 3. 1のレスポンスでリストを上書きするようになっている場合、画面下の状態になるので、再び1を実行する。 対応: nextFetchPolicyをcache-firstにする。 こうすると、loading状態が切り替わったタイミングでも、キャッシュ(最初のリクエスト実行後、結果が追加されたリスト)が返るようになり、画面下にならなくなるので無限ループは解消されます。 fetchPolicyもcache-firstにすると、リスト内容が更新されないので注意してください(fetchPolicyは、network-only か cache-and-network が良いと思います)。 mergeの第三引数のargsが取れない [原因: argsはuseQueryで直接実行したクエリタイプに対応するtypePolicyのmergeにしか値は入らない] fieldを分割していた場合、そちらのmerge(例えば、前述のTopicPageInfo内のmerge)のargsは常にnullになります。 データが重複して取れてしまう [原因: fetchPolicyにnetwork系を指定し、mergeでいつもexistingにincomingを追加している] fetchPolicyにnetwork系を指定していると、コンポーネントのマウント(ページ遷移時など)の際、キャッシュがあってもuseQueryに渡した条件でリクエストが飛ぶので、新しく取ってくるデータは最初のページになります。 mergeの処理がexistingにincomingを追加する、というような処理になっていると、最初のページが二重でキャッシュに乗ってしまいます。 対応: argsのcursor(idなど)と同じ値を持つ要素があった場合、それ以降の要素は一旦消してからincomingを追加する。 ページ遷移をしてもリストが更新されない [原因1: mergeでいつもexistingにincomingを追加するようになっている] こうすると、古いデータが上書きされずに残ってしまう。 対応: 特定の条件下ではexistingをincomingで上書きする。 [原因2: fetchPolicyがcache-firstになっている] cache-firstだとキャッシュがあればいつもそれを使うので、新しいデータがいつまで経っても取得されない。 対応: fetchPolicyをnetwork系にして、必要であればnextFetchPolicyをcache-firstにする。 Q&A useQueryとfetchMoreの違いは? useQueryはfetchPolicyに応じて値を返すのに対して、fetchMoreはリクエストを送り、その結果でキャッシュを更新する。 fetchPolicyとnextFetchPolicyの違いは? fetchPolicyはコンポーネントがマウントされて最初のuseQuery呼び出しで有効なのに対して、nextFetchPolicyは再レンダリングの時などにuseQueryが呼び出される時に有効となる(おそらく)。 mergeの第三引数のargsとvariablesの違いは? argsには実行したGraphQLクエリの引数が入ってきます。 フィールド名もクエリの引数名と同じになるので、variablesのフィールド名とは異なることに注意してください。 query Feeds( $feedsCollectionInput: CollectionInput! $groupId: String! ) { feeds(id: $groupId, collectionInput: $feedsCollectionInput) { pageInfo { startCursor endCursor hasNextPage } edges { ...Feed } } } 例えば、上のようなクエリだとargsは下のようになります。 // args { id: "XXX", collectionInput: { ... } } variablesは以下のようになるので、違いがわかると思います。 { feedsCollectionInput: { ... }, otherCollectionInput: { ... }, groupId: "XXX" } キャッシュはどのような形で保存されるか? データが取得されると、正規化してキャッシュに保存されますが、もしすでにキャッシュされている場合はマージされます。 一番小さく分割した時のオブジェクトは、__typename:idをkeyとして保存されますし、もしidフィールドがなければ、keyFieldsによって指定することもできます。 クエリのキャッシュがあることで、順番を保証することが可能になっています。 mergeなどで受け取るデータは参照を含んでいますが、readFieldによって値を取り出すことが可能です。 // キャッシュの保存例 { "ROOT_QUERY": { "__typename": "Query", "tasks": [{ "__ref": "Task:1" }, { "__ref": "Task:2" }] }, " Task:1": { "id": 1, "__typename": "Task", "title": "タスク1", "content": "タスク1の内容です" }, " Task:2": { "id": 2, "__typename": "Task", "title": "タスク2", "content": "タスク2の内容です" } } 参考: - https://zenn.dev/kazu777/articles/b64935ea7d6fee - https://www.apollographql.com/blog/apollo-client/caching/demystifying-cache-normalization/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material UI の PaletteColor の型をいい感じに拡張させる

半年前から React を触るようになり、Material UIも触るようになりました。 とても便利ですね! UIフレームワークはこれまで Vue で Vuetify をつかってきましたが、断然 Material UI のほうが拡張性があり、使いやすくて結構好きになりました。 ことの発端 デザイナーからの色指定で primary カラーのバリエーションに light main dark 以外の色を追加したいというユースケースが発生しました。仮に disabled とします。 具体的なコードを書くと、こんな感じにしたい。 <Box bgcolor="primary.disabled">not available</Box> JavaScript なら createTheme のところで下記のようにかけば良いのですが、 TypeScript の場合は ThemeOptions と型が合わずにエラーとなります。 theme.ts export const theme = createTheme({ // 略 palette: { primary: { light: '#ffe8d6', main: '#a5a58d', dark: '#6b705c', disabled: '#b7b7a4' // ↑ JS なら通るし Material UI の 色指定でアクセスできる // TS なら createTheme の時点でエラー。 // 乱暴に @ts-ignore すればとりあえず大丈夫だが // theme を import したときにメンバーに出てこないのはイケてない } } // 略 }); 上記のコメントでかいたとおり、 Material UI の theme 関係の恩恵が受けられませんが、メンバーを勝手に生やしてアクセスすること自体はできているにも関わらず、TSのコンパイルエラーになります。 なので、自前で型を変えちゃいます。 declare module を使って上書き Vue の 2系でよくやるやつですね(遠い目)。 いきなりですが、こんな感じの d.ts を書きました。 types/createPalette.d.ts import { Theme } from '@material-ui/core'; import { PaletteColor } from '@material-ui/core/styles/createPalette'; declare module '@material-ui/core/styles/createPalette' { interface CustomTheme extends Theme { palette: CustomPalette; } interface CustomPalette extends Palette { primary: CustomPaletteColor; } interface CustomPaletteColor extends PaletteColor { disabled?: string; } interface SimplePaletteColorOptions { disabled?: string; } } SimplePaletteColorOptions ここで background を指定することで、 createTheme の palette.primary 内に disabled が使えるようになりました。 CustomPaletteColor 一方、 SimplePaletteColorOptions だけを拡張しても、 theme を import してもメンバーに disabled が生えません。なので、 styled components で色を引っ張りたい場合に問題が出ます import React from 'react'; import { theme } from './theme.ts'; const hoge = (): JSX.Element => ( <p style={{ textDecoration: `underline overline ${theme.palette.primary.disabled}` // ~~~~~~~~ そんなものはない }}>hoge</p> ); export default hoge; なので、悩んだのですが、 既存の PaletteColor を拡張して使うことにします。 既に存在するメンバーを拡張させるのは d.ts を使ってもできないので、新しい interface を定義します。今回作成した CustomPaletteColor がそれです。 CustomPalette 上記と同じ理由です。今回は primary の PaletteColor だけを拡張させたいので、 primary だけ拡張した CustomPaletteColor を使いましたが、他の色 error とかも拡張させたい場合は、そちらも追記してください。 CustomTheme これも上記と同じ & 本丸です。これを新しい Theme の型として使いたいのですが、悩んだ結果、 theme に対して アサーションで読み替えてあげることにしました。 export const theme = createTheme({ /* 設定値 */ }) as CustomTheme; 実行結果 import React from 'react'; import { theme } from './theme.ts'; const hoge = (): JSX.Element => ( <p style={{ textDecoration: `underline overline ${theme.palette.primary.disabled}` // ↑メンバーが生えた }}>hoge</p> ); export default hoge; これでいけそうです もちろん、Material UIのコンポーネントでも指定できます。(こっちは文字列渡しなので型の恩恵は受けられませんが…) <Box bgcolor="primary.disabled">not available</Box> ところで 実は当初、型を眺めていたところ、色の指定で利用されている型 PaletteColorOptions では ColorPartial が union で指定されていて、普通に使えそうなんです。 // @material-ui/core/index.d.ts から抜粋 export interface Color { 50: string; 100: string; 200: string; 300: string; 400: string; 500: string; 600: string; 700: string; 800: string; 900: string; A100: string; A200: string; A400: string; A700: string; } // @material-ui/core/styles/createPalette.d.ts から抜粋 export type ColorPartial = Partial<Color>; export type PaletteColorOptions = SimplePaletteColorOptions | ColorPartial; export interface SimplePaletteColorOptions { light?: string; main: string; dark?: string; contrastText?: string; } ところが、実際に生える theme インスタンスの palette の中身の型は PaletteColor になっています。 // @material-ui/core/styles/createPalette.d.ts から抜粋 export interface PaletteColor { light: string; main: string; dark: string; contrastText: string; } 同じ問題に気づいた方がいらっしゃたようで、こちらの Issue でディスカッションとなっていました。 https://github.com/mui-org/material-ui/issues/20277 で、このIssueを読んで、今回の対処法に行き着いた次第です。 まとめ v5 ではもうちょっとかんたんに拡張できそうですね。 それでは。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む