diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-06-09 13:03:25 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-06-09 13:03:25 -0500 |
commit | d6942bffab68ce80d5cb26b42710dd9276f62ded (patch) | |
tree | 309f8d64f95d526d3cae6c00611c93b04f12944e /src | |
parent | 92ca49ab9a309510a5503a4df6a0ebcfba30f918 (diff) | |
download | voidsky-d6942bffab68ce80d5cb26b42710dd9276f62ded.tar.zst |
Add state management
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 80 | ||||
-rw-r--r-- | src/App.tsx | 112 | ||||
-rw-r--r-- | src/App.web.tsx | 80 | ||||
-rw-r--r-- | src/index.js | 8 | ||||
-rw-r--r-- | src/state/env.ts | 27 | ||||
-rw-r--r-- | src/state/index.ts | 30 | ||||
-rw-r--r-- | src/state/models/root-store.ts | 16 | ||||
-rw-r--r-- | src/state/storage.ts | 52 |
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 {} +} |