+2
-1
.eslintrc.js
+2
-1
.eslintrc.js
+2
-1
.prettierrc.js
+2
-1
.prettierrc.js
+2
-2
README.md
+2
-2
README.md
···
7
7
- [React Native](https://reactnative.dev)
8
8
- [React Native for Web](https://necolas.github.io/react-native-web/)
9
9
- [React Navigation](https://reactnative.dev/docs/navigation#react-navigation)
10
-
- (todo) [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
11
-
- (todo) [Async Storage](https://github.com/react-native-async-storage/async-storage)
10
+
- [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
11
+
- [Async Storage](https://github.com/react-native-async-storage/async-storage)
12
12
13
13
## Build instructions
14
14
+7
-7
__tests__/App-test.tsx
+7
-7
__tests__/App-test.tsx
···
2
2
* @format
3
3
*/
4
4
5
-
import 'react-native';
6
-
import React from 'react';
7
-
import App from '../src/App';
5
+
import 'react-native'
6
+
import React from 'react'
7
+
import App from '../src/App'
8
8
9
9
// Note: test renderer must be required after react-native.
10
-
import renderer from 'react-test-renderer';
10
+
import renderer from 'react-test-renderer'
11
11
12
12
it('renders correctly', () => {
13
13
renderer.act(() => {
14
-
renderer.create(<App />);
15
-
});
16
-
});
14
+
renderer.create(<App />)
15
+
})
16
+
})
+1
-1
babel.config.js
+1
-1
babel.config.js
+4
-4
index.native.js
+4
-4
index.native.js
···
2
2
* @format
3
3
*/
4
4
5
-
import {AppRegistry} from 'react-native';
6
-
import App from './src/App';
7
-
import {name as appName} from './src/app.json';
5
+
import {AppRegistry} from 'react-native'
6
+
import App from './src/App'
7
+
import {name as appName} from './src/app.json'
8
8
9
-
AppRegistry.registerComponent(appName, () => App);
9
+
AppRegistry.registerComponent(appName, () => App)
+3
package.json
+3
package.json
···
11
11
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
12
12
},
13
13
"dependencies": {
14
+
"@react-native-async-storage/async-storage": "^1.17.6",
14
15
"@react-navigation/native": "^6.0.10",
15
16
"@react-navigation/native-stack": "^6.6.2",
17
+
"mobx": "^6.6.0",
18
+
"mobx-state-tree": "^5.1.5",
16
19
"react": "17.0.2",
17
20
"react-dom": "17.0.2",
18
21
"react-native": "0.68.2",
+80
src/App.native.tsx
+80
src/App.native.tsx
···
1
+
import React, {useState, useEffect} from 'react'
2
+
import {
3
+
SafeAreaView,
4
+
ScrollView,
5
+
StatusBar,
6
+
Text,
7
+
Button,
8
+
useColorScheme,
9
+
View,
10
+
} from 'react-native'
11
+
import {NavigationContainer} from '@react-navigation/native'
12
+
import {
13
+
createNativeStackNavigator,
14
+
NativeStackScreenProps,
15
+
} from '@react-navigation/native-stack'
16
+
import {RootStore, setupState, RootStoreProvider} from './state'
17
+
18
+
type RootStackParamList = {
19
+
Home: undefined
20
+
Profile: {name: string}
21
+
}
22
+
const Stack = createNativeStackNavigator()
23
+
24
+
const HomeScreen = ({
25
+
navigation,
26
+
}: NativeStackScreenProps<RootStackParamList, 'Home'>) => {
27
+
const isDarkMode = useColorScheme() === 'dark'
28
+
29
+
return (
30
+
<SafeAreaView>
31
+
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
32
+
<ScrollView contentInsetAdjustmentBehavior="automatic">
33
+
<View>
34
+
<Text>Native</Text>
35
+
<Button
36
+
title="Go to Jane's profile"
37
+
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
38
+
/>
39
+
</View>
40
+
</ScrollView>
41
+
</SafeAreaView>
42
+
)
43
+
}
44
+
45
+
const ProfileScreen = ({
46
+
route,
47
+
}: NativeStackScreenProps<RootStackParamList, 'Profile'>) => {
48
+
return <Text>This is {route.params.name}'s profile</Text>
49
+
}
50
+
51
+
function App() {
52
+
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
53
+
54
+
// init
55
+
useEffect(() => {
56
+
setupState().then(setRootStore)
57
+
}, [])
58
+
59
+
// show nothing prior to init
60
+
if (!rootStore) {
61
+
return null
62
+
}
63
+
64
+
return (
65
+
<RootStoreProvider value={rootStore}>
66
+
<NavigationContainer>
67
+
<Stack.Navigator>
68
+
<Stack.Screen
69
+
name="Home"
70
+
component={HomeScreen}
71
+
options={{title: 'Welcome'}}
72
+
/>
73
+
<Stack.Screen name="Profile" component={ProfileScreen} />
74
+
</Stack.Navigator>
75
+
</NavigationContainer>
76
+
</RootStoreProvider>
77
+
)
78
+
}
79
+
80
+
export default App
-112
src/App.tsx
-112
src/App.tsx
···
1
-
/**
2
-
* Sample React Native App
3
-
* https://github.com/facebook/react-native
4
-
*
5
-
* Generated with the TypeScript template
6
-
* https://github.com/react-native-community/react-native-template-typescript
7
-
*
8
-
* @format
9
-
*/
10
-
11
-
import React from 'react';
12
-
import {
13
-
SafeAreaView,
14
-
ScrollView,
15
-
StatusBar,
16
-
StyleSheet,
17
-
Text,
18
-
Button,
19
-
useColorScheme,
20
-
View,
21
-
} from 'react-native';
22
-
import {NavigationContainer} from '@react-navigation/native';
23
-
import {
24
-
createNativeStackNavigator,
25
-
NativeStackScreenProps,
26
-
} from '@react-navigation/native-stack';
27
-
28
-
type RootStackParamList = {
29
-
Home: undefined;
30
-
Profile: {name: string};
31
-
};
32
-
const Stack = createNativeStackNavigator();
33
-
34
-
const Section: React.FC<{
35
-
title: string;
36
-
}> = ({children, title}) => {
37
-
return (
38
-
<View style={styles.sectionContainer}>
39
-
<Text style={styles.sectionTitle}>{title}</Text>
40
-
<Text style={styles.sectionDescription}>{children}</Text>
41
-
</View>
42
-
);
43
-
};
44
-
45
-
const HomeScreen = ({
46
-
navigation,
47
-
}: NativeStackScreenProps<RootStackParamList, 'Home'>) => {
48
-
const isDarkMode = useColorScheme() === 'dark';
49
-
50
-
return (
51
-
<SafeAreaView>
52
-
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
53
-
<ScrollView contentInsetAdjustmentBehavior="automatic">
54
-
<View>
55
-
<Section title="Step One">
56
-
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
57
-
screen and then come back to see your edits.
58
-
<Button
59
-
title="Go to Jane's profile"
60
-
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
61
-
/>
62
-
</Section>
63
-
<Section title="Learn More">
64
-
Read the docs to discover what to do next:
65
-
</Section>
66
-
</View>
67
-
</ScrollView>
68
-
</SafeAreaView>
69
-
);
70
-
};
71
-
72
-
const ProfileScreen = ({
73
-
route,
74
-
}: NativeStackScreenProps<RootStackParamList, 'Profile'>) => {
75
-
return <Text>This is {route.params.name}'s profile</Text>;
76
-
};
77
-
78
-
const App = () => {
79
-
return (
80
-
<NavigationContainer>
81
-
<Stack.Navigator>
82
-
<Stack.Screen
83
-
name="Home"
84
-
component={HomeScreen}
85
-
options={{title: 'Welcome'}}
86
-
/>
87
-
<Stack.Screen name="Profile" component={ProfileScreen} />
88
-
</Stack.Navigator>
89
-
</NavigationContainer>
90
-
);
91
-
};
92
-
93
-
const styles = StyleSheet.create({
94
-
sectionContainer: {
95
-
marginTop: 32,
96
-
paddingHorizontal: 24,
97
-
},
98
-
sectionTitle: {
99
-
fontSize: 24,
100
-
fontWeight: '600',
101
-
},
102
-
sectionDescription: {
103
-
marginTop: 8,
104
-
fontSize: 18,
105
-
fontWeight: '400',
106
-
},
107
-
highlight: {
108
-
fontWeight: '700',
109
-
},
110
-
});
111
-
112
-
export default App;
+80
src/App.web.tsx
+80
src/App.web.tsx
···
1
+
import React, {useState, useEffect} from 'react'
2
+
import {
3
+
SafeAreaView,
4
+
ScrollView,
5
+
StatusBar,
6
+
Text,
7
+
Button,
8
+
useColorScheme,
9
+
View,
10
+
} from 'react-native'
11
+
import {NavigationContainer} from '@react-navigation/native'
12
+
import {
13
+
createNativeStackNavigator,
14
+
NativeStackScreenProps,
15
+
} from '@react-navigation/native-stack'
16
+
import {RootStore, setupState, RootStoreProvider} from './state'
17
+
18
+
type RootStackParamList = {
19
+
Home: undefined
20
+
Profile: {name: string}
21
+
}
22
+
const Stack = createNativeStackNavigator()
23
+
24
+
const HomeScreen = ({
25
+
navigation,
26
+
}: NativeStackScreenProps<RootStackParamList, 'Home'>) => {
27
+
const isDarkMode = useColorScheme() === 'dark'
28
+
29
+
return (
30
+
<SafeAreaView>
31
+
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
32
+
<ScrollView contentInsetAdjustmentBehavior="automatic">
33
+
<View>
34
+
<Text>Web</Text>
35
+
<Button
36
+
title="Go to Jane's profile"
37
+
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
38
+
/>
39
+
</View>
40
+
</ScrollView>
41
+
</SafeAreaView>
42
+
)
43
+
}
44
+
45
+
const ProfileScreen = ({
46
+
route,
47
+
}: NativeStackScreenProps<RootStackParamList, 'Profile'>) => {
48
+
return <Text>This is {route.params.name}'s profile</Text>
49
+
}
50
+
51
+
function App() {
52
+
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
53
+
54
+
// init
55
+
useEffect(() => {
56
+
setupState().then(setRootStore)
57
+
}, [])
58
+
59
+
// show nothing prior to init
60
+
if (!rootStore) {
61
+
return null
62
+
}
63
+
64
+
return (
65
+
<RootStoreProvider value={rootStore}>
66
+
<NavigationContainer>
67
+
<Stack.Navigator>
68
+
<Stack.Screen
69
+
name="Home"
70
+
component={HomeScreen}
71
+
options={{title: 'Welcome'}}
72
+
/>
73
+
<Stack.Screen name="Profile" component={ProfileScreen} />
74
+
</Stack.Navigator>
75
+
</NavigationContainer>
76
+
</RootStoreProvider>
77
+
)
78
+
}
79
+
80
+
export default App
+4
-4
src/index.js
+4
-4
src/index.js
···
2
2
* @format
3
3
*/
4
4
5
-
import {AppRegistry} from 'react-native';
6
-
import App from './App';
5
+
import {AppRegistry} from 'react-native'
6
+
import App from './App'
7
7
8
-
AppRegistry.registerComponent('App', () => App);
8
+
AppRegistry.registerComponent('App', () => App)
9
9
10
10
AppRegistry.runApplication('App', {
11
11
rootTag: document.getElementById('root'),
12
-
});
12
+
})
+27
src/state/env.ts
+27
src/state/env.ts
···
1
+
/**
2
+
* The environment is a place where services and shared dependencies between
3
+
* models live. They are made available to every model via dependency injection.
4
+
*/
5
+
6
+
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
7
+
8
+
export class Environment {
9
+
constructor() {}
10
+
11
+
async setup() {}
12
+
}
13
+
14
+
/**
15
+
* Extension to the MST models that adds the environment property.
16
+
* Usage:
17
+
*
18
+
* .extend(withEnvironment)
19
+
*
20
+
*/
21
+
export const withEnvironment = (self: IStateTreeNode) => ({
22
+
views: {
23
+
get environment() {
24
+
return getEnv<Environment>(self)
25
+
},
26
+
},
27
+
})
+30
src/state/index.ts
+30
src/state/index.ts
···
1
+
import {onSnapshot} from 'mobx-state-tree'
2
+
import {RootStoreModel, RootStore} from './models/root-store'
3
+
import {Environment} from './env'
4
+
import * as storage from './storage'
5
+
6
+
const ROOT_STATE_STORAGE_KEY = 'root'
7
+
8
+
export async function setupState() {
9
+
let rootStore: RootStore
10
+
let data: any
11
+
12
+
const env = new Environment()
13
+
try {
14
+
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
15
+
rootStore = RootStoreModel.create(data, env)
16
+
} catch (e) {
17
+
console.error('Failed to load state from storage', e)
18
+
rootStore = RootStoreModel.create({}, env)
19
+
}
20
+
21
+
// track changes & save to storage
22
+
onSnapshot(rootStore, snapshot =>
23
+
storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
24
+
)
25
+
26
+
return rootStore
27
+
}
28
+
29
+
export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store'
30
+
export type {RootStore} from './models/root-store'
+16
src/state/models/root-store.ts
+16
src/state/models/root-store.ts
···
1
+
/**
2
+
* The root store is the base of all modeled state.
3
+
*/
4
+
5
+
import {Instance, SnapshotOut, types} from 'mobx-state-tree'
6
+
import {createContext, useContext} from 'react'
7
+
8
+
export const RootStoreModel = types.model('RootStore').props({})
9
+
10
+
export interface RootStore extends Instance<typeof RootStoreModel> {}
11
+
export interface RootStoreSnapshot extends SnapshotOut<typeof RootStoreModel> {}
12
+
13
+
// react context & hook utilities
14
+
const RootStoreContext = createContext<RootStore>({} as RootStore)
15
+
export const RootStoreProvider = RootStoreContext.Provider
16
+
export const useStores = () => useContext(RootStoreContext)
+52
src/state/storage.ts
+52
src/state/storage.ts
···
1
+
import AsyncStorage from '@react-native-async-storage/async-storage'
2
+
3
+
export async function loadString(key: string): Promise<string | null> {
4
+
try {
5
+
return await AsyncStorage.getItem(key)
6
+
} catch {
7
+
// not sure why this would fail... even reading the RN docs I'm unclear
8
+
return null
9
+
}
10
+
}
11
+
12
+
export async function saveString(key: string, value: string): Promise<boolean> {
13
+
try {
14
+
await AsyncStorage.setItem(key, value)
15
+
return true
16
+
} catch {
17
+
return false
18
+
}
19
+
}
20
+
21
+
export async function load(key: string): Promise<any | null> {
22
+
try {
23
+
const str = await AsyncStorage.getItem(key)
24
+
if (typeof str !== 'string') {
25
+
return null
26
+
}
27
+
return JSON.parse(str)
28
+
} catch {
29
+
return null
30
+
}
31
+
}
32
+
33
+
export async function save(key: string, value: any): Promise<boolean> {
34
+
try {
35
+
await AsyncStorage.setItem(key, JSON.stringify(value))
36
+
return true
37
+
} catch {
38
+
return false
39
+
}
40
+
}
41
+
42
+
export async function remove(key: string): Promise<void> {
43
+
try {
44
+
await AsyncStorage.removeItem(key)
45
+
} catch {}
46
+
}
47
+
48
+
export async function clear(): Promise<void> {
49
+
try {
50
+
await AsyncStorage.clear()
51
+
} catch {}
52
+
}
+29
yarn.lock
+29
yarn.lock
···
1797
1797
schema-utils "^3.0.0"
1798
1798
source-map "^0.7.3"
1799
1799
1800
+
"@react-native-async-storage/async-storage@^1.17.6":
1801
+
version "1.17.6"
1802
+
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.6.tgz#ddb3520d051f71698c8a0e79e8959a7bf6d9f43b"
1803
+
integrity sha512-XXnoheQI3vQTQmjphdXNLTmtiKZeRqvI8kPQ25X5Eae7nZjdYEEGN+0z8N2qyelbUIQwKgmW0aagJk56q7DyNg==
1804
+
dependencies:
1805
+
merge-options "^3.0.4"
1806
+
1800
1807
"@react-native-community/cli-debugger-ui@^7.0.3":
1801
1808
version "7.0.3"
1802
1809
resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-7.0.3.tgz#3eeeacc5a43513cbcae56e5e965d77726361bcb4"
···
6634
6641
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
6635
6642
integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
6636
6643
6644
+
is-plain-obj@^2.1.0:
6645
+
version "2.1.0"
6646
+
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
6647
+
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
6648
+
6637
6649
is-plain-obj@^3.0.0:
6638
6650
version "3.0.0"
6639
6651
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
···
8128
8140
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
8129
8141
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
8130
8142
8143
+
merge-options@^3.0.4:
8144
+
version "3.0.4"
8145
+
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
8146
+
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
8147
+
dependencies:
8148
+
is-plain-obj "^2.1.0"
8149
+
8131
8150
merge-stream@^2.0.0:
8132
8151
version "2.0.0"
8133
8152
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
···
8505
8524
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
8506
8525
dependencies:
8507
8526
minimist "^1.2.6"
8527
+
8528
+
mobx-state-tree@^5.1.5:
8529
+
version "5.1.5"
8530
+
resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.1.5.tgz#7344d61072705747abb98d23ad21302e38200105"
8531
+
integrity sha512-jugIic0PYWW+nzzYfp4RUy9dec002Z778OC6KzoOyBHnqxupK9iPCsUJYkHjmNRHjZ8E4Z7qQpsKV3At/ntGVw==
8532
+
8533
+
mobx@^6.6.0:
8534
+
version "6.6.0"
8535
+
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.6.0.tgz#617ca1f3b745a781fa89c5eb94a773e3cbeff8ae"
8536
+
integrity sha512-MNTKevLH/6DShLZcmSL351+JgiJPO56A4GUpoiDQ3/yZ0mAtclNLdHK9q4BcQhibx8/JSDupfTpbX2NZPemlRg==
8508
8537
8509
8538
ms@2.0.0:
8510
8539
version "2.0.0"