$30 off During Our Annual Pro Sale. View Details »

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 .

Radoslav Stankov

October 11, 2020
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

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

    View Slide

  2. !

    View Slide

  3. Radoslav Stankov
    @rstankov
    blog.rstankov.com

    twitter.com/rstankov

    github.com/rstankov

    speakerdeck.com/rstankov

    View Slide

  4. View Slide

  5. View Slide

  6. Architecture

    View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. https://rstankov.com/appearances

    View Slide

  11. View Slide

  12. View Slide

  13. Beta

    View Slide

  14. " #

    View Slide

  15. $

    View Slide

  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.

    View Slide

  17. We have only 2 developers who have some Swift experience
    and 0 developers with Android experience.

    View Slide

  18. Apollo for Swift isn't very good.

    View Slide

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

    View Slide

  20. We need todo fast UI iterations and experiments.

    View Slide

  21. We will need Android as well

    View Slide

  22. View Slide

  23. % Tech Stack

    View Slide

  24. View Slide

  25. View Slide

  26. View Slide

  27. View Slide

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

    View Slide

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

    View Slide

  30. 2
    1 3
    Support Components Screens

    View Slide

  31. Support
    Components
    Screens

    View Slide

  32. 1) Support 2) Components 3) Screens

    View Slide

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

    View Slide

  34. 1 Support
    Components
    Pages
    2
    3
    components/
    hooks/
    screens/
    styles/
    types/
    utils/
    app.ts
    config.ts
    paths.ts

    View Slide

  35. View Slide

  36. 3
    Screens

    View Slide

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

    View Slide

  38. View Slide

  39. @react-navigation/native
    +

    View Slide

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

















    );
    }

    View Slide

  41. import * as Screens from '~/screens'
    const MainStack = createStackNavigator();
    function MainStackScreen() {
    return (

















    );
    }

    View Slide

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

    View Slide

  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';
    // ...

    View Slide

  44. screens/
    Home/
    WithSearch/
    FeedItem/
    Query.graphql
    index.ts
    utils.ts

    View Slide

  45. Screen States
    Layout

    View Slide

  46. Loading State
    Screen States
    Layout

    View Slide

  47. Loading State
    Error State
    Screen States
    Layout

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  51. interface IOptions {
    name: string;
    query?: GraphQL.DocumentNode;
    queryVariables?: object | ((params: Params) => object);
    component: React.FC>;
    type?: 'push' | 'actionSheet' | 'overlay';
    title?: string;
    background?: IScreenBackground,
    safeArea?: boolean;
    }
    function createScreen(options: IOptions) {
    // ...
    }

    View Slide

  52. screens/[Name]/index.ts (createScreen) -> screens/index.ts -> app.ts

    View Slide

  53. export default createScreen({
    name: 'Home',
    query: QUERY,
    queryVariables: {
    cursor: null,
    },
    background: 'rainbow',
    component({ data, fetchMore, refetch }) {
    useFeedNavigationOptions();
    return (

    itemComponent={FeedItem}
    connection={data.mobileFeed}
    fetchMore={fetchMore}
    refetch={refetch}
    />

    );
    },
    });

    View Slide

  54. http://graphql.org/

    View Slide

  55. View Slide

  56. apollo client:codegen

    View Slide

  57. components/ProfileAvatar/Fragment.ts
    import gql from 'graphql-tag';
    export default gql`
    fragment ProfileAvatarFragment on Profile {
    id
    name
    kind
    imageUrl
    }
    `;

    View Slide

  58. // ====================================================
    // GraphQL fragment: ProfileAvatarFragment
    // ====================================================
    export interface ProfileAvatarFragment {
    __typename: "Profile";
    id: string;
    name: string;
    kind: string;
    imageUrl: string | null;
    }
    graphql/types.ts

    View Slide

  59. import { ProfileAvatarFragment } from '~/types/graphql';

    View Slide

  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}
    `;

    View Slide

  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}
    `;

    View Slide

  62. Query
    Fragment Fragment
    Fragment
    Fragment
    Fragment
    Fragment

    View Slide

  63. Screen
    Component Component
    Component
    Component
    Component
    Component

    View Slide

  64. View Slide

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

    View Slide

  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;

    View Slide

  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;
    }

    View Slide

  68. import routes from '~/routes';

    routes.back();
    routes.login({ reason: "like" });
    routes.profile(profile);

    View Slide

  69. import routes, { useNavigate } from '~/routes';
    export function MyComponent() {
    const navigate = useNavigate();
    const onPress = () => {
    navigate(routes.product({ id: '5' }));
    }
    return (

    Visit product

    );
    }

    View Slide


  70. View Slide

  71. 2
    Components

    View Slide

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

    View Slide

  73. View Slide

  74. , Component as directory
    components/
    PublicComponent/
    PrivateSubComponent/
    Fragment.graphql
    Mutation.graphql
    index.ts
    styles.ts
    utils.ts

    View Slide


  75. View Slide

  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) => {
    // ...
    });

    View Slide

  77. Only use this component

    View Slide

  78. View Slide

  79. to={routes.profile(profile)} />

    View Slide

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

    View Slide

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

    View Slide

  82. to={routes.profile(profile)} />
    onPress={onClickReturnsPromise} />
    onPress={onClickReturnsPromise}
    confirm="Are you sure?"
    requireLogin={true} />
    mutation={MUTATION}
    input={input}
    onMutate={onMutate} />

    View Slide





  83. View Slide

  84. View Slide

  85. {...}
    {...}

    View Slide

  86. {...}
    {...}
    {...}
    {...}
    {...}

    View Slide

  87. Utility
    Styling
    Domain

    View Slide

  88. LikeButton Button

    View Slide

  89. AnswerCard Like Button

    View Slide











  90. View Slide

  91. Atomic Design

    View Slide

  92. ...Kinda -

    View Slide

  93. . generic components
    / domain components

    View Slide

  94. 1
    Support

    View Slide

  95. View Slide

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

    View Slide

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

    View Slide

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

    View Slide




  99. View Slide




  100. View Slide


  101. We do this on mobile, because we can't make Flex
    component to have "gap" property!

    View Slide

  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;
    }

    View Slide

  103. import { spacingStyle, ISpacing } from '~/styles/spacing';
    interface IProps extends ISpacingProps {
    someProps: any,
    // ...
    }
    function MyComponent(props) {
    // ...
    return (

    {/* ... */}

    );
    }

    View Slide

  104. View Slide

  105. react-native-dynamic
    +

    View Slide

  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 Slide

  107. import { useColor } from '~/styles/theme';
    const backgroundColor = useColor('background');

    View Slide

  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;
    }

    View Slide

  109. View Slide

  110. Recap

    View Slide

  111. / GraphQL
    1 Screens
    ( createScreen
    2 screens helper
    & Components
    3 isolating dependancies
    * directory as folder
    4 domain components
    5 Recap

    View Slide

  112. View Slide

  113. View Slide

  114. Thanks 6

    View Slide

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

    View Slide

  116. View Slide