├── .watchmanconfig
├── renovate.json
├── src
├── constants.ts
├── states
│ ├── saga.ts
│ ├── sample
│ │ ├── sample-actions.ts
│ │ ├── sample-saga.ts
│ │ ├── sample-reducer.ts
│ │ └── sample.test.ts
│ └── store.ts
└── environments.ts
├── assets
├── icon.png
├── splash.png
└── screenshot.png
├── .gitignore
├── .vscode
└── settings.json
├── babel.config.js
├── .expo-shared
└── assets.json
├── README.md
├── app.json
├── tsconfig.json
├── package.json
├── tslint.json
└── App.tsx
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const SampleConstants = {
2 | hoge: "fuga",
3 | };
4 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tawachan/react-native-expo-boilerplate/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tawachan/react-native-expo-boilerplate/HEAD/assets/splash.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "prettier.tslintIntegration": true
4 | }
5 |
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tawachan/react-native-expo-boilerplate/HEAD/assets/screenshot.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | plugins: []
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/states/saga.ts:
--------------------------------------------------------------------------------
1 | import { all, fork } from "redux-saga/effects";
2 | import { sampleSagaWorkers } from "./sample/sample-saga";
3 |
4 | export function* sagaFunctions() {
5 | yield all([fork(sampleSagaWorkers)]);
6 | }
7 |
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "7adc6cdde1172c646f8dda7fcb1186d148e59e6d2a40774bd7e03281a653f19c": true,
3 | "5efae8e1083638bdf1ee77188f2b1ebdab54f729a0fec67a418649c7840d0f47": true,
4 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true
5 | }
--------------------------------------------------------------------------------
/src/states/sample/sample-actions.ts:
--------------------------------------------------------------------------------
1 | import actionCreatorFactory from "typescript-fsa";
2 |
3 | const actionCreator = actionCreatorFactory("SAMPLE");
4 |
5 | export const sampleActions = {
6 | fetchSamplesAsync: actionCreator.async<{}, { text: string }>("FETCH_SAMPLES_ASYNC"),
7 | fetchSamplesSync: actionCreator<{}>("FETCH_SAMPLES_SYNC"),
8 | };
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-expo-boilerplate
2 |
3 | This is a boilerplate to develop a mobile app with React Native and Expo(v33) using TypeScript and Redux (Redux Saga).
4 |
5 | Things already configured:
6 |
7 | - Redux
8 | - Redux Saga
9 | - tslint
10 | - prettier
11 | - jest
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-expo-boilerplate",
4 | "slug": "react-native-expo-boilerplate",
5 | "privacy": "public",
6 | "sdkVersion": "33.0.0",
7 | "platforms": ["ios", "android"],
8 | "version": "1.0.0",
9 | "orientation": "portrait",
10 | "icon": "./assets/icon.png",
11 | "splash": {
12 | "image": "./assets/splash.png",
13 | "resizeMode": "contain",
14 | "backgroundColor": "#ffffff"
15 | },
16 | "updates": {
17 | "fallbackToCacheTimeout": 0
18 | },
19 | "assetBundlePatterns": ["**/*"],
20 | "ios": {
21 | "supportsTablet": true
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/states/store.ts:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, combineReducers, createStore } from "redux";
2 | import createSagaMiddleware from "redux-saga";
3 | import { composeWithDevTools } from "remote-redux-devtools";
4 | import { sagaFunctions } from "./saga";
5 | import { sampleReducer, SampleState } from "./sample/sample-reducer";
6 |
7 | const sagaMiddleware = createSagaMiddleware();
8 | const composeEnhancers = composeWithDevTools(applyMiddleware(sagaMiddleware));
9 |
10 | export interface AppState {
11 | samples: SampleState;
12 | }
13 |
14 | export const store = createStore(
15 | combineReducers({
16 | samples: sampleReducer,
17 | }),
18 | composeEnhancers
19 | );
20 |
21 | sagaMiddleware.run(sagaFunctions);
22 |
--------------------------------------------------------------------------------
/src/states/sample/sample-saga.ts:
--------------------------------------------------------------------------------
1 | import { SagaIterator } from "redux-saga";
2 | import { takeLatest } from "redux-saga/effects";
3 | import { Action } from "typescript-fsa";
4 | import { bindAsyncAction } from "typescript-fsa-redux-saga-extended";
5 | import { sampleActions } from "./sample-actions";
6 |
7 | const sampleFetchSamplesAsyncWorker = bindAsyncAction(sampleActions.fetchSamplesAsync)(function*(
8 | _: Action<{}>
9 | ): SagaIterator {
10 | // const response = yield call(asyncFunction, someInputs);
11 | return { text: "new value generated by asynchronized action" };
12 | });
13 |
14 | export function* sampleSagaWorkers() {
15 | yield takeLatest(sampleActions.fetchSamplesAsync.started, sampleFetchSamplesAsyncWorker);
16 | }
17 |
--------------------------------------------------------------------------------
/src/states/sample/sample-reducer.ts:
--------------------------------------------------------------------------------
1 | import { reducerWithInitialState } from "typescript-fsa-reducers";
2 | import { sampleActions } from "./sample-actions";
3 |
4 | export interface SampleState {
5 | text: string;
6 | }
7 |
8 | export const initialState: SampleState = {
9 | text: "initial value",
10 | };
11 |
12 | export const sampleReducer = reducerWithInitialState(initialState)
13 | .case(sampleActions.fetchSamplesAsync.done, (state, payload) => {
14 | return Object.assign({}, state, {
15 | text: payload.result.text,
16 | });
17 | })
18 | .case(sampleActions.fetchSamplesSync, (state, payload) => {
19 | return Object.assign({}, state, {
20 | text: "new value generated by synchronized action",
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "forceConsistentCasingInFileNames": false,
5 | "declaration": false,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "strictNullChecks": true,
9 | "baseUrl": ".",
10 | "jsx": "react-native",
11 | "lib": ["dom", "es2017", "es2018"],
12 | "module": "es2015",
13 | "moduleResolution": "node",
14 | "sourceMap": true,
15 | "target": "es2015",
16 | "paths": {
17 | "@app/env": ["environments/environment"]
18 | }
19 | },
20 | "include": ["src/**/*", "App.tsx"],
21 | "exclude": ["node_modules"],
22 | "compileOnSave": false,
23 | "atom": {
24 | "rewriteTsconfig": false
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/states/sample/sample.test.ts:
--------------------------------------------------------------------------------
1 | import { sampleActions as actions } from "./sample-actions";
2 | import { initialState, sampleReducer as reducer } from "./sample-reducer";
3 |
4 | describe("SAMPLE", () => {
5 | describe("FETCH_SAMPLES_ASYNC", () => {
6 | const action = {
7 | type: actions.fetchSamplesAsync.done.type,
8 | payload: { result: { text: "Async" } },
9 | };
10 |
11 | it("checks value is Async", () => {
12 | const state = reducer(initialState, action);
13 | expect(state.text).toEqual("Async");
14 | });
15 | });
16 |
17 | describe("FETCH_SAMPLES_SYNC", () => {
18 | const action = {
19 | type: actions.fetchSamplesSync.type,
20 | payload: { text: "Sync" },
21 | };
22 |
23 | it("checks value is Sync", () => {
24 | const state = reducer(initialState, action);
25 | expect(state.text).toEqual("Sync");
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/environments.ts:
--------------------------------------------------------------------------------
1 | import { Constants } from "expo";
2 |
3 | const ENVs = {
4 | dev: {
5 | environment: "development" as "development" | "staging" | "production",
6 | apiUrl: "http://localhost:3000",
7 | },
8 | staging: {
9 | environment: "staging" as "development" | "staging" | "production",
10 | apiUrl: "https://staging.com",
11 | },
12 | production: {
13 | environment: "production" as "development" | "staging" | "production",
14 | apiUrl: "https://production.com",
15 | },
16 | };
17 |
18 | function getEnvVars() {
19 | const options = Constants.manifest.packagerOpts;
20 | const channel = Constants.manifest.releaseChannel;
21 | const isDev = options != null ? options.dev : true;
22 | if (isDev) {
23 | return ENVs.dev;
24 | } else {
25 | if (channel === "production") {
26 | return ENVs.production;
27 | } else {
28 | return ENVs.staging;
29 | }
30 | }
31 | }
32 |
33 | export const ENV = getEnvVars();
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "eject": "expo eject",
8 | "tslint": "tslint --project ./ --fix",
9 | "test": "jest"
10 | },
11 | "dependencies": {
12 | "expo": "^33.0.0",
13 | "react": "16.8.3",
14 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz",
15 | "react-redux": "^6.0.0",
16 | "redux": "^4.0.1",
17 | "redux-saga": "^0.16.0",
18 | "tslib": "1.9.3",
19 | "typescript-fsa-reducers": "^1.2.0",
20 | "typescript-fsa-redux-saga-extended": "^1.0.6"
21 | },
22 | "devDependencies": {
23 | "@types/expo": "32.0.13",
24 | "@types/expo__vector-icons": "9.0.0",
25 | "@types/jest": "^23.3.13",
26 | "@types/react": "16.7.22",
27 | "@types/react-native": "0.57.32",
28 | "@types/react-redux": "^7.0.1",
29 | "babel-preset-expo": "^5.0.0",
30 | "jest": "^24.0.0",
31 | "prettier": "^1.16.3",
32 | "remote-redux-devtools": "^0.5.16",
33 | "tslint": "^5.12.1",
34 | "tslint-config-prettier": "^1.17.0",
35 | "tslint-plugin-prettier": "^2.0.1",
36 | "typescript": "3.3.1"
37 | },
38 | "private": true
39 | }
40 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-config-prettier"],
3 | "rulesDirectory": ["tslint-plugin-prettier"],
4 | "rules": {
5 | "prettier": [
6 | true,
7 | {
8 | "singleQuote": false,
9 | "trailingComma": "es5",
10 | "printWidth": 120
11 | }
12 | ],
13 | "no-duplicate-variable": true,
14 | "no-angle-bracket-type-assertion": false,
15 | "no-reference": false,
16 | "curly": true,
17 | "indent": [true, "spaces"],
18 | "no-conditional-assignment": true,
19 | "no-debugger": true,
20 | "no-console": [true, "log", "debug", "info", "time", "timeEnd", "trace"],
21 | "no-consecutive-blank-lines": true,
22 | "no-eval": true,
23 | "no-namespace": false,
24 | "no-switch-case-fall-through": true,
25 | "no-trailing-whitespace": true,
26 | "no-unused-expression": true,
27 | "no-use-before-declare": false,
28 | "no-use-before-define": ["error", { "functions": false, "classes": true }],
29 | "no-shadowed-variable": false,
30 | "one-line": [
31 | true,
32 | "check-catch",
33 | "check-else",
34 | "check-open-brace",
35 | "check-whitespace"
36 | ],
37 | "semicolon": [true, "always", "ignore-bound-class-methods"],
38 | "switch-default": true,
39 | "triple-equals": [true, "allow-null-check"],
40 | "object-literal-sort-keys": false,
41 | "interface-name": false,
42 | "variable-name": [
43 | true,
44 | "ban-keywords",
45 | "check-format",
46 | "allow-leading-underscore",
47 | "allow-pascal-case",
48 | "allow-snake-case"
49 | ],
50 | "whitespace": [
51 | true,
52 | "check-branch",
53 | "check-decl",
54 | "check-operator",
55 | "check-module",
56 | "check-separator",
57 | "check-type",
58 | "check-typecast"
59 | ],
60 | "max-line-length": [true, 170],
61 | "max-classes-per-file": [true, 2]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch } from "react";
2 | import { Button, StyleSheet, Text, View } from "react-native";
3 | import { connect, Provider } from "react-redux";
4 | import { Action } from "typescript-fsa";
5 | import { sampleActions } from "./src/states/sample/sample-actions";
6 | import { SampleState } from "./src/states/sample/sample-reducer";
7 | import { AppState, store } from "./src/states/store";
8 |
9 | interface BaseProps {
10 | samples: SampleState;
11 | fetchSamplesAsync: () => Action;
12 | fetchSamplesSync: () => Action;
13 | }
14 |
15 | export default class App extends React.Component<{}> {
16 | public render() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | class Base extends React.Component {
28 | public render() {
29 | const { fetchSamplesAsync, fetchSamplesSync, samples } = this.props;
30 | return (
31 |
38 |
47 | Thank you for trying
48 | react-native-expo-boilerplate
49 | with TypeScript!
50 |
51 | This boilerplate already implements
52 | the libraries below:
53 |
54 | - Redux
55 | - Redux Saga
56 | - tslint
57 | - prettier
58 |
59 |
60 |
78 | );
79 | }
80 | }
81 |
82 | const styles = StyleSheet.create({
83 | container: {
84 | flex: 1,
85 | backgroundColor: "#fff",
86 | alignItems: "center",
87 | justifyContent: "center",
88 | },
89 | });
90 |
91 | function mapStateToProps(state: AppState) {
92 | return {
93 | samples: state.samples,
94 | };
95 | }
96 |
97 | function mapDispatchToProps(dispatch: Dispatch>) {
98 | return {
99 | fetchSamplesAsync() {
100 | dispatch(sampleActions.fetchSamplesAsync.started({}));
101 | },
102 | fetchSamplesSync() {
103 | dispatch(sampleActions.fetchSamplesSync({}));
104 | },
105 | };
106 | }
107 |
108 | const ConnectedBase = connect(
109 | mapStateToProps,
110 | mapDispatchToProps
111 | )(Base);
112 |
--------------------------------------------------------------------------------