Templating in React Native.

Templating in React Native.

ยท

7 min read

Templating is a great option when you are work on a big project with a good number of teammates. It saves you a lot of time by keeping a fixed structure for your application folders and when someone new hops in to the project, it does not suck his blood trying to understand the application structure. And if you are someone, as lazy as me, it saves your monotonous task of creating files for every screen or component you create.

So, what are we going to do? Well, we are going to create templates for our App. So let's get started by creating a project first.

npx react-native --ignore-existing init MyApp --template react-native-template-typescript

If you wanna use javascript in your app, you can still follow along. Just the file extension has to .js and .jsx depending on the use case in stead of .ts and .tsx, rest everything is goanna be same. So, no worries. โœŒ

So, run the above command, the react native should give you the boiler plate code. Before, we continue installing another dependency which will be the superhero of our use-case, let's tweak a little bit the boiler plate code.

  • Create a src folder in the root of your project. And create two folders inside it, app and generators.
  • Now move your App.tsx inside src/app. And create other folders as given in the diagram below.

blog.drawio (1).png

Why so many folders, maaan? ๐Ÿคฏ

irritated GIF

Let me explain you.

The components directory will hold all your global components, screen will hold all your screens and the redux directory will have your rootReducer, rootSaga, and your store. In this way, it becomes very obvious of what each directory contains and when you are looking for something specific, you, now know where to find that. ๐Ÿคฉ

okay GIF

Ok. One last pre-requisite step. I, promise, this is the last. Create an index.tsx file inside your components directory and add the following lines.

punch GIF

/* COMPONENT IMPORTS */

export { /* COMPONENT EXPORTS */ };

You will very soon know the reason for adding this.

Now, it's time to onboard our super hero "Plop.js". It's basically a templating framework built on top od Handlebars.js to give you the ultimate power of templating. You can read more about it here.

yarn add --dev plop

Now let's move into the generators directory.

  • Create an index.js inside it and add the following lines.
const componentGenerator = require('./component');
const screenGenerator = require('./container');

module.exports = plop => {
  plop.setGenerator('component', componentGenerator);
  plop.setGenerator('screen', screenGenerator);
};

/* You may add multiple generators based on your requirements. */
  • Now let's create these generators one by one.

  • Component Generator:

  • Create a folder called component.

  • Add an index.js, an index.tsx.hbs and a styles.tsx.hbs file inside it.
  • And add the following:

    index.js :

/* eslint-disable quotes */
const componentExists = require('../checks');

module.exports = {
  description: 'Creates a Global Component',
  prompts: [
    {
      type: 'input',
      name: 'name',
      message: 'Name of the component: ',
      default: 'Button',
      validate: name => {
        if (/.+/.test(name)) {
          return componentExists(name)
            ? 'A component or container with this name already exists'
            : true;
        }

        return 'The name is required';
      },
    },
  ],
  actions: [
    {
      type: 'add',
      path: 'your_path_to_components/{{pascalCase name}}/index.tsx',
      templateFile: './component/index.tsx.hbs',
    },
    {
      type: 'add',
      path: 'your_path_to_components/{{pascalCase name}}/styles.tsx',
      templateFile: './component/styles.tsx.hbs',
    },
    {
      type: 'append',
      path: 'your_path_to_components/index.ts',
      pattern: /(\/\/ COMPONENT IMPORTS)/g,
      template: `import {{pascalCase name}} from './{{pascalCase name}}';`,
    },
    {
      type: 'append',
      path: 'your_path_to_components/index.ts',
      pattern: /(\/\/ COMPONENT EXPORTS)/g,
      template: `  {{pascalCase name}},`,
    },
  ],
};

index.tsx.hbs:

import React, {PureComponent} from "react";
import {View, Text} from "react-native";
import styles from "./styles";

export interface Props {}

export interface State {}

class {{pascalCase name}} extends PureComponent<Props, State> {
    render() {
        return (
            <View style={styles.container}>
                <Text>
                    {{pascalCase name}}
                </Text>
            </View>
        )
    }
}

export default {{pascalCase name}};

styles.tsx.hbs:

import {StyleSheet, ViewStyle} from 'react-native';

interface Styles {
  container: ViewStyle;
}

const styles = StyleSheet.create<Styles>({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default styles;

Now you are done with the components. Now we need to also add a checker to check if the component or file we are creating already exists or not. For that. Create a checks directory within generators.

And add the following line.

const fs = require('fs');
const path = require('path');

const appComponents = fs.readdirSync(
  path.join(__dirname, '../../../app/components'),
);
const appContainers = fs.readdirSync(
  path.join(__dirname, '../../../app/screens'),
);

const components = appComponents.concat(appContainers);

function componentExists(comp) {
  return components.indexOf(comp) >= 0;
}

module.exports = componentExists;
  • Now let's create the screen generators. Follow the similar steps as of components.
  • Create a container folder. And add a index.js file. Additionally, add class.tsx.hbs, function.tsx.hbs, styles.tsx.hbs, actions.ts.hbs, reducer.ts.hbs, saga.ts.hbs, action.types.ts.hbs .
  • Now add the following code.

index.js:

const componentExists = require('../checks');

module.exports = {
  description: 'Creates an App Screen',
  prompts: [
    {
      name: 'name',
      message: 'Name of the app screen: ',
      default: 'HomeScreen',
      validate: name => {
        if (/.+/.test(name)) {
          return componentExists(name)
            ? 'A component or container with this name already exists'
            : true;
        }

        return 'The name is required';
      },
    },
    {
      type: 'list',
      name: 'input',
      message: 'Choose component type: ',
      choices: ['class', 'function'],
      default: 'class',
    },
    {
      type: 'confirm',
      name: 'wantActionsAndReducer',
      default: true,
      message:
        'Do you want an actions/constants/selectors/reducer tuple for this container?',
    },
    {
      type: 'confirm',
      name: 'wantSaga',
      default: true,
      message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)',
    },
  ],
  actions: res => {
    const actions = [
      {
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/styles.tsx',
        templateFile: './container/styles.tsx.hbs',
        abortOnFail: true,
      },
    ];
    if (res.input === 'function') {
      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/index.tsx',
        templateFile: './container/function.tsx.hbs',
        abortOnFail: true,
      });
    }

    if (res.input === 'class') {
      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/index.tsx',
        templateFile: './container/class.tsx.hbs',
        abortOnFail: true,
      });
    }

    if (res.wantActionsAndReducer) {
      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/actions.ts',
        templateFile: './container/actions.ts.hbs',
        abortOnFail: true,
      });

      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/action.types.ts',
        templateFile: './container/action.types.ts.hbs',
        abortOnFail: true,
      });

      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/type.d.ts',
        templateFile: './container/type.d.ts.hbs',
        abortOnFail: true,
      });

      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/selectors.ts',
        templateFile: './container/selectors.ts.hbs',
        abortOnFail: true,
      });

      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/reducer.ts',
        templateFile: './container/reducer.ts.hbs',
        abortOnFail: true,
      });
    }
    if (res.wantSaga) {
      actions.push({
        type: 'add',
        path: '../../app/screens/{{pascalCase name}}/saga.ts',
        templateFile: './container/saga.ts.hbs',
        abortOnFail: true,
      });
    }
    return actions;
  },
};

action.types.ts.hbs:

/*
 *
 * {{ properCase name }} action types
 *
 */

const {{ properCase name }}ActionTypes = {};

export default {{ properCase name }}ActionTypes;

action.ts.hbs:

/*
 *
 * {{ properCase name }} actions
 *
 */

 import * as actionTypes from "./action.types";

class.tsx.hbs:

import React, {PureComponent} from "react";
import {View, Text} from "react-native";
import styles from "./styles";


interface Props {}

interface State {}

class {{pascalCase name}} extends PureComponent<Props,State> {
    render() {
        return(
            <View style={styles.container}>
                <Text>{{pascalCase name}}</Text>
            </View>
        )
    }
}

export default {{pascalCase name}};

function.tsx.hbs:

import React, {memo, FC} from "react";
import {View, Text} from "react-native";
import styles from "./styles";


interface Props {}

const {{pascalCase name}}: FC<Props> = (props) => {
 return (
     <View style={styles.container}>
         <Text>{{pascalCase name}}</Text>
     </View>
 )
};

export default memo({{pascalCase name}});

reducer.ts.hbs:

import * as actionTypes from "./action.types";

/* Mention the types */
export const INITIAL_STATE = {};

/* Mention the types */
const {{ camelCase name }}Reducer = (state = INITIAL_STATE, action) => {
    switch (action.type) {
        default: 
            return state;
    }
 };

 export default {{ camelCase name }}Reducer;

saga.ts.hbs:

/*
 Add your Saga to the root saga
*/

import { take, call, put, select, all } from 'redux-saga/effects';
// import axios from "../../config/axios.config";


export function* {{ camelCase name }}Saga() {
  yield all([
    // call all other sagas here.
  ]);
}

selectors.ts.hbs:

// {{properCase name }} selectors

styles.tsx.hbs:

import {StyleSheet, ViewStyle} from 'react-native';

interface Styles {
  container: ViewStyle;
}

const styles = StyleSheet.create<Styles>({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default styles;

types.d.ts:

/*
 *
 * {{ properCase name }} types
 *
 */

Now, you are done with your generators. Let's add it to your package.json, so that you can simply execute without having to write the complete script.

Add this script to your package.json:

"generate": "plop --plopfile src/generators/index.js"

Now, just run yarn generate or npm run generate to get the CLI options and you are good to go.

See ya'. Happy Coding. Any doubts, reach out to me. :)

Stay Safe, put on a Mask and Sanitize yourself as much as you can! ๐Ÿ˜Š

Like what you read? Support me by making a small donation here. ๐Ÿ˜‡

Coding GIF

ย