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

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. !

  2. " #

  3. $

  4. 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.
  5. We have only 2 developers who have some Swift experience

    and 0 developers with Android experience.
  6. & Have good defaults ' Have good code organization (

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

    Make common operations easy ) Isolate dependencies * Extensibility and reusability
  8. 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> ); }
  9. 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> ); }
  10. 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'; // ...
  11. Loading State Not Found Error Server Error Authorization Error Authentication

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

    Error Error State Loaded State render Screen States Layout
  13. 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>) { // ... }
  14. 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> ); }, });
  15. // ==================================================== // GraphQL fragment: ProfileAvatarFragment // ==================================================== export interface

    ProfileAvatarFragment { __typename: "Profile"; id: string; name: string; kind: string; imageUrl: string | null; } graphql/types.ts
  16. 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} `;
  17. 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} `;
  18. 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;
  19. }; 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; }
  20. 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> ); }
  21. 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) => { // ... });
  22. <Button to={routes.profile(profile)} /> <Button onPress={onClickReturnsPromise} /> <Button onPress={onClickReturnsPromise} confirm="Are you

    sure?" requireLogin={true} /> <Button mutation={MUTATION} input={input} onMutate={onMutate} />
  23. ⚠ We do this on mobile, because we can't make

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

    ISpacingProps { someProps: any, // ... } function MyComponent(props) { // ... return ( <View style={spacingStyle(props)}> {/* ... */} </View> ); }
  26. 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> ); }
  27. 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; }
  28. / GraphQL 1 Screens ( createScreen 2 screens helper &

    Components 3 isolating dependancies * directory as folder 4 domain components 5 Recap