Closed10

ReactとReduxを使っているとき、ドメインオブジェクトとドメインサービスへはどのように連携をとればよいでしょうか?

TakechyTakechy

Reactのサービス層ってなんですか?具体的にどこに書きますか?

React のサービス層は、アプリケーションのビジネスロジックやデータアクセスロジックを処理するために作成される層です。サービス層は、コンポーネントから独立して機能し、API 通信やデータの操作、ドメインオブジェクトのインスタンス化などを行います。サービス層を使用することで、コードの再利用性が向上し、コンポーネント内のロジックをシンプルに保つことができます。

具体的には、サービス層はプロジェクト構造内の特定のディレクトリに配置することが一般的です。例えば、src/services というディレクトリを作成し、その中にサービス関連のファイルを配置することができます。サービス層は、機能やドメインに基づいて複数のサービスに分割することも可能です。これらのサービスは、React コンポーネントやカスタムフックからインポートして使用されます。

サービス層の例:

src/
  services/
    UserService.js      // ユーザー関連のAPI通信や操作を行うサービス
    ProductService.js   // 商品関連のAPI通信や操作を行うサービス
    ...

React コンポーネントやカスタムフック内でサービスを利用する例:

import React, { useEffect, useState } from 'react';
import { UserService } from '../services/UserService';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const userData = await UserService.fetchUserById(userId);
      setUser(userData);
    }

    fetchData();
  }, [userId]);

  return (
    <div>
      {user && (
        <>
          <h1>{user.name}</h1>
          <p>{user.email}</p>
        </>
      )}
    </div>
  );
}

TakechyTakechy

UserService内からから状態管理のReduxを操作しますか?

通常、UserService はAPIとの通信やデータ操作などの役割を担っており、直接Reduxの状態管理を操作することはありません。代わりに、カスタムフックやReduxのアクションクリエイター、サンク(thunk)を使用して、UserService から取得したデータを状態管理に反映させることが一般的です。

例えば、UserService からユーザーデータを取得し、Reduxストアに保存する方法を以下に示します。

  1. Reduxのスライス(slice)を作成し、ユーザーデータを管理するための状態とアクションを定義します。
// src/store/userSlice.js
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    loading: false,
    error: null,
  },
  reducers: {
    fetchUserStart: (state) => {
      state.loading = true;
    },
    fetchUserSuccess: (state, action) => {
      state.loading = false;
      state.data = action.payload;
    },
    fetchUserFailure: (state, action) => {
      state.loading = false;
      state.error = action.payload;
    },
  },
});

export const { fetchUserStart, fetchUserSuccess, fetchUserFailure } = userSlice.actions;
export default userSlice.reducer;

  1. UserService からユーザーデータを取得する非同期アクションクリエイターを定義します。
// src/store/userActions.js
import { fetchUserStart, fetchUserSuccess, fetchUserFailure } from './userSlice'; // [!]
import { UserService } from '../services/UserService';

export const fetchUserById = (userId) => async (dispatch) => {
  try {
    dispatch(fetchUserStart());
    const userData = await UserService.fetchUserById(userId);
    dispatch(fetchUserSuccess(userData)); // [!]
  } catch (error) {
    dispatch(fetchUserFailure(error.message)); // [!]
  }
};

  1. カスタムフック内で非同期アクションを使用して、UserService から取得したデータをReduxストアに保存します。
// src/hooks/useUser.js
import { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchUserById } from '../store/userActions';

export function useUser(userId) {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchUserById(userId));
  }, [userId, dispatch]); // [!]
}

このように、UserService 自体はReduxストアに直接アクセスせず、カスタムフックやアクションクリエイターを介して状態管理にデータを反映させることが推奨されます。これにより、UserService はAPIとの通信やデータ操作に専念でき、コードの責任範囲を明確に保つことができます。

TakechyTakechy

ドメインオブジェクトの状態はReactとReduxではどこで管理しますか?

