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 はホストで実行
.devcontainer/devcontainer.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 が必要
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
{
"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
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
{
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
},
"types": ["jest"]
},
"include": ["src"]
}
metro.config.cjs
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
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
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
/* 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
{
"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"
}
}
実行手順
-
host 側 : npm install
-
vscode 側 : npm start
-
host 側 : npm run adroid
-
vscode 側 : デバッグ開始 ( attach metro )
もし動かなくなったら
キャッシュ消す
npm start -- --reset-cache
cd android
./gradlew clean
cd ..
npm run adroid
cd ios
xcodebuild clean
cd ..
npm run ios