The birth— Structuring a React Native project
In this blog, we will be discussing the project structure that we use at Grofers for our React Native project along with how it is integrated with our existing Android, iOS and Web projects.
Our main stack includes:
- redux for state management
- redux-thunk for handling async actions
- axios for handling API requests
- ts-jest and enzyme for writing tests
- tslint for linting
- react-navigation for handling routing across screens
- recyclerlistview for rendering large lists
Mostly we chose the stack that aligned to what we had been using for our web project as that gave us more consistency when switching between web & React Native code setups.
React-navigation was the recommended library suggested by the React Native core team. It was also much more adopted as compared to other navigation libraries like react-native-navigation. Also, React-Navigation is more suited for brownfield React Native apps that are not React Native from scratch.
We chose recyclerlistview over flatlist as it gave us better performance by recycling views at runtime. Recyclerlistview is able to provide this native recycling experience at the expense of having to define heights upfront based on data for all our UI widgets.
We will go into details for each of the third party module in a later blog post.
We made our existing Android, iOS and Web projects as submodules of our React-Native project. So now all a frontend dev has to do is clone the root React Native project which recursively clones all its submodule which includes Android, iOS & Web. This led us more closer to the goal of having one developer be able to code on all the platforms.
Also, to better improve Web developer experience, we are also exploring option of using Web as Monorepo rather than a submodule, as they both share common JS & React based ecosystem.
We went for tech dependent code architecture (also known as Rails-style) rather than a domain-style or duck-style architecture that is recommended for large redux projects. Check here for understanding these different styles. Having Rails-style architecture does make our project coupled with our tech stack — like having actions, reducers, selectors etc. as top level directories rather than domain based such as auth, cart, user etc. We went for this as it made it easier for our Android & iOS devs to onboard and understand the various constructs used in our project. We will be soon migrating to a domain-style architecture.
Next we will highlight some of these constructs in details:
src/widgets: These are basically components that we render on our widgetised pages. Most of our screens are composed of widgets which can be dynamically updated using a Layout Engine dashboard by marketing, design teams. Each widget also return a height calculator that calculates their heights upfront based on the backend data. This is needed for the recylerlistview as mentioned above.
src/components: These are all the UI components that have no business logic in them. They just contain the presentation logic. In each of our file for components, we write a header text depicting the platforms theses components support amongst Android, Web & iOS (we have a linting rule to make sure this header text always exists).
src/screens: These are the components that go as routes in CreateStackNavigation method of react-navigation. We maintain a SUPPPORTED_DEEPLINKS list and any RN screen that is tested and is ready to go to production is added to the list. Our native apps checks this list during initialising to make a decision whether to render a screen in native or react native.
src/modals: Modals are rendered on top of the screen as a separate UI component. We could have used react-navigation modals but it only supports full screen modals which didn’t suffice to our needs.
src/nativeModules: Native Modules are methods defined in Native side that React Native can call asynchronously (all the communication happens asynchronously via RN bridge). We make sure to define types for Native modules as currently there is no way for us to make sure if the Native module actually exists in the native side of code or not.
src/selectors: We prefer using selectors for accessing store and don’t access it directly in any connected component. Apart from its other benefits like giving the ability to do memoization using reselect, we mainly do this because our web codebase overwrites these with its own selectors which end up accessing web’s redux store.
src/styles: We prefer to keep styles in the same file as the component. However, if the styles are common to multiple components (mainly colors), we put them in the `src/styles` directory as constants.
src/types: Though we prefer to keep the types in the same file as the component but if the types are common to multiple components, they are put in the `src/types` directory. We add typings for libraries using `npm add save-dev @types/__LIBRARY__` command. If types for a library is not found, we declare them as module in the same directory.
src/__tests__: We rely heavily on snapshot tests. We write extensive unit tests for our shared components and are also planning on running separate tests for react native web, replacing react-native with react-native-web during jest initialisation.
We aim for same files (specifically action files) for all platforms as much as is possible. Still there remains exceptions. All web specific files running separate logic have to be put in file ending with `*.web.tsx` extension. If iOS has separate logic, then `*.ios.tsx` file is made. Code for Android is always in `*.index.tsx` files as it is the primary platform for us.
Sharing Reducers b/w RN & Web
Though reducer used in our React Native are to be maintained for Android & iOS only as Web runs on a separate store, there are some cases where we have made the provision to inject RN reducers to web’s root reducer. This is mainly for new features built from scratch in RN whose reducer didn’t exist in web already.
That’s pretty much all the constructs that we have used apart from a few minor ones that I would have missed.
This blog is part of our full React Native blog series.
Feel free to share your feedback on our setup or let us know if you are having trouble setting up RN project at your organisation.
Ashish is the Lead Web Developer at Grofers. He works out of our HQs in Gurgaon, India. Got questions or have suggestions? Reach out to him at [email protected].
Thanks for reading Lambda.
We are hiring across various roles! If you are interested in exploring working at Grofers, we’d love to hear from you. You can either apply on LinkedIn or directly reach out to the author on Twitter or LinkedIn.