When starting a new project, there are plenty of choices to be made around code style, language, folder layout, and more. Consistency is the key for creating clean, maintainable codebases. Therefore once decided, you'd usually need to stick with these choices for a while.
Time and experience will teach you what works and what doesn't. But what if you don't have time? You can always use someone else's experience.
Here are my top 10 tips for structuring a React Native project:
1. Use TypeScript
Yes, there is a bit of a learning curve if you're used to plain JavaScript.
Yes, it's worth it.
Typed JavaScript makes refactoring a whole lot easier, and when done right, gives you a lot more confidence in your code. Use the guide in the docs for setup instructions. Make sure to enable strict mode ("strict": true
in the compilerOptions
).
You can also add type checking in your CI with tsc --noEmit
, so you can be confident in your types!
2. Set up a module alias to /src
Set up a single module alias to /src
(and a separate one for /assets
if needed), so instead of:
import CustomButton from '../../../components/CustomButton';
you can do:
import CustomButton from '@src/components/CustomButton';
I always use a @
or a ~
in front of src
to highlight it's an alias.
I've seen implementations where folks set up multiple type aliases - one for @components
, one for @screens
, one for @util
etc, but I've found a single top level alias to be the clearest.
There's a handy guide for setting this up with TypeScript in the React Native docs.
3. Use Inline Styles
You have an option for using the built in inline styles, or Styled Components.
I started off with Styled Components, then switched to inline styles, because there used to be a performance implication, though that's negligible, so now it's just a preference.
4. One Style File Per Component
Each component should have their own style file with a styles.ts
extension:
FirstComponent.tsx
FirstComponent.styles.ts
SecondComponent.tsx
SecondComponent.styles.tsx
Note, the .styles.ts
in the filename is just a convention I use to indicate that the styles belong to the component, the TypeScript compiler will treat these as regular .ts
files.
Each style file exports a single style object for the component:
// FirstComponent.styles.ts
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
padding: 20,
},
});
export default styles;
Each component only imports only its own styles:
// FirstComponent.tsx
import styles from './FirstComponent.styles';
...
5. Use Global Styles
Create a globalStyles.ts
file at the top level of the /src
directory, and import it to the .styles.ts
as needed.
Always use constants for:
- colours
- fonts
- font sizes
- spacing
It may seem tedious at first, but handy in the long term. And if you find you're ending up creating constant for every single space, it's something to gently bring up with the Design team, as design guides would generally not want that.
6. Flatten Style Constants
Instead of:
const globalStyles = {
color: {
blue: '#235789',
red: '#C1292E',
yellow: '#F1D302',
},
};
Do this:
const globalStyles = {
colorBlue: '#235789',
colorRed: '#C1292E',
colorYellow: '#F1D302',
};
It can be tempting to group these, but I've found that keeping them flat can be more handy, e.g. if you wanted to replace all instances of colorRed
in your codebase, you could do a find and replace, whereas with colors.red
it'd be harder, since the colour could have been destructured.
7. Use Numbers in Style Constants
Instead of:
const globalStyles = {
fontSize: {
extraSmall: 8,
small: 12,
medium: 16,
large: 18,
extraLarge: 24,
},
};
Do this:
const globalStyles = {
fontSize8: 8,
fontSize12: 12,
fontSize16: 16,
fontSize18: 18,
fontSize24: 24,
};
The first option may look nicer when writing it down, but during development, you don't tend to care about "medium" and "large", and just care about the number. And it will avoid the awkward naming when the designers inevitably add a font size 14 and you have to start calling your variables things like mediumSmall
.
8. One Component Per File
Here's the template for a new component:
import React from 'react';
import { View, Text } from 'react-native';
import styles from './App.styles';
const App = () => {
return (
<View style={styles.container}>
<Text>Hello, world!</Text>
</View>
);
};
export default App;
Some things to note here:
- function components over class components: I'd always use function components and manage any state and side-effects using hooks
- I use constant functions, but both
const
andfunction
are equally good here. In factfunction
might be better in the long term - default export: I always use a default export, though there is an argument to be made that named exports are better since they'll be clearer to refactor, and I agree - that might be the next step
9. Separate Components and Screens
Here's a typical folder structure I end up with:
/assets
/images
image.png
anotherImage.png
/icons
icon.svg
anotherIcon.svg
/src
/components
Component1.tsx
Component1.styles.ts
Component1.test.ts
Component2.tsx
Component2.styles.ts
Component2.test.ts
/screens
Screen.tsx
Screen.styles.ts
Modal.tsx
Modal.styles.ts
App.tsx
globalStyles.ts
types.ts
I always separate components in the /components
directory and the screens and modals in the /screens
directory. When using react-navigation
, there is no structural difference between screens and modals, but I prefer to also differentiate the intent by naming the file SomethingModal.tsx
.
Another thing to note is the file names - rather than creating a folder with the file name, and naming each file index.tsx
, the filename should reflect the component name. That is mostly for convenience - in most editors, it'll get tedious to track down which file you're editing when they're all called index.tsx
I've also seen implementations where all components are imported to a single index.ts
file and exported from there. I personally am not a fan of that solution and see it as an unnecessary extra step.
10. Lint Your Code
It's worth it. Trust me!
- Use eslint and prettier - they actually come pre-installed when you initialise a new project
- Set up a pre-commit hook - I usually set up a pre-commit hook for linting and pre-push hook for tests. There's a great guide here.
- Check lint, test and TypeScript errors on CI! This is so important - the only way to ensure a consistent code style across the project lifecycle. Setting up CI is one of the first things I do when starting a new project.
Hope this helps! Got any tips of your own that I did't list here? Let me know in the comments! 👀
Top comments (10)
On Step 9. Would you perhaps consider having a folder called Component 1 under components which would contain. I am not sure if it adds anything or if having a flatter structure like the way you presented is fine.
Component1.tsx
Component1.styles.ts
That's a really valid point! I have actually done this in the past, but keeping the components flat is intentional. Basically you have 4 options:
1) Name the root file
index.tsx
:This will enable you to still import it like
This was my preferred method in the past. The downside of this is is that every component is named
index.tsx
which is very unpleasant to work with in VSCode in particular. Have you ever tried to search a component by file name in a codebase where everything is calledindex
? Even though the path includes the name, VSCode search isn't really built to handle it well. Also, every tab is calledindex.tsx
which makes it hard to tell at a glance which files you have open.2) Keep the component name, but add an
index.ts
for exporting itHere,
index.ts
just importsComponent1
and default exports it. The downside is that you have to remember the convention and you have a lot of extra files just for exporting components.3) Keep the component named
This isn't too bad, and I do that occasionally, but the downside here is that the import will look like this, with the component name listed twice in the path:
4) Keep the folder structure flat as I suggested. The obvious downside is that the list of components will get long twice as fast (or 3x if as fast if you write tests!).
So they all have pros and cons. I used to do option 1 before I started using VSCode which really doesn't work well with
index.js
files. Now I do option 4 and occasionally option 3 is I need to add more files to a single component.Thanks very much. This is an extremely detailed explanation. Option 1 definitely the points you highlighted I have found to be painful. The opening of multiple tabs with index.js was an extreme pain point for my eyes. Option 2 again, I remember when seeing the pattern for the first time I found it quite tedious and just the mental mindset of doing this every time you add new components wasn't pleasing. Yup after reading all this I do see the value and the simplicity Option 4 will bring to the workflow. Having read this I actually think it almost makes for a very nice blog post in itself. Thanks
This is a very informative article thanks for putting it up.
Make every component have their own style file, this does make a lot of sense. Just with all the examples having the styles in the same component file made me miss this one , so thanks for it and compiling the entire list.
Comprehensive and straight to the point, I like it, thank you for the tips!
Wondering if installing axios instead of using the fetch library could be worthwhile. Although I am not sure if there might be compatibility issues. Any thoughts
You get fetch for free in React Native, and the api is very easy to use, so I've never had the reason to use anything else for api requests. But if you really wanted to use axios, I recon it would work just fine.
So one of the things that really pushes me away from using the Fetch API anywhere (Frontend, backend, Mobile/Web) is how the API handles 4xx errors.
From MDN developer.mozilla.org/en-US/docs/W...
The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure or if anything prevented the request from completing. ()
This means errors that normally should fall into the catch block, now fall into our then block and we have to handle them. Some might fail silently if you are not looking for this and keep wondering why your catch block is not firing. That is essentially for me the biggest deterrent not to use the fetch API.
Thanks,
Nice one