DEV Community

Cover image for 10 Tips for Structuring a React Native Project
Kadi Kraman
Kadi Kraman

Posted on

10 Tips for Structuring a React Native Project

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';
Enter fullscreen mode Exit fullscreen mode

you can do:

import CustomButton from '@src/components/CustomButton';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Each component only imports only its own styles:

// FirstComponent.tsx

import styles from './FirstComponent.styles';

...
Enter fullscreen mode Exit fullscreen mode

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',
  },
};
Enter fullscreen mode Exit fullscreen mode

Do this:

const globalStyles = {
  colorBlue: '#235789',
  colorRed: '#C1292E',
  colorYellow: '#F1D302',
};
Enter fullscreen mode Exit fullscreen mode

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

Do this:

const globalStyles = {
  fontSize8: 8,
  fontSize12: 12,
  fontSize16: 16,
  fontSize18: 18,
  fontSize24: 24,
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 and function are equally good here. In fact function 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
Enter fullscreen mode Exit fullscreen mode

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!

  1. Use eslint and prettier - they actually come pre-installed when you initialise a new project
  2. 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.
  3. 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)

Collapse
 
sushmeet profile image
sushmeet sunger

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

Collapse
 
kadikraman profile image
Kadi Kraman • Edited

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:

/components
  /Component1
    index.tsx
    index.styles.ts
    index.test.ts
Enter fullscreen mode Exit fullscreen mode

This will enable you to still import it like

import Component1 from "src/component/Component1";
Enter fullscreen mode Exit fullscreen mode

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 called index? Even though the path includes the name, VSCode search isn't really built to handle it well. Also, every tab is called index.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 it

/components
  /Component1
    Component1.tsx
    Component1.styles.ts
    Component1.test.ts
    index.ts
Enter fullscreen mode Exit fullscreen mode

Here, index.ts just imports Component1 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

/components
  /Component1
    Component1.tsx
    Component1.styles.ts
    Component1.test.ts
Enter fullscreen mode Exit fullscreen mode

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:

import Component1 from "src/component/Component1/Component1";
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
sushmeet profile image
sushmeet sunger

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

Collapse
 
gbols profile image
gbols

This is a very informative article thanks for putting it up.

Collapse
 
sushmeet profile image
sushmeet sunger

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.

Collapse
 
laetitiazammit profile image
Laetitia

Comprehensive and straight to the point, I like it, thank you for the tips!

Collapse
 
sushmeet profile image
sushmeet sunger

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

Collapse
 
kadikraman profile image
Kadi Kraman

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.

Collapse
 
sushmeet profile image
sushmeet sunger • Edited

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,

Collapse
 
teseo profile image
Javier Mellado

Nice one