about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-06-09 13:03:25 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-06-09 13:03:25 -0500
commitd6942bffab68ce80d5cb26b42710dd9276f62ded (patch)
tree309f8d64f95d526d3cae6c00611c93b04f12944e /src
parent92ca49ab9a309510a5503a4df6a0ebcfba30f918 (diff)
downloadvoidsky-d6942bffab68ce80d5cb26b42710dd9276f62ded.tar.zst
Add state management
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx80
-rw-r--r--src/App.tsx112
-rw-r--r--src/App.web.tsx80
-rw-r--r--src/index.js8
-rw-r--r--src/state/env.ts27
-rw-r--r--src/state/index.ts30
-rw-r--r--src/state/models/root-store.ts16
-rw-r--r--src/state/storage.ts52
8 files changed, 289 insertions, 116 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
new file mode 100644
index 000000000..40989caf0
--- /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<RootStackParamList, 'Home'>) => {
+  const isDarkMode = useColorScheme() === 'dark'
+
+  return (
+    <SafeAreaView>
+      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
+      <ScrollView contentInsetAdjustmentBehavior="automatic">
+        <View>
+          <Text>Native</Text>
+          <Button
+            title="Go to Jane's profile"
+            onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
+          />
+        </View>
+      </ScrollView>
+    </SafeAreaView>
+  )
+}
+
+const ProfileScreen = ({
+  route,
+}: NativeStackScreenProps<RootStackParamList, 'Profile'>) => {
+  return <Text>This is {route.params.name}'s profile</Text>
+}
+
+function App() {
+  const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
+
+  // init
+  useEffect(() => {
+    setupState().then(setRootStore)
+  }, [])
+
+  // show nothing prior to init
+  if (!rootStore) {
+    return null
+  }
+
+  return (
+    <RootStoreProvider value={rootStore}>
+      <NavigationContainer>
+        <Stack.Navigator>
+          <Stack.Screen
+            name="Home"
+            component={HomeScreen}
+            options={{title: 'Welcome'}}
+          />
+          <Stack.Screen name="Profile" component={ProfileScreen} />
+        </Stack.Navigator>
+      </NavigationContainer>
+    </RootStoreProvider>
+  )
+}
+
+export default App
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index e0a0241bb..000000000
--- 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 (
-    <View style={styles.sectionContainer}>
-      <Text style={styles.sectionTitle}>{title}</Text>
-      <Text style={styles.sectionDescription}>{children}</Text>
-    </View>
-  );
-};
-
-const HomeScreen = ({
-  navigation,
-}: NativeStackScreenProps<RootStackParamList, 'Home'>) => {
-  const isDarkMode = useColorScheme() === 'dark';
-
-  return (
-    <SafeAreaView>
-      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
-      <ScrollView contentInsetAdjustmentBehavior="automatic">
-        <View>
-          <Section title="Step One">
-            Edit <Text style={styles.highlight}>App.tsx</Text> to change this
-            screen and then come back to see your edits.
-            <Button
-              title="Go to Jane's profile"
-              onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
-            />
-          </Section>
-          <Section title="Learn More">
-            Read the docs to discover what to do next:
-          </Section>
-        </View>
-      </ScrollView>
-    </SafeAreaView>
-  );
-};
-
-const ProfileScreen = ({
-  route,
-}: NativeStackScreenProps<RootStackParamList, 'Profile'>) => {
-  return <Text>This is {route.params.name}'s profile</Text>;
-};
-
-const App = () => {
-  return (
-    <NavigationContainer>
-      <Stack.Navigator>
-        <Stack.Screen
-          name="Home"
-          component={HomeScreen}
-          options={{title: 'Welcome'}}
-        />
-        <Stack.Screen name="Profile" component={ProfileScreen} />
-      </Stack.Navigator>
-    </NavigationContainer>
-  );
-};
-
-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 000000000..18b15821b
--- /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<RootStackParamList, 'Home'>) => {
+  const isDarkMode = useColorScheme() === 'dark'
+
+  return (
+    <SafeAreaView>
+      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
+      <ScrollView contentInsetAdjustmentBehavior="automatic">
+        <View>
+          <Text>Web</Text>
+          <Button
+            title="Go to Jane's profile"
+            onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
+          />
+        </View>
+      </ScrollView>
+    </SafeAreaView>
+  )
+}
+
+const ProfileScreen = ({
+  route,
+}: NativeStackScreenProps<RootStackParamList, 'Profile'>) => {
+  return <Text>This is {route.params.name}'s profile</Text>
+}
+
+function App() {
+  const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
+
+  // init
+  useEffect(() => {
+    setupState().then(setRootStore)
+  }, [])
+
+  // show nothing prior to init
+  if (!rootStore) {
+    return null
+  }
+
+  return (
+    <RootStoreProvider value={rootStore}>
+      <NavigationContainer>
+        <Stack.Navigator>
+          <Stack.Screen
+            name="Home"
+            component={HomeScreen}
+            options={{title: 'Welcome'}}
+          />
+          <Stack.Screen name="Profile" component={ProfileScreen} />
+        </Stack.Navigator>
+      </NavigationContainer>
+    </RootStoreProvider>
+  )
+}
+
+export default App
diff --git a/src/index.js b/src/index.js
index 52c30a178..45a06f40a 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 000000000..90a2cab5e
--- /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<Environment>(self)
+    },
+  },
+})
diff --git a/src/state/index.ts b/src/state/index.ts
new file mode 100644
index 000000000..7c97ce294
--- /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 000000000..164dfcced
--- /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<typeof RootStoreModel> {}
+export interface RootStoreSnapshot extends SnapshotOut<typeof RootStoreModel> {}
+
+// react context & hook utilities
+const RootStoreContext = createContext<RootStore>({} 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 000000000..dc5fb620f
--- /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<string | null> {
+  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<boolean> {
+  try {
+    await AsyncStorage.setItem(key, value)
+    return true
+  } catch {
+    return false
+  }
+}
+
+export async function load(key: string): Promise<any | null> {
+  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<boolean> {
+  try {
+    await AsyncStorage.setItem(key, JSON.stringify(value))
+    return true
+  } catch {
+    return false
+  }
+}
+
+export async function remove(key: string): Promise<void> {
+  try {
+    await AsyncStorage.removeItem(key)
+  } catch {}
+}
+
+export async function clear(): Promise<void> {
+  try {
+    await AsyncStorage.clear()
+  } catch {}
+}