React Native Architecture at Product Hunt

React Native Architecture at Product Hunt

Showcase the React Native architecture we use in our newest app, Your Stack. What we learned, among the way. How we moved what we know from web to mobile.
Topics will be designing reusable React components, GraphQL, routing in the app, application lifecycle, styling .

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

October 11, 2020
Tweet

Transcript

  1. React Native at Product Hunt Radoslav Stankov 24/09/2020

  2. !

  3. Radoslav Stankov @rstankov blog.rstankov.com
 twitter.com/rstankov
 github.com/rstankov
 speakerdeck.com/rstankov

  4. None
  5. None
  6. Architecture

  7. None
  8. None
  9. None
  10. https://rstankov.com/appearances

  11. None
  12. None
  13. Beta

  14. " #

  15. $

  16. We had a terrible experience with React Native ~2 years

    ago. Most issues we had seems to be fixed now. We did two weeks spike to test this.
  17. We have only 2 developers who have some Swift experience

    and 0 developers with Android experience.
  18. Apollo for Swift isn't very good.

  19. The team knows how to deal with React/Apollo.

  20. We need todo fast UI iterations and experiments.

  21. We will need Android as well

  22. None
  23. % Tech Stack

  24. None
  25. None
  26. None
  27. None
  28. & Have good defaults ' Have good code organization (

    Make common operations easy ) Isolate dependencies * Extensibility and reusability
  29. & Have good defaults ' Have good code organization (

    Make common operations easy ) Isolate dependencies * Extensibility and reusability
  30. 2 1 3 Support Components Screens

  31. Support Components Screens

  32. 1) Support 2) Components 3) Screens

  33. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  34. 1 Support Components Pages 2 3 components/ hooks/ screens/ styles/

    types/ utils/ app.ts config.ts paths.ts
  35. None
  36. 3 Screens

  37. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  38. None
  39. @react-navigation/native +

  40. import * as Screens from '~/screens' const MainStack = createStackNavigator();

    function MainStackScreen() { return ( <MainStack.Navigator initialRouteName={Screens.Home.name} headerMode="screen"> <MainStack.Screen {...Screens.Activity} /> <MainStack.Screen {...Screens.AddToStackForm} /> <MainStack.Screen {...Screens.AddToStackPhotos} /> <MainStack.Screen {...Screens.Comments} /> <MainStack.Screen {...Screens.Feedback} /> <MainStack.Screen {...Screens.Home} /> <MainStack.Screen {...Screens.Notifications} /> <MainStack.Screen {...Screens.Privacy} /> <MainStack.Screen {...Screens.Product} /> <MainStack.Screen {...Screens.Profile} /> <MainStack.Screen {...Screens.Questions} /> <MainStack.Screen {...Screens.Settings} /> <MainStack.Screen {...Screens.StackItem} /> <MainStack.Screen {...Screens.Terms} /> <MainStack.Screen {...Screens.Tip} /> </MainStack.Navigator> ); }
  41. import * as Screens from '~/screens' const MainStack = createStackNavigator();

    function MainStackScreen() { return ( <MainStack.Navigator initialRouteName={Screens.Home.name} headerMode="screen"> <MainStack.Screen {...Screens.Activity} /> <MainStack.Screen {...Screens.AddToStackForm} /> <MainStack.Screen {...Screens.AddToStackPhotos} /> <MainStack.Screen {...Screens.Comments} /> <MainStack.Screen {...Screens.Feedback} /> <MainStack.Screen {...Screens.Home} /> <MainStack.Screen {...Screens.Notifications} /> <MainStack.Screen {...Screens.Privacy} /> <MainStack.Screen {...Screens.Product} /> <MainStack.Screen {...Screens.Profile} /> <MainStack.Screen {...Screens.Questions} /> <MainStack.Screen {...Screens.Settings} /> <MainStack.Screen {...Screens.StackItem} /> <MainStack.Screen {...Screens.Terms} /> <MainStack.Screen {...Screens.Tip} /> </MainStack.Navigator> ); }
  42. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  43. export Activity from './Activity'; export AddNoteToStack from './AddNoteToStack'; export AddToStackForm

    from './AddToStack'; export AddToStackPhotos from './AddToStackPhotos'; export Comments from './Comments'; export Feedback from './Feedback'; export Gallery from './Gallery'; export Home from './Home'; export Login from './Login'; // ...
  44. screens/ Home/ WithSearch/ FeedItem/ Query.graphql index.ts utils.ts

  45. Screen States Layout

  46. Loading State Screen States Layout

  47. Loading State Error State Screen States Layout

  48. Loading State Not Found Error Server Error Authorization Error Authentication

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

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

    Error Error State Loaded State render Screen States Layout
  51. interface IOptions<Data, Params> { name: string; query?: GraphQL.DocumentNode; queryVariables?: object

    | ((params: Params) => object); component: React.FC<IComponentProps<Data, Params>>; type?: 'push' | 'actionSheet' | 'overlay'; title?: string; background?: IScreenBackground, safeArea?: boolean; } function createScreen<Data = any, Params = any>(options: IOptions<Data, Params>) { // ... }
  52. screens/[Name]/index.ts (createScreen) -> screens/index.ts -> app.ts

  53. export default createScreen({ name: 'Home', query: QUERY, queryVariables: { cursor:

    null, }, background: 'rainbow', component({ data, fetchMore, refetch }) { useFeedNavigationOptions(); return ( <WithSearch> <InfinityCarousel itemComponent={FeedItem} connection={data.mobileFeed} fetchMore={fetchMore} refetch={refetch} /> </WithSearch> ); }, });
  54. http://graphql.org/

  55. None
  56. apollo client:codegen

  57. components/ProfileAvatar/Fragment.ts import gql from 'graphql-tag'; export default gql` fragment ProfileAvatarFragment

    on Profile { id name kind imageUrl } `;
  58. // ==================================================== // GraphQL fragment: ProfileAvatarFragment // ==================================================== export interface

    ProfileAvatarFragment { __typename: "Profile"; id: string; name: string; kind: string; imageUrl: string | null; } graphql/types.ts
  59. import { ProfileAvatarFragment } from '~/types/graphql';

  60. components/ProfileCard/Fragment.ts import gql from 'graphql-tag'; import ProfileAvatarFragment from '~/components/ProfileAvatar/Fragment'; export

    default gql` fragment ProfileCardFragment on Profile { id name slug ...ProfileAvatarFragment } ${ProfileAvatarFragment} `;
  61. screens/Profile/Query.ts import gql from 'graphql-tag'; import ProfileCardFragment from '~/components/ProfileCard/Fragment';
 //

    ... more fragments export default gql` query ProfileScreen($id: ID!) { profile(id: $id) { id ...ProfileCardFragment // ... more fragments } } ${ProfileCardFragment} `;
  62. Query Fragment Fragment Fragment Fragment Fragment Fragment

  63. Screen Component Component Component Component Component Component

  64. None
  65. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  66. export interface IRoute { transition?: 'push' | 'navigate' | 'goBack';

    screenName: string; params?: any; } const routes = { back(): IRoute { return { transition: 'goBack', }; }, login(reason?: string): IRoute { return { screenName: 'Login', params: { reason }, }; }, profile(profile: { id: string }): IRoute { return { screenName: 'Profile', params: { id: profile?.id }, }; }, // .... }; export default routes;
  67. }; export default routes; export function navigate(navigation: INavigation, to: IRoute)

    { if (to.transition === 'navigate') { navigation.navigate(to.screenName, to.params); } else if (to.transition === 'goBack') { navigation.goBack(); } else { navigation.push(to.screenName, to.params); } } export function useNavigate() { const navigation = useNavigation(); const fn = React.useCallback( (to: IRoute) => { navigate(navigation, to); }, [navigation], ); return fn; }
  68. import routes from '~/routes'; 
 routes.back(); routes.login({ reason: "like" });

    routes.profile(profile);
  69. import routes, { useNavigate } from '~/routes'; export function MyComponent()

    { const navigate = useNavigate(); const onPress = () => { navigate(routes.product({ id: '5' })); } return ( <TouchableOpacity onPress={onPress}> <Text>Visit product</Text> </TouchableOpacity> ); }
  70. <Button to={routes.product(product)} />

  71. 2 Components

  72. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  73. None
  74. , Component as directory components/ PublicComponent/ PrivateSubComponent/ Fragment.graphql Mutation.graphql index.ts

    styles.ts utils.ts
  75. <Text>

  76. interface IProps extends ISpacingProps { bold?: boolean; center?: boolean; children:

    any; color?: IThemeColors; numberOfLines?: number; size?: IFontSize; style?: TextStyle | TextStyle[]; testID?: string; uppercase?: boolean; withTheme?: boolean; } React.forwardRef((props: IProps, ref) => { // ... });
  77. <Text bold={true} size="l" color="black">Only use this component</Text>

  78. None
  79. <Button to={routes.profile(profile)} />

  80. <Button to={routes.profile(profile)} /> <Button onPress={onClickReturnsPromise} />

  81. <Button to={routes.profile(profile)} /> <Button onPress={onClickReturnsPromise} /> <Button onPress={onClickReturnsPromise} confirm="Are you

    sure?" requireLogin={true} />
  82. <Button to={routes.profile(profile)} /> <Button onPress={onClickReturnsPromise} /> <Button onPress={onClickReturnsPromise} confirm="Are you

    sure?" requireLogin={true} /> <Button mutation={MUTATION} input={input} onMutate={onMutate} />
  83. <Button.Text {...props} /> <Button.Icon {...props} /> <Button.Solid {...props} /> <Button.Outline

    {...props} />
  84. None
  85. <Flex.Row>{...}</Flex.Row> <Flex.Column>{...}</Flex.Column>

  86. <Flex.Row>{...}</Flex.Row> <Flex.Column>{...}</Flex.Column> <Flex.Expand>{...}</Flex.Expand> <Flex.Grid>{...}</Flex.Grid> <Flex.Text>{...}</Flex.Text>

  87. Utility Styling Domain

  88. LikeButton Button

  89. AnswerCard Like Button

  90. <Screen> <DismissKeyboard> <InfiniteScroll> <StackCard> <Flex.Column> <Flex.Row> <LikeButton> <Button.Solid> <Icon> <Text>

  91. Atomic Design

  92. ...Kinda -

  93. . generic components / domain components

  94. 1 Support

  95. None
  96. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  97. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  98. components/ hooks/ screens/ styles/ types/ utils/ app.ts config.ts routes.ts

  99. <Flex marginLeft="m" /> <Button marginLeft="m" /> <Text marginLeft="m" />

  100. <Flex paddingLeft="l" /> <Button paddingLeft="l" /> <Text paddingLeft="l" />

  101. ⚠ We do this on mobile, because we can't make

    Flex component to have "gap" property!
  102. import variables from '~/styles/variables'; 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; }
  103. import { spacingStyle, ISpacing } from '~/styles/spacing'; interface IProps extends

    ISpacingProps { someProps: any, // ... } function MyComponent(props) { // ... return ( <View style={spacingStyle(props)}> {/* ... */} </View> ); }
  104. None
  105. react-native-dynamic +

  106. import { DynamicStyleSheet, useDynamicStyleSheet } from 'react-native-dynamic'; import { colors

    } from '~/styles/theme'; const dynamicStyles = new DynamicStyleSheet({ item: { backgroundColorc: colors.backgroundColor // ... }, // ... }); function MyComponent(props) { const styles = useDynamicStyleSheet(dynamicStyles); // ... return ( <View style={styles.item}> {/* ... */} </View> ); }
  107. import { useColor } from '~/styles/theme'; const backgroundColor = useColor('background');

  108. import { useDarkMode, DynamicValue } from 'react-native-dynamic'; export const colors

    = { text: new DynamicValue('#000000', '#eaeaea'), subtitle: new DynamicValue('#999999', '#878889'), lightBackground: new DynamicValue('#fffff', '#282a2e'), background: new DynamicValue('#f9f9f9', '#1e2023'), border: new DynamicValue('#e8e8e8', '#414141'), // .... }; export type IThemeColors = keyof typeof colors; export function useColor(color: IThemeColors, withTheme: boolean = true) { const value = colors[color]; const isDark = useDarkMode(); return withTheme && isDark ? value.dark : value.light; }
  109. None
  110. Recap

  111. / GraphQL 1 Screens ( createScreen 2 screens helper &

    Components 3 isolating dependancies * directory as folder 4 domain components 5 Recap
  112. None
  113. None
  114. Thanks 6

  115. https://speakerdeck.com/rstankov Thanks 6

  116. None