🌐

[Gatsby x TypeScript]Reduxを使った言語設定の表示切り替え

2024/04/25に公開

Gatsby x Wordpress でプロジェクト作成中、言語切り替えの値をstore管理したくてやり方を調べていた。途中ChatGPTなんかにも助けてもらいつつ備忘録をまとめた。(コピペ説明部分ありです)

免責:当方React もGatsbyもTypeScriptも初学者です。

ほか参考
Reac初心者でも読めば必ずわかるReactのRedux講座
ts公式 : React Redux TypeScript Quick Start

Getting started
初期プロジェクトならViteやNext.js向けにテンプレートがあるらしいが、既存アプリなので俺は黙ってnpm install.。react-reduxはReactとReduxを連携させるためのもの、@reduxjs/toolkitはReduxのボイラープレートを減らすためのツールセット

npm install @reduxjs/toolkit react-redux

ここでTsユーザーに朗報なのだが、React-Redux v8はTsで書かれているため、全ては型定義済みとのこと。

React-Redux v8 is written in TypeScript, so all types are automatically included.

この間エラー出まくったのでsassのバージョン上げたりgatsbyバージョン上げたり依存関係修復してました。Gatsby 5.13.12-14の3つ(どれも安定版)でビルドし直していたのですが、どれもreact-server-dom-webpackが特定の実験的なバージョンのReactを要求しているらしく("0.0.0-experimental-c8...")warningを全て解消するのは(現時点では)ちょっよ骨が折れるなあ、ということで断念。23 warningでヒヤヒヤしながらコーディングを続けようと思います。エロい人がいたら教えてください。

npm ls react // こっちで問題出てた
npm ls gatsby

Reduxのストアを設定

npm install @reduxjs/toolkit react-reduxできたので気を取り直してstore設定。
ここでは、@reduxjs/toolkitのconfigureStoreを使用。これにより、middlewareやdevtoolsの設定が簡単になるらしい。

toolkitについて。前置き長いので23:45頃から
YouTube: Let’s Learn Modern Redux! (with Mark Erikson) — Learn With Jason

Actionとreducerの作成

Redux Toolkit の createSlice 関数を使い、言語設定管理のための

  • reducer
  • action
    を定義。

1. 初期状態の定義 (Initial State)

src/store/ディレクトリ作成。

  • LanguageState インターフェース: ここでは、状態の型を定義しています。language プロパティは、文字列リテラル 'En' または 'Ja' のみを受け入れるようになっています。これにより、言語設定がこれらの値に限定されることを型システムが保証します。
  • initialState: この変数は、リデューサーの初期状態を設定します。アプリケーションの言語設定のデフォルトは 'En'(英語)です。
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// Define a type for the slice state
interface LanguageState {
  language: 'en' | 'ja'
}
// Define the initial state in reducer
const initialState: LanguageState = {
  language: 'en', // default lang
}

2. Slice の作成

  • createSlice 関数: Redux Toolkit によって提供されるこの関数は、リデューサーと関連するアクションを定義するためのもの。
  • name: スライスの名前を指定。この名前は、生成されるアクションタイプにプレフィックスとして使用される。
  • initialState: 上で定義した初期状態をこのスライスに割り当て。
  • reducers オブジェクト: アクションタイプとそれを処理するリデューサーロジックのマッピング。この例では、setLanguage アクションが定義されている。
    • setLanguage リデューサー: stateaction を引数に取り、state.languageaction.payload で更新。PayloadAction<'En' | 'Ja'> は、このアクションのペイロードが 'En' または 'Ja' であることを保証。
const LanguageSlice = createSlice({
  // name: name of the slice. Used as a prefix for action types
  name: 'language',
  // initialState: 上で定義した初期状態をこのスライスに割り当て
  initialState,
  // reducers: アクションタイプとそれを処理するリデューサーロジックのマッピング
  reducers: {
    // state と action を引数に取り、state.language を action.payload で更新
    setLanguage: (state, action: PayloadAction<'en' | 'ja'>) => {
      state.language = action.payload
    },
  },
})

3. アクションとリデューサーのエクスポート

  • languageSlice.actions: createSlice によって自動的に生成されるアクションクリエーター。setLanguage アクションクリエーターをエクスポートしている。これでコンポーネントからこのアクションを簡単にディスパッチできる。
  • languageSlice.reducer: スライスに対応するリデューサー。このリデューサーは、スライスの状態を更新するためのロジックを含んでいる。このリデューサーをストアの設定時に組み込むことで、対応するアクションがディスパッチされたときに状態更新が行われる。
export const { setLanguage } = LanguageSlice.actions;
export default LanguageSlice.reducer;

