Last active
September 26, 2024 05:02
-
-
Save tranquan/7c219eda2a24165e21000cdbe840a9ad to your computer and use it in GitHub Desktop.
Strong typed-check event listener, can be use to passing callback between scenes with react-navigation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { StatusBar } from "expo-status-bar"; | |
import React from "react"; | |
import { StyleSheet, Text, View } from "react-native"; | |
import { NavigationContainer } from "@react-navigation/native"; | |
import { createNativeStackNavigator } from "@react-navigation/native-stack"; | |
import ProfileScene from "./ProfileScene"; | |
import CountryPickerScene from "./CountryPickerScene"; | |
const Stack = createNativeStackNavigator(); | |
export default function App() { | |
return ( | |
<NavigationContainer> | |
<Stack.Navigator> | |
<Stack.Screen name="Profile" component={ProfileScene} /> | |
<Stack.Screen name="CountryPicker" component={CountryPickerScene} /> | |
</Stack.Navigator> | |
</NavigationContainer> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NavigationProp, RouteProp, useNavigation, useRoute } from "@react-navigation/native"; | |
import React from "react"; | |
import { View, StyleSheet, Text, Pressable, Touchable, TouchableOpacity } from "react-native"; | |
import { makeEventNotifier } from "../hooks/useEventListener"; | |
import EventEmitter from "../services/EventEmitter"; | |
export function showCountryPickerOld( | |
navigation: NavigationProp<any, any>, | |
country: string, | |
onCountrySelected: (country: string) => void | |
) { | |
navigation.navigate("CountryPicker", { country: country, onCountrySelected: onCountrySelected }); | |
} | |
export function showCountryPicker(navigation: NavigationProp<any, any>, country: string) { | |
navigation.navigate("CountryPicker", { country: country }); | |
} | |
const COUNTRIES = [ | |
"Australia", | |
"France", | |
"Finland", | |
"Germany", | |
"Italy", | |
"Japan", | |
"Netherland", | |
"Norway", | |
"Poland", | |
"Spain", | |
"Sweden", | |
"United Kingdom", | |
"United States", | |
]; | |
type CountryPickerSceneParams = { | |
CountryPickerScene: { | |
country: string; | |
onCountrySelected?: (country: string) => void; | |
}; | |
}; | |
const notifer = makeEventNotifier<{ country: string }>("OnCountrySelected"); | |
// Youy can add a snippet to generate this | |
export function useCountryPickerListener(listener: typeof notifer.notify, deps: ReadonlyArray<any>) { | |
notifer.useEventListener(listener, deps); | |
} | |
const CountryPickerScene: React.FC<{}> = () => { | |
const navigation = useNavigation(); | |
const route = useRoute<RouteProp<CountryPickerSceneParams>>(); | |
const country = route.params?.country ?? ""; | |
const handleOnCountrySelected = (country: string) => { | |
notifer.notify({ country: country }); | |
navigation.goBack(); | |
}; | |
// Opt 1: use event emitter | |
// const onCountrySelected = route.params?.onCountrySelected; | |
// const handleOnCountrySelectedOld = (country: string) => { | |
// if (onCountrySelected) { | |
// onCountrySelected(country); | |
// } else { | |
// EventEmitter.notify("OnCountrySelected", country); | |
// } | |
// navigation.goBack(); | |
// }; | |
return ( | |
<View style={styles.container}> | |
<View style={styles.list}> | |
{COUNTRIES.map((c) => { | |
return ( | |
<TouchableOpacity onPress={() => handleOnCountrySelected(c)} key={c}> | |
<View style={styles.cell}> | |
<Text style={styles.labelText}>{c}</Text> | |
{c === country ? <Text style={styles.selectedText}>{"selected"}</Text> : null} | |
</View> | |
</TouchableOpacity> | |
); | |
})} | |
</View> | |
</View> | |
); | |
}; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
backgroundColor: "#eee", | |
justifyContent: "flex-start", | |
alignItems: "stretch", | |
}, | |
list: { | |
justifyContent: "flex-start", | |
alignItems: "stretch", | |
}, | |
cell: { | |
flexDirection: "row", | |
justifyContent: "flex-start", | |
alignItems: "center", | |
backgroundColor: "#fff", | |
borderBottomWidth: 1, | |
borderBottomColor: "#aaa", | |
paddingVertical: 12, | |
paddingHorizontal: 16, | |
}, | |
labelText: { | |
fontSize: 16, | |
minWidth: 90, | |
color: "#808080", | |
}, | |
selectedText: { | |
flex: 1, | |
fontSize: 14, | |
fontWeight: "500", | |
textAlign: "right", | |
color: "#00e", | |
}, | |
}); | |
export default CountryPickerScene; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A simplify version of https://github.com/primus/eventemitter3 | |
*/ | |
const listenersMap: { [id: string]: Array<(...params: any[]) => void> } = {}; | |
function addListener(eventName: string, listener: (...params: any[]) => void) { | |
listenersMap[eventName] = listenersMap[eventName] || []; | |
listenersMap[eventName].push(listener); | |
} | |
function removeListener(eventName: string, listener: (...params: any[]) => void) { | |
let lis = listenersMap[eventName]; | |
if (!lis) return; | |
for (let i = lis.length - 1; i >= 0; i--) { | |
if (lis[i] === listener) { | |
lis.splice(i, 1); | |
break; | |
} | |
} | |
} | |
function removeAllListeners(eventName: string) { | |
listenersMap[eventName] = []; | |
} | |
function notify<T = any>(eventName: string, ...params: T[]) { | |
let listeners = listenersMap[eventName]; | |
if (!listeners) return false; | |
listeners.forEach(fnc => { | |
fnc(...params); | |
}); | |
return true; | |
} | |
export default { | |
addListener, | |
removeListener, | |
removeAllListeners, | |
notify, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useNavigation } from "@react-navigation/native"; | |
import React, { useEffect } from "react"; | |
import { Text, View, StyleSheet, Button } from "react-native"; | |
import { showCountryPicker, useCountryPickerListener } from "./CountryPickerScene"; | |
import EventEmitter from "../services/EventEmitter"; | |
const ProfileScene: React.FC<{}> = () => { | |
const navigation = useNavigation(); | |
const [country, setCountry] = React.useState("Japan"); | |
// Opt1: Use event emitter only | |
// useEffect(() => { | |
// const handleCountrySelected = (country) => { | |
// setCountry(country); | |
// }; | |
// EventEmitter.addListener("OnCountrySelected", handleCountrySelected); | |
// return () => { | |
// EventEmitter.removeListener("OnCountrySelected", handleCountrySelected); | |
// }; | |
// }, []); | |
// Opt 2: Use event emitter + strong-typed check | |
useCountryPickerListener(({ country: selectedCountry }) => { | |
setCountry(selectedCountry); | |
}, []); | |
return ( | |
<View style={styles.container}> | |
<> | |
<View style={styles.cell}> | |
<Text style={styles.labelText}>{"Name: "}</Text> | |
<Text style={styles.valueText}>{"Kenji Taro"}</Text> | |
</View> | |
<View style={styles.cell}> | |
<Text style={styles.labelText}>{"Age: "}</Text> | |
<Text style={styles.valueText}>{"35"}</Text> | |
</View> | |
<View style={styles.cell}> | |
<Text style={styles.labelText}>{"Address: "}</Text> | |
<Text style={styles.valueText}>{"1200 Lgh, Tokyo"}</Text> | |
</View> | |
<View style={styles.cell}> | |
<Text style={styles.labelText}>{"Country: "}</Text> | |
<Text style={styles.valueText}>{`${country}`}</Text> | |
</View> | |
</> | |
<Button | |
title="Change country" | |
onPress={() => { | |
showCountryPicker(navigation, country); | |
}} | |
/> | |
</View> | |
); | |
}; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
backgroundColor: "#eee", | |
justifyContent: "flex-start", | |
alignItems: "stretch", | |
}, | |
cell: { | |
flexDirection: "row", | |
justifyContent: "flex-start", | |
alignItems: "center", | |
backgroundColor: "#fff", | |
borderBottomWidth: 1, | |
borderBottomColor: "#aaa", | |
paddingVertical: 12, | |
paddingHorizontal: 16, | |
}, | |
labelText: { | |
fontSize: 16, | |
minWidth: 90, | |
color: "#808080", | |
}, | |
valueText: { | |
flex: 1, | |
fontSize: 16, | |
fontWeight: "500", | |
textAlign: "right", | |
color: "#101010", | |
}, | |
}); | |
export default ProfileScene; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import EventEmitter from "src/services/EventEmitter"; | |
function useEventListener<T extends (...params: any) => void>(event: string, listener: T, deps: ReadonlyArray<any>) { | |
React.useEffect(() => { | |
EventEmitter.addListener(event, listener); | |
return () => { | |
EventEmitter.removeListener(event, listener); | |
}; | |
}, deps); | |
} | |
export function makeEventNotifier<P>(name: string) { | |
return { | |
name: name, | |
notify: (param: P) => { | |
EventEmitter.notify(name, param); | |
}, | |
useEventListener: (listener: (param: P) => void, deps: ReadonlyArray<any>) => | |
useEventListener(name, listener, deps), | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment