Add state management
This commit is contained in:
parent
92ca49ab9a
commit
d6942bffab
17 changed files with 340 additions and 133 deletions
80
src/App.native.tsx
Normal file
80
src/App.native.tsx
Normal 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
|
112
src/App.tsx
112
src/App.tsx
|
@ -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
80
src/App.web.tsx
Normal 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
|
|
@ -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
27
src/state/env.ts
Normal 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
30
src/state/index.ts
Normal 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'
|
16
src/state/models/root-store.ts
Normal file
16
src/state/models/root-store.ts
Normal 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
52
src/state/storage.ts
Normal 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 {}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue