Add state management

This commit is contained in:
Paul Frazee 2022-06-09 13:03:25 -05:00
parent 92ca49ab9a
commit d6942bffab
17 changed files with 340 additions and 133 deletions

80
src/App.native.tsx Normal file
View file

@ -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

View file

@ -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;

80
src/App.web.tsx Normal file
View file

@ -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

View file

@ -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'),
});
})

27
src/state/env.ts Normal file
View file

@ -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)
},
},
})

30
src/state/index.ts Normal file
View file

@ -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'

View file

@ -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)

52
src/state/storage.ts Normal file
View file

@ -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 {}
}