diff --git a/.eslintrc.js b/.eslintrc.js
index 4acd8ff8..898ffe6d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -10,7 +10,8 @@ module.exports = {
'@typescript-eslint/no-shadow': 'off',
'no-shadow': 'off',
'no-undef': 'off',
+ semi: [2, 'never'],
},
},
],
-};
+}
diff --git a/.prettierrc.js b/.prettierrc.js
index 2b540746..15ad6f42 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -1,7 +1,8 @@
module.exports = {
+ semi: false,
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
singleQuote: true,
trailingComma: 'all',
-};
+}
diff --git a/README.md b/README.md
index 352fc769..81f53001 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@ Uses:
- [React Native](https://reactnative.dev)
- [React Native for Web](https://necolas.github.io/react-native-web/)
- [React Navigation](https://reactnative.dev/docs/navigation#react-navigation)
-- (todo) [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
-- (todo) [Async Storage](https://github.com/react-native-async-storage/async-storage)
+- [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
+- [Async Storage](https://github.com/react-native-async-storage/async-storage)
## Build instructions
diff --git a/__tests__/App-test.tsx b/__tests__/App-test.tsx
index dab45f32..47060512 100644
--- a/__tests__/App-test.tsx
+++ b/__tests__/App-test.tsx
@@ -2,15 +2,15 @@
* @format
*/
-import 'react-native';
-import React from 'react';
-import App from '../src/App';
+import 'react-native'
+import React from 'react'
+import App from '../src/App'
// Note: test renderer must be required after react-native.
-import renderer from 'react-test-renderer';
+import renderer from 'react-test-renderer'
it('renders correctly', () => {
renderer.act(() => {
- renderer.create();
- });
-});
+ renderer.create()
+ })
+})
diff --git a/babel.config.js b/babel.config.js
index f842b77f..cf1f9fbb 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,3 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
-};
+}
diff --git a/index.native.js b/index.native.js
index c3999052..2d2c9ca4 100644
--- a/index.native.js
+++ b/index.native.js
@@ -2,8 +2,8 @@
* @format
*/
-import {AppRegistry} from 'react-native';
-import App from './src/App';
-import {name as appName} from './src/app.json';
+import {AppRegistry} from 'react-native'
+import App from './src/App'
+import {name as appName} from './src/app.json'
-AppRegistry.registerComponent(appName, () => App);
+AppRegistry.registerComponent(appName, () => App)
diff --git a/metro.config.js b/metro.config.js
index e91aba93..c81b3ca1 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -14,4 +14,4 @@ module.exports = {
},
}),
},
-};
+}
diff --git a/package.json b/package.json
index d6218613..346cbcc9 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,11 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
+ "@react-native-async-storage/async-storage": "^1.17.6",
"@react-navigation/native": "^6.0.10",
"@react-navigation/native-stack": "^6.6.2",
+ "mobx": "^6.6.0",
+ "mobx-state-tree": "^5.1.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-native": "0.68.2",
diff --git a/src/App.native.tsx b/src/App.native.tsx
new file mode 100644
index 00000000..40989caf
--- /dev/null
+++ b/src/App.native.tsx
@@ -0,0 +1,80 @@
+import React, {useState, useEffect} from 'react'
+import {
+ SafeAreaView,
+ ScrollView,
+ StatusBar,
+ Text,
+ Button,
+ useColorScheme,
+ View,
+} from 'react-native'
+import {NavigationContainer} from '@react-navigation/native'
+import {
+ createNativeStackNavigator,
+ NativeStackScreenProps,
+} from '@react-navigation/native-stack'
+import {RootStore, setupState, RootStoreProvider} from './state'
+
+type RootStackParamList = {
+ Home: undefined
+ Profile: {name: string}
+}
+const Stack = createNativeStackNavigator()
+
+const HomeScreen = ({
+ navigation,
+}: NativeStackScreenProps) => {
+ const isDarkMode = useColorScheme() === 'dark'
+
+ return (
+
+
+
+
+ Native
+
+
+
+ )
+}
+
+const ProfileScreen = ({
+ route,
+}: NativeStackScreenProps) => {
+ return This is {route.params.name}'s profile
+}
+
+function App() {
+ const [rootStore, setRootStore] = useState(undefined)
+
+ // init
+ useEffect(() => {
+ setupState().then(setRootStore)
+ }, [])
+
+ // show nothing prior to init
+ if (!rootStore) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index e0a0241b..00000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * Sample React Native App
- * https://github.com/facebook/react-native
- *
- * Generated with the TypeScript template
- * https://github.com/react-native-community/react-native-template-typescript
- *
- * @format
- */
-
-import React from 'react';
-import {
- SafeAreaView,
- ScrollView,
- StatusBar,
- StyleSheet,
- Text,
- Button,
- useColorScheme,
- View,
-} from 'react-native';
-import {NavigationContainer} from '@react-navigation/native';
-import {
- createNativeStackNavigator,
- NativeStackScreenProps,
-} from '@react-navigation/native-stack';
-
-type RootStackParamList = {
- Home: undefined;
- Profile: {name: string};
-};
-const Stack = createNativeStackNavigator();
-
-const Section: React.FC<{
- title: string;
-}> = ({children, title}) => {
- return (
-
- {title}
- {children}
-
- );
-};
-
-const HomeScreen = ({
- navigation,
-}: NativeStackScreenProps) => {
- const isDarkMode = useColorScheme() === 'dark';
-
- return (
-
-
-
-
-
- Edit App.tsx to change this
- screen and then come back to see your edits.
-
-
- Read the docs to discover what to do next:
-
-
-
-
- );
-};
-
-const ProfileScreen = ({
- route,
-}: NativeStackScreenProps) => {
- return This is {route.params.name}'s profile;
-};
-
-const App = () => {
- return (
-
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- sectionContainer: {
- marginTop: 32,
- paddingHorizontal: 24,
- },
- sectionTitle: {
- fontSize: 24,
- fontWeight: '600',
- },
- sectionDescription: {
- marginTop: 8,
- fontSize: 18,
- fontWeight: '400',
- },
- highlight: {
- fontWeight: '700',
- },
-});
-
-export default App;
diff --git a/src/App.web.tsx b/src/App.web.tsx
new file mode 100644
index 00000000..18b15821
--- /dev/null
+++ b/src/App.web.tsx
@@ -0,0 +1,80 @@
+import React, {useState, useEffect} from 'react'
+import {
+ SafeAreaView,
+ ScrollView,
+ StatusBar,
+ Text,
+ Button,
+ useColorScheme,
+ View,
+} from 'react-native'
+import {NavigationContainer} from '@react-navigation/native'
+import {
+ createNativeStackNavigator,
+ NativeStackScreenProps,
+} from '@react-navigation/native-stack'
+import {RootStore, setupState, RootStoreProvider} from './state'
+
+type RootStackParamList = {
+ Home: undefined
+ Profile: {name: string}
+}
+const Stack = createNativeStackNavigator()
+
+const HomeScreen = ({
+ navigation,
+}: NativeStackScreenProps) => {
+ const isDarkMode = useColorScheme() === 'dark'
+
+ return (
+
+
+
+
+ Web
+
+
+
+ )
+}
+
+const ProfileScreen = ({
+ route,
+}: NativeStackScreenProps) => {
+ return This is {route.params.name}'s profile
+}
+
+function App() {
+ const [rootStore, setRootStore] = useState(undefined)
+
+ // init
+ useEffect(() => {
+ setupState().then(setRootStore)
+ }, [])
+
+ // show nothing prior to init
+ if (!rootStore) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/src/index.js b/src/index.js
index 52c30a17..45a06f40 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,11 +2,11 @@
* @format
*/
-import {AppRegistry} from 'react-native';
-import App from './App';
+import {AppRegistry} from 'react-native'
+import App from './App'
-AppRegistry.registerComponent('App', () => App);
+AppRegistry.registerComponent('App', () => App)
AppRegistry.runApplication('App', {
rootTag: document.getElementById('root'),
-});
+})
diff --git a/src/state/env.ts b/src/state/env.ts
new file mode 100644
index 00000000..90a2cab5
--- /dev/null
+++ b/src/state/env.ts
@@ -0,0 +1,27 @@
+/**
+ * The environment is a place where services and shared dependencies between
+ * models live. They are made available to every model via dependency injection.
+ */
+
+import {getEnv, IStateTreeNode} from 'mobx-state-tree'
+
+export class Environment {
+ constructor() {}
+
+ async setup() {}
+}
+
+/**
+ * Extension to the MST models that adds the environment property.
+ * Usage:
+ *
+ * .extend(withEnvironment)
+ *
+ */
+export const withEnvironment = (self: IStateTreeNode) => ({
+ views: {
+ get environment() {
+ return getEnv(self)
+ },
+ },
+})
diff --git a/src/state/index.ts b/src/state/index.ts
new file mode 100644
index 00000000..7c97ce29
--- /dev/null
+++ b/src/state/index.ts
@@ -0,0 +1,30 @@
+import {onSnapshot} from 'mobx-state-tree'
+import {RootStoreModel, RootStore} from './models/root-store'
+import {Environment} from './env'
+import * as storage from './storage'
+
+const ROOT_STATE_STORAGE_KEY = 'root'
+
+export async function setupState() {
+ let rootStore: RootStore
+ let data: any
+
+ const env = new Environment()
+ try {
+ data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
+ rootStore = RootStoreModel.create(data, env)
+ } catch (e) {
+ console.error('Failed to load state from storage', e)
+ rootStore = RootStoreModel.create({}, env)
+ }
+
+ // track changes & save to storage
+ onSnapshot(rootStore, snapshot =>
+ storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
+ )
+
+ return rootStore
+}
+
+export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store'
+export type {RootStore} from './models/root-store'
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
new file mode 100644
index 00000000..164dfcce
--- /dev/null
+++ b/src/state/models/root-store.ts
@@ -0,0 +1,16 @@
+/**
+ * The root store is the base of all modeled state.
+ */
+
+import {Instance, SnapshotOut, types} from 'mobx-state-tree'
+import {createContext, useContext} from 'react'
+
+export const RootStoreModel = types.model('RootStore').props({})
+
+export interface RootStore extends Instance {}
+export interface RootStoreSnapshot extends SnapshotOut {}
+
+// react context & hook utilities
+const RootStoreContext = createContext({} as RootStore)
+export const RootStoreProvider = RootStoreContext.Provider
+export const useStores = () => useContext(RootStoreContext)
diff --git a/src/state/storage.ts b/src/state/storage.ts
new file mode 100644
index 00000000..dc5fb620
--- /dev/null
+++ b/src/state/storage.ts
@@ -0,0 +1,52 @@
+import AsyncStorage from '@react-native-async-storage/async-storage'
+
+export async function loadString(key: string): Promise {
+ try {
+ return await AsyncStorage.getItem(key)
+ } catch {
+ // not sure why this would fail... even reading the RN docs I'm unclear
+ return null
+ }
+}
+
+export async function saveString(key: string, value: string): Promise {
+ try {
+ await AsyncStorage.setItem(key, value)
+ return true
+ } catch {
+ return false
+ }
+}
+
+export async function load(key: string): Promise {
+ try {
+ const str = await AsyncStorage.getItem(key)
+ if (typeof str !== 'string') {
+ return null
+ }
+ return JSON.parse(str)
+ } catch {
+ return null
+ }
+}
+
+export async function save(key: string, value: any): Promise {
+ try {
+ await AsyncStorage.setItem(key, JSON.stringify(value))
+ return true
+ } catch {
+ return false
+ }
+}
+
+export async function remove(key: string): Promise {
+ try {
+ await AsyncStorage.removeItem(key)
+ } catch {}
+}
+
+export async function clear(): Promise {
+ try {
+ await AsyncStorage.clear()
+ } catch {}
+}
diff --git a/yarn.lock b/yarn.lock
index d6892a77..0001c937 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1797,6 +1797,13 @@
schema-utils "^3.0.0"
source-map "^0.7.3"
+"@react-native-async-storage/async-storage@^1.17.6":
+ version "1.17.6"
+ resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.6.tgz#ddb3520d051f71698c8a0e79e8959a7bf6d9f43b"
+ integrity sha512-XXnoheQI3vQTQmjphdXNLTmtiKZeRqvI8kPQ25X5Eae7nZjdYEEGN+0z8N2qyelbUIQwKgmW0aagJk56q7DyNg==
+ dependencies:
+ merge-options "^3.0.4"
+
"@react-native-community/cli-debugger-ui@^7.0.3":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-7.0.3.tgz#3eeeacc5a43513cbcae56e5e965d77726361bcb4"
@@ -6634,6 +6641,11 @@ is-obj@^1.0.1:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
+is-plain-obj@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+ integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
is-plain-obj@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
@@ -8128,6 +8140,13 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+merge-options@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
+ integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
+ dependencies:
+ is-plain-obj "^2.1.0"
+
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -8506,6 +8525,16 @@ mkdirp@^0.5.1, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.6"
+mobx-state-tree@^5.1.5:
+ version "5.1.5"
+ resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.1.5.tgz#7344d61072705747abb98d23ad21302e38200105"
+ integrity sha512-jugIic0PYWW+nzzYfp4RUy9dec002Z778OC6KzoOyBHnqxupK9iPCsUJYkHjmNRHjZ8E4Z7qQpsKV3At/ntGVw==
+
+mobx@^6.6.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.6.0.tgz#617ca1f3b745a781fa89c5eb94a773e3cbeff8ae"
+ integrity sha512-MNTKevLH/6DShLZcmSL351+JgiJPO56A4GUpoiDQ3/yZ0mAtclNLdHK9q4BcQhibx8/JSDupfTpbX2NZPemlRg==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"