ReactNativeのアーキテクチャについて

ReactNativeのアーキテクチャについて簡易的にまとめました。

概要

​ 今回の設計では ​

  • 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}>;

©Tsurutan. All Rights Reserved.