概要
今回の設計では
redux
AtomicDesign
を使用し、状態管理・UI コンポーネントの設計を行う。 またビジネスロジック・データ通信・永続化はクリーンアーキテクチャ・DDD を参考に - Repository
- Service
と分けて開発を行う。
また言語は TypeScript を使用し、型付けをおこなう。
特別な理由がない場合
any
型は極力控えること。 絶対パスとして~
を指定すると、src
配下を参照するようにする。
フォルダ構成
全体のフォルダ構成は下記のようになる。
src/
├── components
│ ├── atoms
│ ├── molecules
│ ├── organisms
│ ├── pages
│ └── templates
├── constants
├── hooks
├── models
├── navigations
├── redux
│ └── modules
├── repositories
└── services
components
components では AtomicDesign に沿ったフォルダ構成となっている。 AtomicDesign についてはこちらを参照のこと。
atoms
Atom が入る。
molecules
Molecules が入る。
organisms
Organism が入る。
templates
Template が入る。
ファイル名の Prefix には Template
と付けること。
ex
HomeTemplate
pages
Page が入る
ファイル名の Prefix には Page
と付けること。
今回のアーキテクチャで Redux コンテナの役割はこの Page が果たすこと。
ex
HomePage
hooks
関数コンポーネントにて使用される Hooks を配置するディレクトリ。
関数名は use
から始めること。
ex
useInputState
models
このディレクトリには Model に関わるファイルの配置を行う。 また Model の書き方では例を使い説明を行う。 例: user.ts
export interface Model { readonly id: string; readonly name: string; readonly email?: string; readonly createdAt: string; readonly updatedAt: string; } export interface Values { readonly name: string; readonly email?: string; } export function factory(todo: Values): Model { assertIsDefined(todo.title); const now = new Date().toISOString(); return { id: generateUuid(), title: todo.title, email: todo.email, createdAt: now, updatedAt: now, }; } export function change(todo: Model, newValues: Values): Model { assertIsDefined(newValues.title); const now = new Date().toISOString(); return { ...todo, ...newValues, updatedAt: now, }; }
上記の例では User に関する構造を Interface で定義しており、User の作成は factory
で行い、ユーザーの更新は change
を使う。
この例でわかるように User データは immutable でデータ自体の変更は行わず新しくオブジェクトを作成するようにする。
TypeScript では readonly
と付けることで変数の書き込みに制限を加えることができるため、極力これを付けるようにすること。
またこの User モデルを使用するときは下記のように import * as User from './user';
とすべての関数・変数を import し
その モデル名にあった名前を付ける。こうすることで名前空間を作成することができ、ほかモデルでは自由度高く関数名を付けることができる。
redux
このディレクトリには Redux に関わる処理を記入する。
今回は redux-toolkit
という Redux を簡易的にかけるパッケージを使用し、Ducks というアーキテクチャを用いる。
また redux
の非同期処理では redux-thunk
を使用している。
modules
このディレクトリ配下では Redux の Action
ActionCreator
Reducer
を一つにまとめたファイルを配置する。
例
/* eslint no-await-in-loop: 0 */ import UserService from '~/services/UserService'; import RealmUserRepository from '~/repositories/RealmUserRepository'; import * as User from '~/models/user'; import {createSlice, PayloadAction} from '@reduxjs/toolkit'; const service = new UserService(new RealmUserRepository()); export const initialState: UserStateInterface = { user: User.Model, }; const persistDataSlice = createSlice({ name: 'userState', initialState, reducers: { fetchUserAction(state, {payload}: PayloadAction<User.Model>) { state.user = payload; }, }, }); const {fetchPrefecturesAction} = persistDataSlice.actions; export const fetchUser = () => (dispatch: any) => { service .fetchUser() .then((user: User.Model) => { dispatch(fetchUserAction(user)); }) .catch((err) => { // Error handling}); }; export default persistDataSlice.reducer;
また これら作成した reducer
をまとめるためのファイルもこのディレクトリ配下に作成する。
import UserState from '~/redux/modules/UserState'; import SomethingState from '~/redux/modules/SomethingState'; const reducers = { userState: UserState, somethingState: SomethingState, }; export default reducers;
navigations
ReactNavigation で作成される Navigation
となるファイルを格納する。
つまり画面遷移をするときの親となる画面である。
ex: HomeNavigator
import React from 'react'; import {createStackNavigator} from '@react-navigation/stack'; import {USER, HOME} from '~/constants/path'; import {Home, User} from '~/containers'; import {HeaderLeft, headerStyle, headerTintColor} from '../Header'; import {COLOR} from '~/constants/theme'; const cardStyle = { backgroundColor: COLOR.MAIN, }; const Stack = createStackNavigator(); function HomeNavigator() { return ( <Stack.Navigator screenOptions={{ headerStyle, headerTintColor, cardStyle, }}> <Stack.Screen name={HOME} component={Home} options={{ headerLeft: () => <HeaderLeft />, title: 'Home', }} /> <Stack.Screen name={USER} component={User} options={{ title: 'User', }} /> </Stack.Navigator> ); } export default HomeNavigator;
repositories
データ通信・データの永続化を行う Repository を配置するディレクトリ。 インフラ層の中身を Repository に依存するモジュールに知らせないために、 抽象的な Repository インターフェスを作成し、具体的な処理(データ永続化・通信)はこのインターフェスを 実装したクラスで書くようにする。 ex: UserRepository
// 抽象的なRepositoryの方針について書く interface UserRepository { getAll(): User[]; save(user: User): void; }
ex: RealmUserRepository
// Repositoryの具体的な中身について書く class RealmUserRepository implements UserRepository { getAll(): User[] { // 具体的なすべてのユーザーを取得する処理 } save(user: User): void { // 具体的なユーザーを保存する処理 } }
services
Service では主にモデル内で書ききれないユースケースに関わる処理について記入する。 またこのとき Service の引数で Repository が渡ってくるが、その Repository は上記で説明している インターフェスを指定する。
class UserService { constructor(private userRepository: UserRepository); saveAllUser(): void { const users = this.userRepository.getAll(); users.forEach((user) => { this.userRepository.save(user); }); } }
selectors
React では prop または state に変更が検知されたときに画面の更新を行うが、
意図していない状況で更新が発生してしまうケースが多く、これがパフォーマンスへ影響する。
例えば prop, state の変更検知には shallow 比較という、プロパティが直接保持している値のみを比較する。
JavaScript では Object の参照値を変数が保持するため、Object のプロパティは変更前後で変わっていないが、
Object が新しく代入されてしまうと React は更新されたと考えコンポーネントを更新してしまう。
これを防ぐために Object のプロパティを一つ一つ比較する必要があり、これを実現するのがこの selector になる。
比較をするに当たり reselect
というパッケージを使用する。
import {createSelector} from 'reselect'; import * as User from '../models/User'; import {AppState} from '../modules'; function selectUsers(state: AppState) { return state.users; } export const getArray = createSelector([selectUsers], (users) => Object.values(users).map((user: User.Model) => ({ id: user.id, name: user.name, email: user.email, createdAt: new Date(user.createdAt).getTime(), updatedAt: new Date(user.updatedAt).getTime(), })), ); export const getUsers = createSelector([getArray], (users) => users.sort((a, b) => b.createdAt - a.createdAt), );
またこれらの selector は Redux
で提供されている useSelector
を使用し、
対象となるデータを効率よく取得することができる。
const users = useSelector(getUsers);
constants
testId, color, path(ReactNavigation) などの定数についてまとめる。 例: testIds
export default { INITIAL: 'INITIAL', INITIAL_PAGE1: 'INITIAL_PAGE1', INITIAL_PAGE2: 'INITIAL_PAGE2', INITIAL_PAGE3: 'INITIAL_PAGE3', } as Readonly<{[key: string]: string}>;