React、Redux、Sanity.ioを使用したWebアプリの構築
公開: 2022-03-10デジタルプラットフォームの急速な進化により、Wordpressのような従来のCMSに深刻な制限が課せられました。 これらのプラットフォームは結合されており、柔軟性がなく、製品ではなくプロジェクトに重点を置いています。 ありがたいことに、これらの課題やその他多くの課題に取り組むために、いくつかのヘッドレスCMSが開発されました。
従来のCMSとは異なり、サービスとしてのソフトウェア(SaaS)として説明できるヘッドレスCMSは、Webサイト、モバイルアプリ、デジタルディスプレイなどの開発に使用できます。 それらは無制限のプラットフォームで使用できます。 プラットフォームに依存せず、開発者優先で、クロスプラットフォームサポートを提供するCMSを探している場合は、ヘッドレスCMSから遠くを探す必要はありません。
ヘッドレスCMSは、単にヘッドのないCMSです。 ここでのhead
はフロントエンドまたはプレゼンテーション層を指し、 body
はバックエンドまたはコンテンツリポジトリを指します。 これには多くの興味深い利点があります。 たとえば、開発者は自分で選択したフロントエンドを選択できます。また、必要に応じてプレゼンテーション層を設計することもできます。
ヘッドレスCMSはたくさんありますが、最も人気のあるものには、Strapi、Contentful、Contentstack、Sanity、Butter CMS、Prismic、Storyblok、Directusなどがあります。これらのヘッドレスCMSはAPIベースであり、それぞれに長所があります。 たとえば、Sanity、Strapi、Contentful、StoryblokなどのCMSは、小規模なプロジェクトでは無料です。
これらのヘッドレスCMSは、さまざまな技術スタックにも基づいています。 Sanity.ioはReact.jsに基づいていますが、StoryblokはVue.jsに基づいています。 React開発者として、これが私がすぐにSanityに興味を持った主な理由です。 ただし、ヘッドレスCMSであるため、これらの各プラットフォームは、Angular、Vue、Reactのいずれのフロントエンドにも接続できます。
これらのヘッドレスCMSにはそれぞれ、大幅な価格上昇を表す無料プランと有料プランの両方があります。 これらの有料プランはより多くの機能を提供しますが、中小規模のプロジェクトにはそれほど多くの費用をかけたくないでしょう。 Sanityは、従量課金制のオプションを導入することで、この問題を解決しようとしています。 これらのオプションを使用すると、使用した分だけ支払うことができ、価格の急上昇を回避できます。
私がSanity.ioを選んだもう1つの理由は、GROQ言語です。 私にとって、Sanityはこのツールを提供することで群衆から際立っています。 Graphical-Relational Object Queries(GROQ)は、開発時間を短縮し、必要なコンテンツを必要な形式で取得するのに役立ちます。また、開発者がコードを変更せずに新しいコンテンツモデルでドキュメントを作成するのに役立ちます。
さらに、開発者はGROQ言語に制約されません。 GraphQLまたは従来のaxios
を使用して、Reactアプリでfetch
してバックエンドにクエリを実行することもできます。 他のほとんどのヘッドレスCMSと同様に、Sanityには、プラットフォーム上で構築するための役立つヒントを含む包括的なドキュメントがあります。
注:この記事では、React、Redux、CSSの基本を理解している必要があります。
Sanity.io入門
マシンでSanityを使用するには、SanityCLIツールをインストールする必要があります。 これはプロジェクトにローカルにインストールできますが、将来のアプリケーションからアクセスできるように、グローバルにインストールすることをお勧めします。
これを行うには、端末で次のコマンドを入力します。
npm install -g @sanity/cli
上記のコマンドの-g
フラグは、グローバルインストールを有効にします。
次に、アプリケーションでSanityを初期化する必要があります。 これは別のプロジェクトとしてインストールできますが、通常はフロントエンドアプリ(この場合はReact)内にインストールすることをお勧めします。
彼女のブログで、KapeheはSanityをReactと統合する方法を詳細に説明しました。 このチュートリアルを続行する前に、記事を確認しておくと役に立ちます。
次のコマンドを入力して、ReactアプリでSanityを初期化します。
sanity init
Sanity CLIツールをインストールすると、 sanity
コマンドを使用できるようになります。 ターミナルでsanity
またはsanity help
と入力すると、使用可能なSanityコマンドのリストを表示できます。
プロジェクトを設定または初期化するときは、プロンプトに従ってカスタマイズする必要があります。 また、データセットを作成する必要があり、データが入力されたカスタムデータセットを選択することもできます。 このリスティングアプリでは、SanityのカスタムSF映画データセットを使用します。 これにより、自分でデータを入力する必要がなくなります。
データセットを表示および編集するには、ターミナルのSanityサブディレクトリにcd
して、sanitystartと入力しsanity start
。 これは通常https://localhost:3333/
で実行されます。 インターフェイスにアクセスするにはログインが必要な場合があります(プロジェクトの初期化時に使用したのと同じアカウントでログインするようにしてください)。 環境のスクリーンショットを以下に示します。
Sanity-React双方向コミュニケーション
SanityとReactは、完全に機能するアプリケーションのために相互に通信する必要があります。
SanityManagerでのCORSオリジン設定
まず、ReactアプリをSanityに接続します。 これを行うには、 https://manage.sanity.io/
//manage.sanity.io/にログインし、 Settings
]タブの[ API Settings
でCORS origins
を見つけます。 ここでは、フロントエンドのオリジンをSanityバックエンドにフックする必要があります。 Reactアプリはデフォルトでhttps://localhost:3000/
で実行されるため、CORSに追加する必要があります。
これを下の図に示します。
正気を反応につなぐ
Sanityは、作成するすべてのプロジェクトにproject ID
を関連付けます。 このIDは、フロントエンドアプリケーションに接続するときに必要です。 プロジェクトIDは、 SanityManagerで確認できます。
バックエンドは、sanityclientと呼ばれるライブラリを使用してReactと通信しsanity client
。 次のコマンドを入力して、このライブラリをSanityプロジェクトにインストールする必要があります。
npm install @sanity/client
プロジェクトのsrc
フォルダーにsanitySetup.js
ファイル(ファイル名は関係ありません)を作成し、次のReactコードを入力して、SanityとReact間の接続を設定します。
import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });
projectId
、 dataset name
、ブール値のuseCdn
を、 @sanity/client
からインポートされたsanityクライアントのインスタンスに渡しました。 これは魔法のように機能し、アプリをバックエンドに接続します。
双方向接続が完了したので、すぐにプロジェクトを構築しましょう。
Reduxをセットアップしてアプリに接続する
ReactアプリでReduxを操作するには、いくつかの依存関係が必要です。 React環境でターミナルを開き、次のbashコマンドを入力します。
npm install redux react-redux redux-thunk
Reduxは、ほとんどのフロントエンドフレームワークやReactなどのライブラリで使用できるグローバルな状態管理ライブラリです。 ただし、 ReduxストアとReactアプリケーション間の通信を可能にするには、中間ツールreact-redux
が必要です。 Reduxサンクは、Reduxからアクションオブジェクトの代わりに関数を返すのに役立ちます。
Reduxワークフロー全体を1つのファイルに書き込むこともできますが、多くの場合、懸念事項を分離する方が適切です。 このために、ワークフローをactions
、 reducers
、 store
の3つのファイルに分割します。 ただし、 action types
を保存するための別のファイル( constants
とも呼ばれます)も必要です。
ストアの設定
ストアはReduxで最も重要なファイルです。 状態を整理してパッケージ化し、Reactアプリケーションに送信します。
Reduxワークフローを接続するために必要なReduxストアの初期設定は次のとおりです。
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );
このファイルのcreateStore
関数は、 reducer
(必須)、初期状態、エンハンサー(通常はミドルウェア、この場合はapplyMiddleware
を介して提供されるthunk
)の3つのパラメーターを取ります。 レデューサーはreducers
フォルダーに保存され、それらを組み合わせて、 reducers
フォルダーのindex.js
ファイルにエクスポートします。 これは、上記のコードでインポートしたファイルです。 このファイルは後で再検討します。
SanityのGROQ言語の紹介
Sanityは、GROQを導入することで、JSONデータのクエリをさらに一歩進めます。 GROQは、Graph-RelationalObjectQueriesの略です。 Sanity.ioによると、GROQは、主にスキーマのないJSONドキュメントのコレクションをクエリするように設計された宣言型クエリ言語です。
Sanityは、開発者が言語に慣れるためのGROQPlaygroundも提供しています。 ただし、遊び場にアクセスするには、 sanityvisionをインストールする必要があります。 ターミナルでsanity install @sanity/vision
を実行してインストールします。
GROQの構文はGraphQLに似ていますが、より凝縮されて読みやすくなっています。 さらに、GraphQLとは異なり、GROQを使用してJSONデータをクエリできます。
たとえば、ムービードキュメント内のすべてのアイテムを取得するには、次のGROQ構文を使用します。
*[_type == "movie"]
ただし、ムービードキュメントの_ids
とcrewMembers
のみを取得する場合。 これらのフィールドを次のように指定する必要があります。
`*[_type == 'movie']{ _id, crewMembers }
ここでは、 *
を使用して、 _type
ムービーのすべてのドキュメントが必要であることをGROQに通知しました。 _type
は、ムービーコレクションの下の属性です。 次のように、 _id
およびcrewMembers
を実行したようにタイプを返すこともできます。
*[_type == 'movie']{ _id, _type, crewMembers }
ReduxアクションにGROQを実装することで、GROQの詳細に取り組みますが、GROQのSanity.ioのドキュメントで、GROQの詳細を確認できます。 GROQクエリのチートシートには、クエリ言語を習得するのに役立つ多くの例が記載されています。
定数の設定
Reduxワークフローのすべての段階でアクションタイプを追跡するための定数が必要です。 定数は、各時点でディスパッチされるアクションのタイプを判別するのに役立ちます。 たとえば、APIが読み込まれているとき、完全に読み込まれているとき、エラーが発生したときを追跡できます。
必ずしも別のファイルで定数を定義する必要はありませんが、単純さと明確さのために、これは通常、Reduxのベストプラクティスです。
慣例により、Javascriptの定数は大文字で定義されます。 ここでは、定数を定義するためのベストプラクティスに従います。 これは、動画のフェッチを移動するリクエストを示す定数の例です。
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";
ここでは、 MOVIE_FETCH_REQUEST
のアクションタイプを示す定数MOVIE_FETCH_REQUEST
を作成しました。 これにより、 strings
を使用せずにこのアクションタイプを簡単に呼び出して、バグを回避できます。 また、定数をエクスポートして、プロジェクトのどこでも使用できるようにしました。
同様に、リクエストが成功または失敗したことを示すアクションタイプをフェッチするための他の定数を作成できます。 movieConstants.js
の完全なコードは、以下のコードに示されています。
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";
ここでは、映画または映画のリストを取得し、最も人気のある映画を並べ替えて取得するためのいくつかの定数を定義しました。 リクエストがいつloading
され、 successful
、 failed
たかを判断するために定数を設定していることに注意してください。
同様に、 personConstants.js
ファイルを以下に示します。
export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST"; export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS"; export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL"; export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST"; export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS"; export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL"; export const PERSONS_COUNT = "PERSONS_COUNT";
movieConstants.js
と同様に、1人または複数の人をフェッチするための定数のリストを設定します。 また、人を数えるための定数を設定します。 定数は、 movieConstants.js
で説明されている規則に従い、アプリケーションの他の部分からアクセスできるようにエクスポートしました。
最後に、アプリにライトモードとダークモードを実装するため、別の定数ファイルglobalConstants.js
があります。 それを見てみましょう。
export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";
ここでは、ライトモードまたはダークモードがいつディスパッチされるかを決定する定数を設定します。 SET_LIGHT_THEME
は、ユーザーが明るいテーマに切り替えるタイミングを決定し、 SET_DARK_THEME
は、暗いテーマが選択されるタイミングを決定します。 図のように定数もエクスポートしました。
アクションの設定
慣例により、アクションは別のフォルダーに保存されます。 アクションは、タイプに従ってグループ化されます。 たとえば、映画のアクションはmovieActions.js
に保存され、個人のアクションはpersonActions.js
ファイルに保存されます。
また、テーマをライトモードからダークモードに切り替えるためのglobalActions.js
もあります。
moviesActions.js
内のすべての映画を取得しましょう。
import sanityAPI from "../../sanitySetup"; import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster": poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } };
ReactをSanityバックエンドに接続するためにsanitySetup.js
ファイルを作成したときのことを覚えていますか? ここでは、GROQを使用して健全性バックエンドをクエリできるようにセットアップをインポートしました。 また、 constants
フォルダーのmovieConstants.js
ファイルからエクスポートされたいくつかの定数をインポートしました。
次に、コレクション内のすべての映画をフェッチするためのfetchAllMovies
アクション関数を作成しました。 ほとんどの従来のReactアプリケーションは、 axios
またはfetch
を使用してバックエンドからデータをフェッチします。 ここではこれらのいずれかを使用できますが、SanityのGROQ
を使用しています。 GROQ
モードに入るには、上記のコードに示すようにsanityAPI.fetch()
関数を呼び出す必要があります。 ここで、 sanityAPI
は、前に設定したReact-Sanity接続です。 これはPromise
を返すため、非同期で呼び出す必要があります。 ここではasync-await
構文を使用しましたが、 .then
構文を使用することもできます。
アプリケーションでthunk
を使用しているため、アクションオブジェクトの代わりに関数を返すことができます。 ただし、returnステートメントを1行で渡すことを選択しました。
const fetchAllMovies = () => async (dispatch) => { ... }
このように関数を書くこともできることに注意してください。
const fetchAllMovies = () => { return async (dispatch)=>{ ... } }
一般に、すべての映画をフェッチするために、最初に、リクエストがまだロードされているときに追跡するアクションタイプをディスパッチしました。 次に、SanityのGROQ構文を使用して、映画ドキュメントを非同期的にクエリしました。 映画データの_id
とposterurlを取得しました。 次に、APIから取得したデータを含むペイロードを返しました。
同様に、 _id
で映画を取得し、映画を並べ替えて、最も人気のある映画を取得できます。
特定の人物の参照に一致する映画を取得することもできます。 これは、 fetchMoviesByRef
関数で行いました。
const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title } ` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } };
この関数は引数を取り、 person._ref
またはcrewMembers
のcastMembers
が渡された引数と一致するかどうかをチェックします。 映画の_id
、 poster url
、 title
一緒に返します。 また、タイプMOVIES_REF_FETCH_SUCCESS
のアクションをディスパッチして、返されたデータのペイロードを添付します。エラーが発生した場合は、 try-catch
ラッパーのおかげで、タイプMOVIE_REF_FETCH_FAIL
のアクションをディスパッチして、エラーメッセージのペイロードを添付します。
fetchMovieById
関数では、 GROQ
を使用して、関数に渡された特定のid
に一致する映画を取得しました。
関数のGROQ
構文を以下に示します。
const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` );
fetchAllMovies
アクションと同様に、最初はmovie
タイプのすべてのドキュメントを選択しましたが、さらに関数に指定されたIDを持つドキュメントのみを選択しました。 映画の詳細をたくさん表示するつもりなので、取得する属性の束を指定しました。
映画id
と、 castMembers
配列内のいくつかの属性、つまりref
、 characterName
、人物の名前、および人物の画像を取得しました。 また、エイリアスをcastMembers
からcast
に変更しました。
castMembers
と同様に、 crewMembers
配列からいくつかの属性、つまりref
、 department
、 job
、人物の名前、人物の画像を選択しました。 また、エイリアスをcrewMembers
からcrew
に変更しました。
同様に、概要テキスト、人気、映画のポスターURL、映画のリリース日、タイトルを選択しました。
SanityのGROQ言語を使用すると、ドキュメントを並べ替えることもできます。 アイテムを並べ替えるには、パイプ演算子の横に注文を渡します。
たとえば、映画をreleaseDate
で昇順で並べ替える場合は、次のようにすることができます。
const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );
この概念をsortMoviesBy
関数で使用して、昇順または降順で並べ替えました。
以下のこの関数を見てみましょう。
const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } };
リクエストがいつ読み込まれるかを判断するために、タイプMOVIES_SORT_REQUEST
のアクションをディスパッチすることから始めました。 次に、 GROQ
構文を使用して、 movie
コレクションからデータを並べ替えてフェッチしました。 並べ替える項目は変数item
で提供され、並べ替えのモード(昇順または降順)は変数type
で提供されます。 その結果、 id
、poster url、およびtitleを返しました。 データが返されると、タイプMOVIES_SORT_SUCCESS
のアクションをディスパッチし、失敗した場合は、タイプMOVIES_SORT_FAIL
のアクションをディスパッチします。
同様のGROQ
の概念がgetMostPopular
関数に適用されます。 GROQ
の構文を以下に示します。
const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` );
ここでの唯一の違いは、人気順に映画を並べ替えてから、最初の3つだけを選択したことです。 アイテムはゼロベースのインデックスで返されるため、最初の3つのアイテムはアイテム0、1、2です。最初の10個のアイテムを取得する場合は、 [0..9]
を関数に渡すことができます。
これは、 movieActions.js
ファイル内のムービーアクションの完全なコードです。
import sanityAPI from "../../sanitySetup"; import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL, MOVIES_REF_FETCH_REQUEST } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } }; const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title }` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } }; const fetchMovieById = (id) => async (dispatch) => { try { dispatch({ type: MOVIE_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` ); dispatch({ type: MOVIE_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIE_FETCH_FAIL, payload: error.message }); } }; const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_MOST_POPULAR_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } }; const getMostPopular = () => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` ); dispatch({ type: MOVIES_MOST_POPULAR_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_MOST_POPULAR_FAIL, payload: error.message }); } }; export { fetchAllMovies, fetchMovieById, sortMoviesBy, getMostPopular, fetchMoviesByRef };
レデューサーの設定
レデューサーはReduxの最も重要な概念の1つです。 それらは前の状態を取り、状態の変化を決定します。
通常、switchステートメントを使用して、各アクションタイプの条件を実行します。 たとえば、アクションタイプが読み込みを示している場合はloading
を返し、成功またはエラーを示している場合はペイロードを返すことができます。 initial state
とaction
を引数として取ることが期待されます。
movieReducers.js
ファイルには、 movieActions.js
ファイルで定義されたアクションに一致するさまざまなレデューサーが含まれています。 ただし、各レデューサーの構文と構造は似ています。 唯一の違いは、それらが呼び出すconstants
とそれらが返す値です。
まず、 movieReducers.js
ファイルのfetchAllMoviesReducer
を見てみましょう。
import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } };
すべてのレデューサーと同様に、 fetchAllMoviesReducer
は、初期状態オブジェクト( state
)とaction
オブジェクトを引数として受け取ります。 switchステートメントを使用して、各時点でのアクションタイプを確認しました。 MOVIES_FETCH_REQUEST
に対応する場合、ユーザーに読み込みインジケーターを表示できるように、loadingをtrueとして返します。
MOVIES_FETCH_SUCCESS
に対応する場合は、読み込みインジケーターをオフにしてから、アクションペイロードを可変movies
で返します。 ただし、 MOVIES_FETCH_FAIL
の場合は、読み込みをオフにしてからエラーを返します。 また、ムービーをリセットするオプションも必要です。 これにより、必要なときに状態をクリアできます。
他のレデューサーについても同じ構造です。 完全なmovieReducers.js
を以下に示します。
import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_FETCH_RESET, MOVIES_REF_FETCH_REQUEST, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } }; const fetchMoviesByRefReducer = (state = {}, action) => { switch (action.type) { case MOVIES_REF_FETCH_REQUEST: return { loading: true }; case MOVIES_REF_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_REF_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const fetchMovieByIdReducer = (state = {}, action) => { switch (action.type) { case MOVIE_FETCH_REQUEST: return { loading: true }; case MOVIE_FETCH_SUCCESS: return { loading: false, movie: action.payload }; case MOVIE_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const sortMoviesByReducer = (state = {}, action) => { switch (action.type) { case MOVIES_SORT_REQUEST: return { loading: true }; case MOVIES_SORT_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_SORT_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const getMostPopularReducer = (state = {}, action) => { switch (action.type) { case MOVIES_MOST_POPULAR_REQUEST: return { loading: true }; case MOVIES_MOST_POPULAR_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_MOST_POPULAR_FAIL: return { loading: false, error: action.payload }; default: return state; } }; export { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer };
また、 personReducers.js
についてもまったく同じ構造に従いました。 たとえば、 fetchAllPersonsReducer
関数は、データベース内のすべての人をフェッチするための状態を定義します。
これは以下のコードで与えられます。
import { PERSONS_FETCH_FAIL, PERSONS_FETCH_REQUEST, PERSONS_FETCH_SUCCESS, } from "../constants/personConstants"; const fetchAllPersonsReducer = (state = {}, action) => { switch (action.type) { case PERSONS_FETCH_REQUEST: return { loading: true }; case PERSONS_FETCH_SUCCESS: return { loading: false, persons: action.payload }; case PERSONS_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } };
fetchAllMoviesReducer
と同様に、 state
とaction
を引数としてfetchAllPersonsReducer
を定義しました。 これらはReduxレデューサーの標準設定です。 次に、switchステートメントを使用してアクションタイプをチェックし、タイプがPERSONS_FETCH_REQUEST
の場合、loadingをtrueとして返します。 PERSONS_FETCH_SUCCESS
の場合は、読み込みをオフにしてペイロードを返し、 PERSONS_FETCH_FAIL
の場合は、エラーを返します。
レデューサーの組み合わせ
ReduxのcombineReducers
関数を使用すると、複数のレデューサーを組み合わせてストアに渡すことができます。 映画と人物のレデューサーを、 reducers
フォルダー内のindex.js
ファイルに結合します。
それを見てみましょう。
import { combineReducers } from "redux"; import { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer } from "./movieReducers"; import { fetchAllPersonsReducer, fetchPersonByIdReducer, countPersonsReducer } from "./personReducers"; import { toggleTheme } from "./globalReducers"; export default combineReducers({ fetchAllMoviesReducer, fetchMovieByIdReducer, fetchAllPersonsReducer, fetchPersonByIdReducer, sortMoviesByReducer, getMostPopularReducer, countPersonsReducer, fetchMoviesByRefReducer, toggleTheme });
ここでは、映画、人物、グローバルレデューサーファイルからすべてのレデューサーをインポートし、 combineReducers
関数に渡しました。 combineReducers
関数は、すべてのレデューサーを渡すことを可能にするオブジェクトを取ります。 プロセスの引数にエイリアスを追加することもできます。
後でglobalReducers
に取り組みます。
これで、 store.js
ファイルでレデューサーを渡すことができます。 これを以下に示します。
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));
Reduxワークフローを設定したら、Reactアプリケーションを設定しましょう。
Reactアプリケーションのセットアップ
私たちのreactアプリケーションは、映画とそれに対応するキャストとクルーメンバーを一覧表示します。 ルーティングにreact-router-dom
を使用し、アプリのスタイリングにはstyled-components
を使用します。 アイコンと一部のUIコンポーネントにもマテリアルUIを使用します。
次のbash
コマンドを入力して、依存関係をインストールします。
npm install react-router-dom @material-ui/core @material-ui/icons query-string
これが私たちが構築するものです:
ReduxをReactアプリに接続する
React-redux
には、アプリケーションをReduxストアに接続できるプロバイダー機能が付属しています。 これを行うには、ストアのインスタンスをプロバイダーに渡す必要があります。 これは、 index.js
ファイルまたはApp.js
ファイルのいずれかで実行できます。
これがindex.jsファイルです。
import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
ここでは、 react-redux
からProvider
をインポートし、Redux store
からstoreをインポートしました。 次に、コンポーネントツリー全体をプロバイダーでラップし、ストアをプロバイダーに渡します。
次に、Reactアプリケーションでルーティングするためreact-router-dom
が必要です。 react-router-dom
には、パスとルートを定義するために使用できるBrowserRouter
、 Switch
、およびRoute
が付属しています。
これは、 App.js
ファイルで行います。 これを以下に示します。
import React from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import MoviesList from "./pages/MoviesListPage"; import PersonsList from "./pages/PersonsListPage"; function App() { return ( <Router> <main className="contentwrap"> <Header /> <Switch> <Route path="/persons/"> <PersonsList /> </Route> <Route path="/" exact> <MoviesList /> </Route> </Switch> </main> <Footer /> </Router> ); } export default App;
これは、react-router-domを使用したルーティングの標準設定です。 あなたは彼らのドキュメントでそれをチェックすることができます。 コンポーネントHeader
、 Footer
、 PersonsList
、 MovieList
をインポートしました。 次に、すべてをRouter
とSwitch
でラップして、 react-router-dom
を設定します。
ページで同じヘッダーとフッターを共有する必要があるため、構造体をSwitch
でラップする前に、 <Header />
>コンポーネントと<Footer />
コンポーネントを渡す必要がありました。 アプリケーション全体をラップしたいので、 main
要素でも同様のことを行いました。
各コンポーネントを、 react-router-dom
からのRoute
を使用してルートに渡しました。
ページとコンポーネントの定義
私たちのアプリケーションは構造化された方法で編成されています。 再利用可能なコンポーネントはcomponents
フォルダーに保存され、Pagesはpages
フォルダーに保存されます。
私たちのpages
は、 movieListPage.js
、 moviePage.js
、 PersonListPage.js
、 PersonPage.js
で構成されています。 MovieListPage.js
は、Sanity.ioバックエンド内のすべての映画と最も人気のある映画を一覧表示します。
すべての映画を一覧表示するには、 movieAction.js
ファイルで定義されているfetchAllMovies
アクションをdispatch
するだけです。 ページが読み込まれるとすぐにリストをフェッチする必要があるため、 useEffect
でリストを定義する必要があります。 これを以下に示します。
import React, { useEffect } from "react"; import { fetchAllMovies } from "../redux/actions/movieActions"; import { useDispatch, useSelector } from "react-redux"; const MoviesListPage = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchAllMovies()); }, [dispatch]); const { loading, error, movies } = useSelector( (state) => state.fetchAllMoviesReducer ); return ( ... ) }; export default MoviesListPage;
useDispatch
とuseSelector
フックのおかげで、Reduxアクションをディスパッチし、Reduxストアから適切な状態を選択できます。 loading
、 error
、 movies
の状態はレデューサー関数で定義されており、ここではReactReduxのuseSelector
フックを使用してそれらを選択していることに注意してください。 これらの状態、つまりloading
、 error
、およびmovies
は、 fetchAllMovies()
アクションをディスパッチするとすぐに使用可能になります。
映画のリストを取得したら、 map
機能を使用して、または任意の方法でアプリケーションに表示できます。
これがmoviesListPage.js
ファイルの完全なコードです。
import React, {useState, useEffect} from 'react' import {fetchAllMovies, getMostPopular, sortMoviesBy} from "../redux/actions/movieActions" import {useDispatch, useSelector} from "react-redux" import Loader from "../components/BackdropLoader" import {MovieListContainer} from "../styles/MovieStyles.js" import SortIcon from '@material-ui/icons/Sort'; import SortModal from "../components/Modal" import {useLocation, Link} from "react-router-dom" import queryString from "query-string" import {MOVIES_FETCH_RESET} from "../redux/constants/movieConstants" const MoviesListPage = () => { const location = useLocation() const dispatch = useDispatch() const [openSort, setOpenSort] = useState(false) useEffect(()=>{ dispatch(getMostPopular()) const {order, type} = queryString.parse(location.search) if(order && type){ dispatch({ type: MOVIES_FETCH_RESET }) dispatch(sortMoviesBy(order, type)) }else{ dispatch(fetchAllMovies()) } }, [dispatch, location.search]) const {loading: popularLoading, error: popularError, movies: popularMovies } = useSelector(state => state.getMostPopularReducer) const { loading: moviesLoading, error: moviesError, movies } = useSelector(state => state.fetchAllMoviesReducer) const { loading: sortLoading, error: sortError, movies: sortMovies } = useSelector(state => state.sortMoviesByReducer) return ( <MovieListContainer> <div className="mostpopular"> { popularLoading ? <Loader /> : popularError ? popularError : popularMovies && popularMovies.map(movie => ( <Link to={`/movie?id=${movie._id}`} className="popular" key={movie._id} style={{backgroundImage: `url(${movie.poster})`}}> <div className="content"> <h2>{movie.title}</h2> <p>{movie.overview.text.substring(0, 50)}…</p> </div> </Link> )) } </div> <div className="moviespanel"> <div className="top"> <h2>All Movies</h2> <SortIcon onClick={()=> setOpenSort(true)} /> </div> <div className="movieslist"> { moviesLoading ? <Loader /> : moviesError ? moviesError : movies && movies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) } { ( sortLoading ? !movies && <Loader /> : sortError ? sortError : sortMovies && sortMovies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) ) } </div> </div> <SortModal open={openSort} setOpen={setOpenSort} /> </MovieListContainer> ) } export default MoviesListPage
useEffect
フックでgetMostPopular
映画アクション(このアクションは最も人気のある映画を選択します)をディスパッチすることから始めました。 これにより、ページが読み込まれるとすぐに最も人気のある映画を取得できます。 さらに、ユーザーがreleaseDate
日とpopularity
で映画を並べ替えることができるようにしました。 これは、上記のコードでディスパッチされたsortMoviesBy
アクションによって処理されます。 さらに、クエリパラメータに応じてfetchAllMovies
をディスパッチしました。
また、 useSelector
フックを使用して、これらの各アクションに対応するレデューサーを選択しました。 各レデューサーのloading
、 error
、 movies
の状態を選択しました。
レデューサーからmovies
を取得したら、ユーザーに表示できるようになります。 ここでは、ES6 map
関数を使用してこれを行いました。 ムービーの各状態が読み込まれているときは常にローダーを最初に表示し、エラーが発生した場合はエラーメッセージを表示します。 最後に、映画を入手した場合、 map
機能を使用して映画の画像をユーザーに表示します。 コンポーネント全体をMovieListContainer
コンポーネントでラップしました。
<MovieListContainer> … </MovieListContainer>
タグは、スタイル付きコンポーネントを使用して定義されたdiv
です。 それについてはすぐに簡単に見ていきます。
スタイル付きコンポーネントを使用したアプリのスタイリング
スタイル付きコンポーネントを使用すると、ページとコンポーネントを個別にスタイル設定できます。 また、 inheritance
、 Theming
、 passing of props
など、いくつかの興味深い機能も提供します。
私たちは常にページを個別にスタイリングしたいのですが、グローバルなスタイリングが望ましい場合もあります。 興味深いことに、styled-componentsは、 createGlobalStyle
関数のおかげでそれを行う方法を提供します。
アプリケーションでスタイル付きコンポーネントを使用するには、それをインストールする必要があります。 反応プロジェクトでターミナルを開き、次のbash
コマンドを入力します。
npm install styled-components
styled-componentsをインストールしたら、グローバルスタイルを始めましょう。
styles
という名前のsrc
ディレクトリに別のフォルダを作成しましょう。 これにより、すべてのスタイルが保存されます。 また、stylesフォルダー内にglobalStyles.js
ファイルを作成しましょう。 styled-componentsでグローバルスタイルを作成するには、 createGlobalStyle
をインポートする必要があります。
import { createGlobalStyle } from "styled-components";
次に、スタイルを次のように定義できます。
export const GlobalStyle = createGlobalStyle` ... `
スタイル付きコンポーネントは、テンプレートリテラルを使用して小道具を定義します。 このリテラル内で、従来のCSS
コードを記述できます。
また、 definition.js
という名前のファイルで定義されたdeviceWidth
をインポートしました。 deviceWidth
は、メディアクエリを設定するためのブレークポイントの定義を保持します。
import { deviceWidth } from "./definition";
アプリケーションのフローを制御するために、オーバーフローを非表示に設定しました。
html, body{ overflow-x: hidden; }
また、 .header
スタイルセレクターを使用してヘッダースタイルを定義しました。
.header{ z-index: 5; background-color: ${(props)=>props.theme.midDarkBlue}; display:flex; align-items:center; padding: 0 20px; height:50px; justify-content:space-between; position:fixed; top:0; width:100%; @media ${deviceWidth.laptop_lg} { width:97%; } ... }
ここでは、背景色、z-index、パディング、およびその他の多くの従来のCSSプロパティなどのさまざまなスタイルが定義されています。
styled-componentsのprops
を使用して背景色を設定しました。 これにより、コンポーネントから渡すことができる動的変数を設定できます。 さらに、テーマの切り替えを最大限に活用できるように、テーマの変数も渡しました。
ここでは、アプリケーション全体をstyled-componentsのThemeProvider
でラップしているため、テーマ設定が可能です。 これについては後で説明します。 さらに、 CSS flexbox
を使用してヘッダーのスタイルを適切に設定し、位置をfixed
に設定して、ブラウザーに対して固定されたままになるようにしました。 また、ヘッダーをモバイルフレンドリーにするためにブレークポイントを定義しました。
これがglobalStyles.js
ファイルの完全なコードです。
import { createGlobalStyle } from "styled-components"; import { deviceWidth } from "./definition"; export const GlobalStyle = createGlobalStyle` html{ overflow-x: hidden; } body{ background-color: ${(props) => props.theme.lighter}; overflow-x: hidden; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } #root{ display: grid; flex-direction: column; } h1,h2,h3, label{ font-family: 'Aclonica', sans-serif; } h1, h2, h3, p, span:not(.MuiIconButton-label), div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){ color: ${(props) => props.theme.bodyText} } p, span, div, input{ font-family: 'Jost', sans-serif; } .paginate button{ color: ${(props) => props.theme.bodyText} } .header{ z-index: 5; background-color: ${(props) => props.theme.midDarkBlue}; display: flex; align-items: center; padding: 0 20px; height: 50px; justify-content: space-between; position: fixed; top: 0; width: 100%; @media ${deviceWidth.laptop_lg}{ width: 97%; } @media ${deviceWidth.tablet}{ width: 100%; justify-content: space-around; } a{ text-decoration: none; } label{ cursor: pointer; color: ${(props) => props.theme.goldish}; font-size: 1.5rem; } .hamburger{ cursor: pointer; color: ${(props) => props.theme.white}; @media ${deviceWidth.desktop}{ display: none; } @media ${deviceWidth.tablet}{ display: block; } } } .mobileHeader{ z-index: 5; background-color: ${(props) => props.theme.darkBlue}; color: ${(props) => props.theme.white}; display: grid; place-items: center; width: 100%; @media ${deviceWidth.tablet}{ width: 100%; } height: calc(100% - 50px); transition: all 0.5s ease-in-out; position: fixed; right: 0; top: 50px; .menuitems{ display: flex; box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme}; flex-direction: column; align-items: center; justify-content: space-around; height: 60%; width: 40%; a{ display: flex; flex-direction: column; align-items:center; cursor: pointer; color: ${(props) => props.theme.white}; text-decoration: none; &:hover{ border-bottom: 2px solid ${(props) => props.theme.goldish}; .MuiSvgIcon-root{ color: ${(props) => props.theme.lightred} } } } } } footer{ min-height: 30px; margin-top: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.875rem; background-color: ${(props) => props.theme.midDarkBlue}; color: ${(props) => props.theme.white}; } `;
リテラル内に純粋なCSSコードを記述しましたが、いくつかの例外があることに注意してください。 スタイル付きコンポーネントを使用すると、小道具を渡すことができます。 これについて詳しくは、ドキュメントをご覧ください。
グローバルスタイルの定義とは別に、個々のページのスタイルを定義できます。
たとえば、 styles
フォルダーのPersonListPage.js
で定義されているPersonListPage.jsのスタイルは次のPersonStyle.js
です。
import styled from "styled-components"; import { deviceWidth, colors } from "./definition"; export const PersonsListContainer = styled.div` margin: 50px 80px; @media ${deviceWidth.tablet} { margin: 50px 10px; } a { text-decoration: none; } .top { display: flex; justify-content: flex-end; padding: 5px; .MuiSvgIcon-root { cursor: pointer; &:hover { color: ${colors.darkred}; } } } .personslist { margin-top: 20px; display: grid; place-items: center; grid-template-columns: repeat(5, 1fr); @media ${deviceWidth.laptop} { grid-template-columns: repeat(4, 1fr); } @media ${deviceWidth.tablet} { grid-template-columns: repeat(3, 1fr); } @media ${deviceWidth.tablet_md} { grid-template-columns: repeat(2, 1fr); } @media ${deviceWidth.mobile_lg} { grid-template-columns: repeat(1, 1fr); } grid-gap: 30px; .person { width: 200px; position: relative; img { width: 100%; } .content { position: absolute; bottom: 0; left: 8px; border-right: 2px solid ${colors.goldish}; border-left: 2px solid ${colors.goldish}; border-radius: 10px; width: 80%; margin: 20px auto; padding: 8px 10px; background-color: ${colors.transparentWhite}; color: ${colors.darkBlue}; h2 { font-size: 1.2rem; } } } } `;
最初に、 styled-components
からstyled
をインポートし、 definition
ファイルからdeviceWidth
をインポートしました。 次に、 PersonsListContainer
をスタイルを保持するdiv
として定義しました。 メディアクエリと確立されたブレークポイントを使用して、さまざまなブレークポイントを設定することにより、ページをモバイルフレンドリーにしました。
ここでは、小さい画面、大きい画面、および非常に大きい画面に対して、標準のブラウザーブレークポイントのみを使用しました。 また、CSSフレックスボックスとグリッドを最大限に活用して、ページのコンテンツを適切にスタイル設定して表示しました。
PersonListPage.js
ファイルでこのスタイルを使用するには、次のようにインポートしてページに追加しました。
import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;
スタイルでdivとして定義したため、ラッパーはdiv
を出力します。
テーマを追加してまとめる
アプリケーションにテーマを追加することは、常に優れた機能です。 このためには、次のものが必要です。
- 別のファイル(この場合は
definition.js
ファイル)で定義されたカスタムテーマ。 - Reduxアクションとレデューサーで定義されたロジック。
- アプリケーションでテーマを呼び出し、それをコンポーネントツリーに渡します。
これをチェックしてみましょう。
これがdefinition.js
ファイルのtheme
オブジェクトです。
export const theme = { light: { dark: "#0B0C10", darkBlue: "#253858", midDarkBlue: "#42526e", lightBlue: "#0065ff", normal: "#dcdcdd", lighter: "#F4F5F7", white: "#FFFFFF", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "#0B0C10", lightshadowtheme: "rgba(0, 0, 0, 0.1)" }, dark: { dark: "white", darkBlue: "#06090F", midDarkBlue: "#161B22", normal: "#dcdcdd", lighter: "#06090F", white: "white", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "white", lightshadowtheme: "rgba(255, 255, 255, 0.9)" } };
明るいテーマと暗いテーマにさまざまな色のプロパティを追加しました。 ライトモードとダークモードの両方で視認できるように、色は慎重に選択されています。 テーマは自由に定義できます。 これは厳格なルールではありません。
次に、Reduxに機能を追加しましょう。
ReduxアクションフォルダーにglobalActions.js
を作成し、次のコードを追加しました。
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; import { theme } from "../../styles/definition"; export const switchToLightTheme = () => (dispatch) => { dispatch({ type: SET_LIGHT_THEME, payload: theme.light }); localStorage.setItem("theme", JSON.stringify(theme.light)); localStorage.setItem("light", JSON.stringify(true)); }; export const switchToDarkTheme = () => (dispatch) => { dispatch({ type: SET_DARK_THEME, payload: theme.dark }); localStorage.setItem("theme", JSON.stringify(theme.dark)); localStorage.setItem("light", JSON.stringify(false)); };
ここでは、定義したテーマをインポートしただけです。 対応するアクションをディスパッチし、必要なテーマのペイロードを渡しました。 ペイロードの結果は、明るいテーマと暗いテーマの両方で同じキーを使用してローカルストレージに保存されます。 これにより、ブラウザで状態を保持できます。
また、テーマのレデューサーを定義する必要があります。
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; export const toggleTheme = (state = {}, action) => { switch (action.type) { case SET_LIGHT_THEME: return { theme: action.payload, light: true }; case SET_DARK_THEME: return { theme: action.payload, light: false }; default: return state; } };
これは私たちがやってきたことと非常に似ています。 switch
ステートメントを使用してアクションのタイプを確認してから、適切なpayload
を返しました。 また、ユーザーが明るいテーマと暗いテーマのどちらを選択したかを決定する状態light
を返しました。 これをコンポーネントで使用します。
また、ルートレデューサーとストアに追加する必要があります。 これがstore.js
の完全なコードです。
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { theme as initialTheme } from "../styles/definition"; import reducers from "./reducers/index"; const theme = localStorage.getItem("theme") ? JSON.parse(localStorage.getItem("theme")) : initialTheme.light; const light = localStorage.getItem("light") ? JSON.parse(localStorage.getItem("light")) : true; const initialState = { toggleTheme: { light, theme } }; export default createStore(reducers, initialState, applyMiddleware(thunk));
ユーザーが更新するときにテーマを永続化する必要があるため、 localStorage.getItem()
を使用してローカルストレージからテーマを取得し、初期状態に渡す必要がありました。
Reactアプリケーションに機能を追加する
スタイル付きコンポーネントは、アプリケーションを介してテーマを渡すことができるThemeProvider
を提供します。 App.jsファイルを変更して、この機能を追加できます。
それを見てみましょう。
import React from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { useSelector } from "react-redux"; import { ThemeProvider } from "styled-components"; function App() { const { theme } = useSelector((state) => state.toggleTheme); let Theme = theme ? theme : {}; return ( <ThemeProvider theme={Theme}> <Router> ... </Router> </ThemeProvider> ); } export default App;
テーマをThemeProvider
に渡すことで、スタイルでテーマの小道具を簡単に使用できます。
たとえば、次のように、色をbodyText
カスタム色に設定できます。
color: ${(props) => props.theme.bodyText};
アプリケーションで色が必要な場所ならどこでもカスタムテーマを使用できます。
たとえば、 border-bottom
を定義するには、次のようにします。
border-bottom: 2px solid ${(props) => props.theme.goldish};
結論
まず、Sanity.ioを詳しく調べてセットアップし、Reactアプリケーションに接続しました。 次に、Reduxをセットアップし、GROQ言語を使用してAPIをクエリしました。 私たちは、 react-redux
を使用してReactアプリにReduxを接続して使用する方法、styled-componentsとテーマを使用する方法を見ました。
ただし、これらのテクノロジーで可能なことのほんの一部にすぎません。 私のGitHubリポジトリのコードサンプルを確認し、これらのテクノロジーを使用してまったく異なるプロジェクトを試して、それらを学習および習得することをお勧めします。
資力
- 正気のドキュメント
- KapeheによるSanity.ioでブログを構築する方法
- Reduxドキュメント
- スタイル付きコンポーネントのドキュメント
- GROQチートシート
- マテリアルUIドキュメント
- ReduxミドルウェアとSideEffects
- ReduxThunkドキュメント