全コード

// src/state/languageSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// Define a type for the slice state
interface LanguageState {
  language: 'en' | 'ja'
}

// Define the initial state in reducer
const initialState: LanguageState = {
  language: 'en', // default lang
}

// Define Slice
// createSlice can define reducers and action creators together
const LanguageSlice = createSlice({
  // name: name of the slice. Used as a prefix for action types
  name: 'language',
  // initialState: 上で定義した初期状態をこのスライスに割り当て
  initialState,
  // reducers: アクションタイプとそれを処理するリデューサーロジックのマッピング
  reducers: {
    // state と action を引数に取り、state.language を action.payload で更新
    setLanguage: (state, action: PayloadAction<'en' | 'ja'>) => {
      state.language = action.payload
    },
  },
})

// languageSlice.actions: createSlice によって自動的に生成されるアクションクリエーター
// setLanguage アクションクリエーターをエクスポート
export const { setLanguage } = LanguageSlice.actions;
// languageSlice.reducer: スライスに対応するリデューサー
// リデューサーをストアの設定時に組み込むことで、対応するアクションがディスパッチされたときに状態更新が行われる
export default LanguageSlice.reducer;


Redux Toolkit の configureStore を使用してアプリケーションのストアを設定

configureStore 関数を使用し Redux ストアを作成.
アプリケーション全体で利用するための設定を行う。

  1. インポートと依存関係
  • configureStore: Redux Toolkit から configureStore 関数をインポート。
    この関数はストアを作成し、Redux DevTools と thunk ミドルウェアなどの一般的なミドルウェアを自動的に設定。
  • languageReducer: languageSlice.ts で定義されたリデューサーをインポート。これは言語設定の状態を管理するためのリデューサー。
import { configureStore } from '@reduxjs/toolkit';
import languageReducer from './languageSlice';

2. ストアの作成

  • configureStore 関数を使用しストアを作成。この関数にはオプションを含むオブジェクトを渡す。
  • reducer: このオプションは、アプリケーションの各状態を管理するリデューサーをマッピングするオブジェクト。
    ここでは language というキーに languageReducer を割り当て。これにより、language の状態に対するすべての操作が languageReducer によって処理される。
const store = configureStore({
  reducer: {
    language: languageReducer,
  },
});

3. 型定義のエクスポート

  • RootState: ストアの全体の状態の型を定義。
    store.getState の戻り値の型を利用して、ストアの状態の型を取得しています。これにより、コンポーネントや他の部分で状態を参照する際に型安全が保証される
  • AppDispatch: ストアの dispatch 関数の型を定義。これは、アクションをディスパッチする際に使用され、型安全なディスパッチ操作を可能にする。
  • ストアのエクスポート
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

コード全文

// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import languageReducer from './languageSlice';

const store = configureStore({
  reducer: {
    language: languageReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export default store;

createSlice

React 全体に適用

// gatsby-browser.tsx
import React from 'react'
import { Provider } from 'react-redux'
import store from './src/store/store'

export const wrapRootElement = ({ element }: { element: React.ReactNode }) => (
  <Provider store={store}>{element}</Provider>
)

コンポーネントで使用

react-redux の useSelectoruseDispatch フックを使い、Redux ストアの状態を読み取り、アクションをディスパッチする。

1. 必要なモジュールのインポート

// header.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from '../store/store';
import { setLanguage } from '../store/languageSlice';
  • useSelector, useDispatch: Redux フックをインポート。useSelector は Redux ストアの状態を読み取るため、useDispatch はアクションをディスパッチするために使用。
  • RootState, AppDispatch: ストアの型定義をインポート。これにより、TypeScript がストアの状態とディスパッチ関数の型を検証できる。
  • setLanguage: 言語設定を切り替えるためのアクションクリエーター

2. コンポーネントの定義

// header.tsx
const LanguageSwitcher: React.FC = () => {
  const language = useSelector((state: RootState) => state.language.language);
  const dispatch = useDispatch<AppDispatch>();

  // せっかくだからクラスもつける
  const langBtnClassEn = `lang_btn_en __${language === 'en' ? 'en' : 'ja'}`
  const langBtnClassJa = `lang_btn_ja __${language === 'en' ? 'en' : 'ja'}`

  return (
   <>
    {// 省略}
        <div className="hidden lg:flex lg:flex-1 lg:justify-end">
          <div className="text-sm font-semibold leading-6 text-gray-900">
            <button className={langBtnClassEn} onClick={() => dispatch(setLanguage('en'))}>
              EN
            </button>{' '}
            /{' '}
            <button className={langBtnClassJa} onClick={() => dispatch(setLanguage('ja'))}>
              JA
            </button>
          </div>
        </div>
    </>
  );
};
  • コンポーネントの構造: LanguageSwitcher は関数コンポーネントとして定義されており、React の Functional Component (FC) 型を使用。
  • 言語の読み取り: useSelector フックを使用して、Redux ストアから現在の言語設定 (language) を読み取り。RootState を使って、適切な型情報とともにストアの状態を参照する。
  • アクションのディスパッチ: useDispatch フックを用いて Redux ストアの dispatch 関数を取得する。ボタンがクリックされると、setLanguage アクションがディスパッチされ、言語設定が更新される。

スタイルはこんな感じ。style.scssでインポートしてる。

// header/style.scss
.lang_btn_en,
.lang_btn_ja {
  transition: color 0.5s ease;
}
.lang_btn_en {
  &.__en {
    color: #cfa254;
  }
  &.__ja {
    color: #444;
  }
}
.lang_btn_ja {
  &.__ja {
    color: #cfa254;
  }
  &.__en {
    color: #444;
  }
}

3. JSX のレンダリング

	<div className="hidden lg:flex lg:flex-1 lg:justify-end">
	  <div className="text-sm font-semibold leading-6 text-gray-900">
		<button className={langBtnClassEn} onClick={() => dispatch(setLanguage('en'))}>
		  EN
		</button>{' '}
		/{' '}
		<button className={langBtnClassJa} onClick={() => dispatch(setLanguage('ja'))}>
		  JA
		</button>
	  </div>
	</div>
  • 表示: ボタンに onClick イベントハンドラを設定。クリックで指定言語に切り替える(予定)。
  • アクションのトリガー: ボタンがクリックされたとき、dispatch(setLanguage('En'))dispatch(setLanguage('Ja')) が呼び出され、言語設定の状態が更新される。
  • <p>{language}<p/>とかどっかに入れると状態見れる

4. コンポーネントのエクスポート

export default LanguageSwitcher;

ページでの表示切り替え

languageの値に応じて表示変更する。例として、プロフィールを表示するAboutページで。

インポート

// about.tsx
import React from 'react'
import { useSelector } from 'react-redux'
import { RootState } from '../store/store'

マークダウンの表示にはreact-markdowを使用。

npm i react-markdown@8.0.6

インポート

import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
  1. useSelector フックを使って Redux ストアから現在の言語設定 language を取得。
  2. 取得した language に基づいて myIntroduction 配列から対応するプロフィールを検索。
    検索に失敗した場合のデフォルトとして myIntroduction[0] (通常は英語) を設定。
  3. 選択された profile の name と description を表示.

マークダウン実装

  1. オブジェクトに入れたマークダウンを<Markdown remarkPlugins={[remarkGfm]}>{profile.description}</Markdown>として表示するだけ。
  2. オブジェクトに入れるmarkdownはインデント入れないよう注意。(ここでハマった!)
// about.tsx

const AboutPage = ({ data, location }: PageProps<GatsbyTypes.Query>) => {
  const language = useSelector((state: RootState) => state.language.language)

  const myIntroduction = [
    {
      lang: 'en',
      name: 'test osterone',
      description: `
## Front-End Developer

---

### 🌐 About Me

Aspiring front-end developer skilled in HTML, CSS, JavaScript, and React.

### 🚀 Projects

- **Portfolio Website:** Built with React. [Visit Site](#)

### 📞 Contact

- **Email:** test.sterone@example.com
- **LinkedIn:** [linkedin.com/in/testosterone](#)

      `,
    },
    {
      lang: 'ja',
      name: 'テスト ステロン',
      description: `
## フロントエンド開発者

---

### 🌐 自己紹介

HTML、CSS、JavaScript、Reactを得意とするフロントエンド開発者。

### 🚀 プロジェクト

- **ポートフォリオウェブサイト:** Reactで構築。[サイトを見る](#)

### 📞 連絡先

- **メール:** test.sterone@example.com
- **LinkedIn:** [linkedin.com/in/testosterone](#)

      `,
    },
  ]

  const profile = myIntroduction.find(p => p.lang === language) || myIntroduction[0]

  return (
    <>
      <Layout location={{ pathname: '/about' }} title="About">
        <Seo title="About" />
        <h1>About Me</h1>
        <h2>{profile.name}</h2>
        <article>
          <Markdown remarkPlugins={[remarkGfm]}>{profile.description}</Markdown>
        </article>
      </Layout>
    </>
  )
}

export default AboutPage

できた〜!
今度はmdファイルを別ファイルとして読み込む設定をしたい。

Discussion