Skip to content

Instantly share code, notes, and snippets.

@tranquan
Last active March 9, 2024 17:57
Show Gist options
  • Save tranquan/7c219eda2a24165e21000cdbe840a9ad to your computer and use it in GitHub Desktop.
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
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>
);
}
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;
/**
* 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,
};
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;
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