2025年5月 React Native 開発環境

# TLDR
- RN 開発環境作成の備忘録
- なるべく devcontainer で
- デバッグには vscode-react-native 1.14.0 が必要
  現在 1.13.0 https://marketplace.visualstudio.com/items?itemName=msjsdiag.vscode-react-native
- パスエイリアスには @react-native/babel-preset が必要

# devcontainer
- 開発環境は devcontainer で管理しているため、RN 開発でもそうしたい
- iOS / Android のビルドは devcontainer では不可能
- なので、Hermes はコンテナで、Android はホストで実行

![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69410/35dc4e8b-fc9d-4afe-8398-404fdb9f4046.png)


### .devcontainer/devcontainer.json
```json
{
  "name": "Node.js & TypeScript",
  "dockerComposeFile": "docker-compose/docker-compose.yml",
  "service": "devcontainer",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

  "forwardPorts": [8081],

  "customizations": {
    "vscode": {
      "extensions": [
        "esbenp.prettier-vscode",
        "/tmp/vscode-react-native-1.14.0.vsix",
        "dbaeumer.vscode-eslint",
        "mike-co.import-sorter"
      ],
      "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true,

        "editor.codeActionsOnSave": {
          "source.organizeImports": "never",
          "source.fixAll.eslint": "explicit"
        },
        "typescript.preferences.importModuleSpecifier": "relative"
      }
    }
  },

  "remoteUser": "node"
}
```

### .devcontainer/docker-compose/docker-compose.yml 
- [注意] node_modules を volume にしているので、ホスト側でも npm install が必要

```yml
name: your_project
services:
  devcontainer:
    init: true
    build:
      context: devcontainer
    volumes:
      - ../../..:/workspaces:cached
      - node_modules:/workspaces/your_project/node_modules
    command: sleep infinity

volumes:
  node_modules:
```

### .devcontainer/docker-compose/devcontainer/Dockerfile
```
FROM mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm
COPY vscode-react-native-1.14.0.vsix /tmp/
```

# デバッグ
- 公開されている ```vscode-react-native``` 1.13.0 では Hermes に繋がらない
- 1.14.0 で治るようだ。以下から vsix を取得する。
  https://github.com/microsoft/vscode-react-native/issues/2230
- Hermes にアタッチできれば、ブレークポイントが効くようになる

### .vscode/launch.json
```json
{
  "configurations": [
    {
      "name": "Attach Android Hermes",
      "request": "attach",
      "type": "reactnativedirect",
      "cwd": "${workspaceFolder}",
      "platform": "android"
    }
  ]
}
```

# パス エイリアス
- 自分のコードは src ディレクトリ以下で管理する
- @/ で src ディレクトリが参照できるように、パスエイリアスを設定する
- デフォルトで ```babel.config.js``` に指定されている ```module:metro-react-native-babel-preset``` だとうまく動かないので、```@react-native/babel-preset``` に差し替える
   https://github.com/facebook/react-native/issues/50683#issuecomment-2807742468
- ( パスエイリアス の設定がいちばん面倒くさい )

### babel.config.cjs
```js
module.exports = {
  presets: ['@react-native/babel-preset'],
  plugins: [
    'react-native-reanimated/plugin',
    [
      'module-resolver',
      {
        extensions: [
          '.ios.js',
          '.android.js',
          '.ios.jsx',
          '.android.jsx',
          '.js',
          '.jsx',
          '.json',
          '.ts',
          '.tsx',
        ],
        root: ['./src'],
        alias: {
          '@': './src',
        },
      },
    ],
  ],
};
```

### tsconfig.json
```json
{
  "extends": "@react-native/typescript-config/tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"]
    },
    "types": ["jest"]
  },
  "include": ["src"]
}
```
### metro.config.cjs
```js
const path = require('path');

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

/**
 * Metro configuration
 * https://reactnative.dev/docs/metro
 *
 * @type {import('@react-native/metro-config').MetroConfig}
 */
const config = {
  watchFolders: [path.resolve(__dirname, 'src')],
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```

# import オーダー
- import の並び順が、パスエイリアス込みで、良い感じになるように
 
### .eslintrc.cjs
```js
module.exports = {
  root: true,
  extends: '@react-native',
  plugins: ['import'],
  rules: {
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          ['parent', 'sibling', 'index'],
        ],
        pathGroups: [
          {
            pattern: '@/**',
            group: 'internal',
          },
        ],
        pathGroupsExcludedImportTypes: ['builtin'],
        alphabetize: {
          order: 'asc',
          caseInsensitive: true,
        },
        'newlines-between': 'always',
      },
    ],
  },
  settings: {
    'import/resolver': {
      typescript: {
        project: './tsconfig.json',
      },
    },
  },
};
```

# jest

- vitest は動かなかったので jest で
- jest にも、パスエイリアスの設定が必要

### jest.config.cjs
```js
module.exports = {
  preset: 'react-native',
  transform: {
    '^.+\\.[jt]sx?$': 'babel-jest',
  },
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|@react-navigation|react-redux)/)',
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
  coverageDirectory: '<rootDir>/coverage',
  setupFiles: ['./jest.setup.cjs'],
};
```

### jest.setup.cjs
```js
/* global jest */
const mockAsyncStorage = require('@react-native-async-storage/async-storage/jest/async-storage-mock');
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
```

## 参考
### package.json
```json
{
  "name": "your_project",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "react-native start",
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint \"src/**/*.{ts,tsx}\"",
    "format": "prettier \"src/**/*.{ts,tsx,json}\" --write",
    "fix": "npm run lint -- --fix && npm run format",
    "test": "jest --detectOpenHandles",
    "test:coverage": "jest --coverage"
  },
  "dependencies": {
    "@react-native-async-storage/async-storage": "^2.1.2",
    "@react-native-picker/picker": "^2.11.0",
    "@react-navigation/native": "^7.1.6",
    "@react-navigation/native-stack": "^7.3.10",
    "@reduxjs/toolkit": "^2.7.0",
    "axios": "^1.8.4",
    "babel-plugin-module-resolver": "^5.0.2",
    "i18next": "^25.0.1",
    "metro-react-native-babel-preset": "^0.77.0",
    "react": "^19.0.0",
    "react-i18next": "^15.5.1",
    "react-native": "^0.79.1",
    "react-native-reanimated": "^3.17.5",
    "react-native-safe-area-context": "^5.4.0",
    "react-native-screens": "^4.10.0",
    "react-redux": "^9.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@babel/preset-env": "^7.25.3",
    "@babel/runtime": "^7.25.0",
    "@react-native-community/cli": "18.0.0",
    "@react-native-community/cli-platform-android": "18.0.0",
    "@react-native-community/cli-platform-ios": "18.0.0",
    "@react-native/babel-preset": "0.79.1",
    "@react-native/eslint-config": "0.79.1",
    "@react-native/metro-config": "0.79.1",
    "@react-native/typescript-config": "0.79.1",
    "@testing-library/react-native": "^13.2.0",
    "@types/jest": "^29.5.14",
    "@types/react": "^19.0.0",
    "@typescript-eslint/eslint-plugin": "^7.18.0",
    "@typescript-eslint/parser": "^7.18.0",
    "babel-jest": "^29.7.0",
    "eslint": "^8.19.0",
    "eslint-import-resolver-typescript": "^4.3.4",
    "eslint-plugin-import": "^2.31.0",
    "eslint-plugin-react": "^7.37.5",
    "jest": "^29.7.0",
    "prettier": "2.8.8",
    "react-test-renderer": "^19.0.0",
    "typescript": "5.0.4"
  },
  "engines": {
    "node": ">=18"
  }
}
```

# 実行手順

0. host 側 : npm install

1. vscode 側 : npm start
2. host 側 : npm run adroid
3. vscode 側 : デバッグ開始 ( attach metro )


# もし動かなくなったら
### キャッシュ消す
```bash
npm start -- --reset-cache
```
```bash
cd android
./gradlew clean
cd ..
npm run adroid
```
```bash
cd ios
xcodebuild clean
cd ..
npm run ios
```