Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React Native for Better or Worst

Radoslav Stankov
October 15, 2023
120

React Native for Better or Worst

Radoslav Stankov

October 15, 2023
Tweet

Transcript

  1. "

  2. - implement a mobile app $ - one developer (me)

    ! - support iOS % / Android & - ship it as fast as possible '
  3. ( backed by Google ) uses Dart * cross platform

    + compiles to native machine code , custom components - battery included
  4. . backed by Facebook / uses Javascript (or TypeScript) 0

    uses React 1 cross platform 2 minimal 3 native and JS bridge 4 native components
  5. React Native in 2019 was 5 React Native in 2021

    was 6 React Native in 2022 was 7 React Native in 2023 still is 7
  6. React Native is fast enough has a rich ecosystem Flutter

    is faster has more batteries included I have done 3 React Native apps previously I know React/TypeScript/GraphQL extremely well I don't know Dart I don't know Flutter
  7. Expo in 2020 was ... 8 Expo in 2020 was

    ... 9 Expo in 2022 was ... 7 Expo in 2023 is ...:
  8. 0 Make common operations easy 4 Have good code organization

    1 Isolate dependencies + Extensibility and reusability
  9. 0 Make common operations easy 4 Have good code organization

    1 Isolate dependencies + Extensibility and reusability
  10. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  11. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx 1 Support Components Pages 2 3
  12. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  13. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  14. { "expo": { "name": "Angry Building", "version": "1.48.0", "orientation": "portrait",

    "icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#252629" } }, // ... }
  15. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  16. ... I still don't use it, because it was released

    fairly recently ; ... however the way the app is setup, transition should be easy <
  17. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  18. import screens from 'src/screens'; import { NavigationContainer } from '@react-navigation/native';

    import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createStackNavigator } from '@react-navigation/stack'; Sentry.init(); const RootStack = createStackNavigator(); export default function App() { return ( <ActionSheetProvider> <ApolloProvider client={apolloClient}> <NavigationContainer> <WithViewer> <RootStack.Navigator> <RootStack.Screen name="Main" component={TabsScreen} options={{ header <RootStack.Screen {...screens.apartmentNotOwning} /> <RootStack.Screen {...screens.apartmentOwning} /> <RootStack.Screen {...screens.apartmentUserCreate} /> <RootStack.Screen {...screens.issueCreate} /> <RootStack.Screen {...screens.serviceRequestCreate} /> <RootStack.Screen {...screens.services} /> </RootStack.Navigator> </WithViewer> </NavigationContainer> </ApolloProvider>
  19. export default function App() { return ( <ActionSheetProvider> <ApolloProvider client={apolloClient}>

    <NavigationContainer> <WithViewer> <RootStack.Navigator> <RootStack.Screen name="Main" component={TabsScreen} options={{ header <RootStack.Screen {...screens.apartmentNotOwning} /> <RootStack.Screen {...screens.apartmentOwning} /> <RootStack.Screen {...screens.apartmentUserCreate} /> <RootStack.Screen {...screens.issueCreate} /> <RootStack.Screen {...screens.serviceRequestCreate} /> <RootStack.Screen {...screens.services} /> </RootStack.Navigator> </WithViewer> </NavigationContainer> </ApolloProvider> </ActionSheetProvider> ); } 14 pt
  20. const Tab = createBottomTabNavigator(); function TabsScreen() { return ( <Tab.Navigator

    screenOptions={tabNavigatorOptions}> <Tab.Screen name="buildingTab" component={Screen} initialParams={{ initialRoute: screens.home.name }} options={/* ... */} /> <Tab.Screen name="apartmentTab" component={Screen} initialParams={{ initialRoute: screens.apartment.name }} options={/* ... */} /> <Tab.Screen name="bulletinBoardTab" component={Screen} initialParams={{ initialRoute: screens.bulletinBoard.name }} options={/* ... */} /> <Tab.Screen name="cashReserveTab" component={Screen} initialParams={{ initialRoute: screens.cashReserve.name }}
  21. /> <Tab.Screen name="apartmentTab" component={Screen} initialParams={{ initialRoute: screens.apartment.name }} options={/* ...

    */} /> <Tab.Screen name="bulletinBoardTab" component={Screen} initialParams={{ initialRoute: screens.bulletinBoard.name }} options={/* ... */} /> <Tab.Screen name="cashReserveTab" component={Screen} initialParams={{ initialRoute: screens.cashReserve.name }} options={/* ... */} /> <Tab.Screen name="issuesTab" component={Screen} initialParams={{ initialRoute: screens.issues.name }} options={/* ... */} /> </Tab.Navigator> ); }
  22. const ScreenStack = createStackNavigator(); function Screen(props) { const initialRoute =

    props.route.params.initialRoute; return ( <ScreenStack.Navigator initialRouteName={initialRoute}> <ScreenStack.Screen {...screens.apartmentHeatingMeasurements} /> <ScreenStack.Screen {...screens.apartmentNotes} /> <ScreenStack.Screen {...screens.apartmentPayments} /> <ScreenStack.Screen {...screens.apartmentTaxes} /> <ScreenStack.Screen {...screens.apartmentUnpaidTaxes} /> <ScreenStack.Screen {...screens.apartmentUsers} /> <ScreenStack.Screen {...screens.apartment} /> <ScreenStack.Screen {...screens.buildingNotes} /> <ScreenStack.Screen {...screens.bulletinBoardPostCreate} /> <ScreenStack.Screen {...screens.bulletinBoardPostUpdate} /> <ScreenStack.Screen {...screens.bulletinBoard} /> <ScreenStack.Screen {...screens.cashReserve} /> <ScreenStack.Screen {...screens.contacts} /> <ScreenStack.Screen {...screens.home} /> <ScreenStack.Screen {...screens.issues} /> <ScreenStack.Screen {...screens.messages} /> <ScreenStack.Screen {...screens.settings} /> <ScreenStack.Screen {...screens.votingPoll} /> </ScreenStack.Navigator> ); }
  23. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  24. Loading State Not Found Error Server Error Authorization Error Authentication

    Error Error State Loaded State Screen States Layout
  25. Loading State Not Found Error Server Error Authorization Error Authentication

    Error Error State Loaded State render Screen States Layout
  26. interface IOptions<D, P> { name: string; query?: DocumentNode; queryVariables?: object

    | ((params: P) => object); queryRefreshOnShow?: boolean; component: IComponent<D, P>; type?: keyof typeof layouts; headerTitle?: ITranslation; background: IBackground; } interface IScreen { name: string; options: StackNavigationOptions; component: any; } export default function createScreen<D = any, P = any>(options: IOptions<D, P>): IScreen // ... }
  27. export default createScreen<IHomeScreen>({ name: 'home', type: 'plain', query: QUERY, background:

    'black', component({ data, fetchMore, refetch }) { usePushNotificationsRegister(); usePushNotificationHandle(data.viewer); return ( <View style={styles.container}> <ImageBackground resizeMode="cover" source={image} style={styles.topBackgroun <View style={styles.bottomBackground} /> <BuildingApartmentsList refetch={refetch} fetchMore={fetchMore} building={data.building} /> <ReportIssueButton /> </View> ); }, });
  28. ▾ src/ ▾ screens/ ▾ home/ ▸ Status/ background.png BuildingApartmentsList.tsx

    Header.tsx index.tsx Query.ts SelectEntrancePicker.tsx
  29. components/ApartmentStatus/Fragment.tsx import { gql } from '@apollo/client'; export default gql`

    fragment IApartmentStatusFragment on Apartment { id number name overdueAmount } `;
  30. types/grahpql.ts export type IApartmentStatusFragment = { __typename: 'Apartment'; id: string;

    number?: string | null; name?: string | null; overdueAmount: number; };
  31. import { gql } from '@apollo/client'; import IHomeScreenStatusFragment from './Status/Fragment';

    import IApartmentStatusFragment from 'src/components/ApartmentStatus/Fragment'; export default gql` query IHomeScreen($cursor: String) { building { id name apartments(first: 100, after: $cursor) { nodes { id name floor isViewerSelected ...IApartmentStatusFragment } pageInfo { hasNextPage endCursor } } ...IHomeScreenStatusFragment } } ${IHomeScreenStatusFragment} ${IApartmentStatusFragment} `; screens/home/Query.ts
  32. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  33. import { useNavigation } from '@react-navigation/native'; import screens from 'src/screens';

    type IScreenName = keyof typeof screens; export type IRoute = | { transition: 'goBack' } | { screen: IScreenName; params?: IParams; root?: boolean } | { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, };
  34. import { useNavigation } from '@react-navigation/native'; import screens from 'src/screens';

    type IScreenName = keyof typeof screens; export type IRoute = | { transition: 'goBack' } | { screen: IScreenName; params?: IParams; root?: boolean } | { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, };
  35. import { useNavigation } from '@react-navigation/native'; import screens from 'src/screens';

    type IScreenName = keyof typeof screens; export type IRoute = | { transition: 'goBack' } | { screen: IScreenName; params?: IParams; root?: boolean } | { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, };
  36. import { useNavigation } from '@react-navigation/native'; import screens from 'src/screens';

    type IScreenName = keyof typeof screens; export type IRoute = | { transition: 'goBack' } | { screen: IScreenName; params?: IParams; root?: boolean } | { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, };
  37. | { screen: IScreenName; params?: IParams; root?: boolean } |

    { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, }; export default routes; export interface INavigation { navigate: (screen: string, params?: IParams) => void; goBack: VoidFunction; }
  38. | { screen: IScreenName; params?: IParams; root?: boolean } |

    { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, }; export default routes; export interface INavigation { navigate: (screen: string, params?: IParams) => void; goBack: VoidFunction; }
  39. import { useNavigation } from '@react-navigation/native'; import screens from 'src/screens';

    type IScreenName = keyof typeof screens; export type IRoute = | { transition: 'goBack' } | { screen: IScreenName; params?: IParams; root?: boolean } | { screen: 'buildingTab' | 'apartmentTab' | 'cashReserveTab' | 'issuesTab'; params?: IParams; root: true; }; const routes = { back: { transition: 'goBack' } as IRoute, home: { root: true, screen: 'buildingTab' } as IRoute, // ... bulletinBoardPostUpdate(post: { id: string }) { return { screen: 'bulletinBoardPostUpdate', params: { post }, } as IRoute; }, };
  40. export interface INavigation { navigate: (screen: string, params?: IParams) =>

    void; goBack: VoidFunction; } export function navigate(navigation: INavigation, to: IRoute) { if ('transition' in to) { navigation.goBack(); } else if (to.root) { navigation.navigate('Main', to); } else { navigation.navigate(to.screen, to.params); } } export function useNavigate() { const navigation: any = useNavigation(); const fn = React.useCallback( (to: IRoute) => { navigate(navigation, to); }, [navigation], ); return fn; }
  41. import routes, { useNavigate } from '~/routes'; export function MyComponent()

    { const navigate = useNavigate(); const onPress = () => { navigate(routes.home); } return ( <TouchableOpacity onPress={onPress}> <Text>Visit product</Text> </TouchableOpacity> ); }
  42. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  43. <Button to={routes.profile(profile)} /> <Button onPress={onClickReturnsPromise} /> <Button onPress={onClickReturnsPromise} confirm="Are you

    sure?" requireLogin={true} /> <Button mutation={MUTATION} input={input} onMutate={onMutate} />
  44. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  45. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  46. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  47. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  48. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  49. import i18n from 'i18next'; import captureError from 'src/utils/captureError'; import {

    isProduction } from 'src/config'; import bg from './bg.json'; import en from './en.json'; import es from './es.json'; // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio i18n.init({ resources: { bg: { translation: bg, }, en: { translation: en, }, es: { translation: es, }, }, lng: 'bg', saveMissing: true, missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => { if (isProduction) { captureError(`Key not found t('${key}')`);
  50. import i18n from 'i18next'; import captureError from 'src/utils/captureError'; import {

    isProduction } from 'src/config'; import bg from './bg.json'; import en from './en.json'; import es from './es.json'; // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio i18n.init({ resources: { bg: { translation: bg, }, en: { translation: en, }, es: { translation: es, }, }, lng: 'bg', saveMissing: true, missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => { if (isProduction) { captureError(`Key not found t('${key}')`);
  51. import i18n from 'i18next'; import captureError from 'src/utils/captureError'; import {

    isProduction } from 'src/config'; import bg from './bg.json'; import en from './en.json'; import es from './es.json'; // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio i18n.init({ resources: { bg: { translation: bg, }, en: { translation: en, }, es: { translation: es, }, }, lng: 'bg', saveMissing: true, missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => { if (isProduction) { captureError(`Key not found t('${key}')`);
  52. // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio i18n.init({ resources: { bg: { translation:

    bg, }, en: { translation: en, }, es: { translation: es, }, }, lng: 'bg', saveMissing: true, missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => { if (isProduction) { captureError(`Key not found t('${key}')`); return key; } throw new Error(`Key not found t('${key}')`); }, compatibilityJSON: 'v3', });
  53. // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio i18n.init({ resources: { bg: { translation:

    bg, }, en: { translation: en, }, es: { translation: es, }, }, lng: 'bg', saveMissing: true, missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => { if (isProduction) { captureError(`Key not found t('${key}')`); return key; } throw new Error(`Key not found t('${key}')`); }, compatibilityJSON: 'v3', });
  54. // NOTE(rstankov): Documentation https://www.i18next.com/overview/configuration-optio i18n.init({ resources: { bg: { translation:

    bg, }, en: { translation: en, }, es: { translation: es, }, }, lng: 'bg', saveMissing: true, missingKeyHandler: (_lngs: any, _ns: string, key: string, _fallbackValue: any) => { if (isProduction) { captureError(`Key not found t('${key}')`); return key; } throw new Error(`Key not found t('${key}')`); }, compatibilityJSON: 'v3', });
  55. captureError(`Key not found t('${key}')`); return key; } throw new Error(`Key

    not found t('${key}')`); }, compatibilityJSON: 'v3', }); export type ITranslation = keyof typeof bg; export type ILanguage = 'bg' | 'en' | 'es'; export default i18n.t as (key: keyof typeof bg, interpolations?: any) => string; export function changeLanguage(newLanguage: ILanguage) { if (i18n.language === newLanguage) { return; } return i18n.changeLanguage(newLanguage); }
  56. export type ITranslation = keyof typeof bg; export type ILanguage

    = 'bg' | 'en' | 'es'; export default i18n.t as (key: keyof typeof bg, interpolations?: any) => string; export function changeLanguage(newLanguage: ILanguage) { if (i18n.language === newLanguage) { return; } return i18n.changeLanguage(newLanguage); }
  57. ▾ src/ ▸ components/ ▸ hooks/ ▸ screens/ ▸ styles/

    ▸ translations/ ▸ types/ ▸ utils/ config.ts routes.ts app.json App.tsx
  58. import * as React from 'react'; import { View, StyleSheet

    } from 'react-native'; import s from 'src/styles'; import Logo from 'src/components/Logo'; export default React.memo(function Ribbon() { return ( <> <View style={styles.topBackground}> <Logo height={127} /> </View> <View style={styles.container}> <View style={styles.view} /> </View> </> ); }); const styles = StyleSheet.create({ container: { backgroundColor: s.colors.black, }, view: { marginTop: 10, borderTopLeftRadius: s.radius, borderTopRightRadius: s.radius, width: '100%',
  59. import * as React from 'react'; import { View, StyleSheet

    } from 'react-native'; import s from 'src/styles'; import Logo from 'src/components/Logo'; export default React.memo(function Ribbon() { return ( <> <View style={styles.topBackground}> <Logo height={127} /> </View> <View style={styles.container}> <View style={styles.view} /> </View> </> ); }); const styles = StyleSheet.create({ container: { backgroundColor: s.colors.black, }, view: { marginTop: 10, borderTopLeftRadius: s.radius, borderTopRightRadius: s.radius, width: '100%',
  60. import * as React from 'react'; import { View, StyleSheet

    } from 'react-native'; import s from 'src/styles'; import Logo from 'src/components/Logo'; export default React.memo(function Ribbon() { return ( <> <View style={styles.topBackground}> <Logo height={127} /> </View> <View style={styles.container}> <View style={styles.view} /> </View> </> ); }); const styles = StyleSheet.create({ container: { backgroundColor: s.colors.black, }, view: { marginTop: 10, borderTopLeftRadius: s.radius, borderTopRightRadius: s.radius, width: '100%',
  61. }); const styles = StyleSheet.create({ container: { backgroundColor: s.colors.black, },

    view: { marginTop: 10, borderTopLeftRadius: s.radius, borderTopRightRadius: s.radius, width: '100%', height: s.radius * 2, marginBottom: -s.radius, backgroundColor: s.colors.white, }, topBackground: { position: 'absolute', top: -500, height: 500, right: 0, left: 0, backgroundColor: s.colors.black, alignItems: 'center', justifyContent: 'flex-end', paddingBottom: s.spacing.l, }, });
  62. const styles = StyleSheet.create({ container: { backgroundColor: s.colors.black, }, view:

    { marginTop: 10, borderTopLeftRadius: s.radius, borderTopRightRadius: s.radius, width: '100%', height: s.radius * 2, marginBottom: -s.radius, backgroundColor: s.colors.white, }, topBackground: { position: 'absolute', top: -500, height: 500, right: 0, left: 0, backgroundColor: s.colors.black, alignItems: 'center', justifyContent: 'flex-end', paddingBottom: s.spacing.l, }, });
  63. import variables from 'src/styles'; export type ISpacing = keyof typeof

    spacing; export interface ISpacingProps { marginBottom?: ISpacing | null; marginHorizontal?: ISpacing | null; marginLeft?: ISpacing | null; marginRight?: ISpacing | null; marginTop?: ISpacing | null; marginVertical?: ISpacing | null; margin?: ISpacing | null; padding?: ISpacing | null; paddingBottom?: ISpacing | null; paddingHorizontal?: ISpacing | null; paddingLeft?: ISpacing | null; paddingRight?: ISpacing | null; paddingTop?: ISpacing | null; paddingVertical?: ISpacing | null; } export function spacingStyle(props: ISpacingProps, style: any = {}) { if (props.marginTop) { style.marginTop = variables.spacing[props.marginTop]; } // ... return style; }
  64. import { spacingStyle, ISpacing } from '~/styles/spacing'; interface IProps extends

    ISpacingProps { someProps: any, // ... } function MyComponent(props) { // ... return ( <View style={spacingStyle(props)}> {/* ... */} </View> ); }