React と Redux を使用している場合、ドメインオブジェクトの状態は Redux のストアで管理されることが一般的です。Redux はアプリケーションの状態を一元管理するためのライブラリであり、ドメインオブジェクトの状態をアプリケーション全体で共有したり、管理しやすくする目的で使用されます。

ドメインオブジェクトの状態を Redux で管理するためには、以下の手順を実行します。

  1. Redux の store を作成し、ドメインオブジェクトの状態を保持するための reducer を定義します。reducer は、状態を更新するための純粋な関数です。
  2. reducer を組み合わせて、アプリケーション全体で使用する store を作成します。
  3. Redux の Provider コンポーネントを使用して、作成した store をアプリケーション全体で利用できるようにします。
  4. ドメインオブジェクトの状態にアクセスする必要があるコンポーネントで、useSelector と useDispatch を使用して、状態にアクセスし、アクションをディスパッチします。

このように、React と Redux を使用することで、ドメインオブジェクトの状態を一元管理し、アプリケーション全体で共有しやすくなります。また、状態変更に関連するロジックを reducer でまとめることで、状態管理がより簡潔でメンテナンスしやすくなります。

TakechyTakechy

具体的にUserドメインオブジェクト、UserServiceとReduxを使ってサンプルコードを書いてみて

TakechyTakechy

整理

  • ドメインオブジェクト
  • ドメインサービス
    • ドメインオブジェクトの状態管理を含む
  • アプリケーションサービス?
    • Redux => アプリの状態管理?

仮説

  • 状態保存すべき情報は、ドメインサービスで取得した結果から、アプリ全体で必要なものはstoreで管理し、コンポーネント内で必要なものはReact.Stateフックを使う。
  • ドメインオブジェクトのインスタンスの生成もコンポーネント内で行うべき。
  • コンポーネント毎にドメインオブジェクト、ドメインサービスの読み込みを行うので、アプリ全体でのメモリ圧迫は防げるかもしれないが、各ページ内の読み込み時間は増えるかも知れない。

困りごと

  • 状態保存すべき情報を、アプリ全体、コンポーネント内で分けると「ドメインオブジェクト」と「値オブジェクト」に分類できない。ここに「アプリオブジェクト」という概念を作ってしまうことになる。
  • 「アプリオブジェクト」はドメインオブジェクトとして抽象化できるかも知れない。
  • Reduxはインスタンス変数を直接ステートとして持てない...よってドメインオブジェクトをここで管理できない。
  • もっと、DDDの知識を整理して解決方法を探すべきだ。
TakechyTakechy

アプリケーションサービスからReduxを操作する?

=> アプリケーションサービスはReduxに対してアクションを発行し、Reduxが状態を変更することを期待するべきです。

じゃぁ、ドメイン駆動開発ではReduxの方針と合わない?

Reduxは、アプリケーションの状態管理を行うためのライブラリであり、アプリケーションの状態をグローバルに管理することができます。一方で、ドメイン駆動開発では、ビジネスロジックをドメインオブジェクトとしてモデル化し、オブジェクト指向の原則に従って設計することが重視されます。

ドメイン駆動開発においては、ドメインオブジェクトがアプリケーションの状態を管理することが多く、Reduxのようにグローバルに状態を管理するアプローチは、ドメイン駆動開発の方針とは少し異なると言えます。

ただし、ReduxはUI層の状態管理に特化しており、ドメイン駆動開発のようにビジネスロジックを重視するアプリケーションでも、UI層の状態管理にReduxを活用することができます。また、Reduxのアーキテクチャは、アプリケーション全体の状態を明確に管理することができるため、ビジネスロジックとの組み合わせも可能です。

TakechyTakechy

DDDでのReduxライブラリの立ち位置は?

DDDにおいてReduxライブラリは、状態管理の仕組みの一つとして位置付けられます。具体的には、ドメインオブジェクトの状態やビジネスロジックに必要な情報を、Reduxのstoreに格納し、Reactコンポーネントで参照できるようにします。ただし、Reduxのstoreがドメインオブジェクトそのものとして考えることはできず、単に状態管理の仕組みとして位置づけられます。また、Reduxを使うかどうかは、アプリケーションの規模や複雑さ、チームの開発体制などによって異なるため、必ずしも全てのプロジェクトで必要とされるものではありません。

TakechyTakechy

ここまでの情報を踏まえて、DDDでReactをつかったWebアプリを作る際にはReduxの導入は開発を複雑にするものだと考えられますか?

DDDでReactを使ったWebアプリケーションを開発する場合、Reduxを導入することは必須ではありませんが、Reduxが提供するグローバルな状態管理のメリットを生かせる場合があります。

ただし、Reduxを導入することによって、アプリケーション全体の状態管理が複雑になる可能性があるため、適切に設計する必要があります。また、Reduxが提供するアーキテクチャを理解することが必要であり、適切に実装しなければ、コードの可読性が低下したり、不必要に複雑になる恐れがあります。

そのため、Reduxを導入する場合には、適切な設計や実装を行い、開発チーム全員が理解した上で採用することが望ましいです。

TakechyTakechy

Reduxを使わないで、ユーザーログインを状態管理する方法をサンプルコードを書いて説明して。必要に応じてクッキーからトークンを取得してログイン状態を確認する方法をステート管理を用いてサンプルコードを書いて。

import React, { useState, useEffect } from 'react';

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    const token = getAuthTokenFromCookie();
    if (token) {
      // APIを呼び出してトークンの妥当性を検証する
      validateToken(token).then(result => {
        setIsLoggedIn(result);
      });
    }
  }, []);

  const handleLogin = async (username, password) => {
    // APIを呼び出してログイン処理を行う
    const token = await login(username, password);
    setIsLoggedIn(true);
    // トークンをクッキーに保存する
    setAuthTokenToCookie(token);
  };

  const handleLogout = () => {
    // クッキーからトークンを削除する
    removeAuthTokenFromCookie();
    setIsLoggedIn(false);
  };

  return (
    <div>
      {isLoggedIn ? (
        <button onClick={handleLogout}>Logout</button>
      ) : (
        <LoginForm onLogin={handleLogin} />
      )}
    </div>
  );
}

function setAuthTokenToCookie(token) {
  document.cookie = `authToken=${token}; path=/`;
}

function getAuthTokenFromCookie() {
  const name = 'authToken=';
  const cookies = document.cookie.split(';');
  for (let i = 0; i < cookies.length; i++) {
    let cookie = cookies[i];
    while (cookie.charAt(0) === ' ') {
      cookie = cookie.substring(1);
    }
    if (cookie.indexOf(name) === 0) {
      return cookie.substring(name.length, cookie.length);
    }
  }
  return '';
}

function removeAuthTokenFromCookie() {
  document.cookie = 'authToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
}

async function validateToken(token) {
  // APIを呼び出してトークンの妥当性を検証する
  const response = await fetch('/api/validateToken', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
  return response.ok;
}

async function login(username, password) {
  // APIを呼び出してログイン処理を行う
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      username,
      password,
    }),
  });
  if (response.ok) {
    const data = await response.json();
    return data.token;
  } else {
    throw new Error('Login failed');
  }
}

function LoginForm({ onLogin }) {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (event) => {
    event.preventDefault();
    onLogin(username, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input type="text" id="username" value={username} onChange={e => setUsername(e.target.value)} />
      </div>
      <div>
        <label htmlFor="password">Password:</
  </div>
  <button type="submit">Login</button>
</form>
);
}

export default App;
TakechyTakechy

結論

一旦、reduxを使わないで開発してみる。
ステート管理はReactの基本的なフックで実装する。

このスクラップは2023/08/27にクローズされました