2.1.1 • Published 4 years ago

@gtdudu/react-gen-routes v2.1.1

Weekly downloads
1
License
MIT
Repository
github
Last release
4 years ago

An easy way to manage client routing for react app similar to the one in next.js.
Automates creation of react-router-config config file based on file tree.
Automatic reloading on file system events.

Table of contents

Why

If you try to create a react app that works with server side rendering you will probably end up using react-router-config. This is fine but as soon as you add more routes the config file will grow and become more and more obscure.

If you've checked out next.js you've seen that they automatically manage routing based on a special directory architecture (pages/). I very much like this idea as it makes it really easy to understand what's going on with just a few simple rules!

This package attempts to replicate this feature and only this features.
Everything else is up to you.

So why use this ?

  • does only one job, create the routes config file
  • customize input/output directory
  • does not wrap react functions
  • support nested routes
  • support catch all routes

Installation

npm install --save-dev @gtdudu/react-gen-routes

Usage

gt-rgr [args]

  • Mandatory args:

    • -i or --inputDir: relative path from project root to pages folder
    • -o or --outputDir: relative path from project root to config output folder
  • Optional args:

    • -e or --extensions: coma separated list of accepted extensions, default to 'js'
    • -f or --filename: whatever you want the config file to be named, default to routes.js
    • -t or --templatesDir: path to component and imports templates folder
    • -w or --watch: boolean, set to true for automatic recomputing of config file on inputDir file system events (via chokidar), default to false
    • -k or --keywords: coma separated list of exports to search for in each component

Enable logs

You need to set DEBUG env var to rgr to enable logs.

Using --templatesDir option

This package assumes you're using @loadable/component for code splitting.
Each route will have a component field that looks like this:

  component: loadable(props => import('./pages/index.js')),

If that does not suit your needs you can specify the path to a templates folder holding two files:

  • imports

By default our imports template looks like this.

import loadable from '@loadable/component';

This will be appended on top of the routes config file.
This can be as many lines as you want.

  • component

By default our component template looks like this.

component: loadable(props => import('<%= componentPath %>')),
i18n: loadable(props => import(`<%= componentRootPath %>/<%= name %>.i18n.${props.language}.js`)),

This template will be populated using ejs templating.

Available variables are:

  • componentPath: full relative path to file from routes config
  • componentRootPath: full relative path to folder where component file is located
  • filename: name of component file with extension
  • name: name without extension
  • extension: extension

This file can be as many lines as you want, they will be added to every route object.

Avoid using field path, exact and routes as this lib already takes care of those.
Also, you need to have a field component in order for routes config to be correct.

Using --watch option

If you use --watch the package will use chokidar to listen for any file system events in input directory and recompute routes config accordingly.
This watch is blocking so you need to use npm-run-all or concurrently to run both your server and the watch.
It's also a good idea to wait for config file to be created before starting anything that relies on it.. you can use wait-on for this.

I tend to use this:

// In packages.json scripts

"dev": "concurrently -r \"npm:watch:server\" \"npm:watch:routes\"",
"watch:routes": "gt-rgr -o ./src/shared -i ./src/shared/pages -w -f my-routes.js",
"watch:server": "wait-on ./src/shared/my-routes.js && [start server/nodemon/whatever here]",

// don't forget to install deps

npm i --save-dev concurrently
npm i --save-dev wait-on

Using --keywords option

If set, each component file will be parsed using babel to search for exported variables matching values in keywords list.

If keyword references a function a boolean flag will be set on the route object. If keyword references a Boolean, Integer, String, or an array of the above, value will be extracted and set on the route object.

Files will be parsed using preset "@babel/preset-react",.

For, instance:

Let's say your directory structure looks like:

pages/
  index.js

and index.js is:

export default Home() {
  return (
    <div>
      ...
    </div>
  );
}

export roles = ['admin', 'user'];

export function getInitialData() {

}

if you run with -k getInitialData,roles then the routes config will be:

import loadable from '@loadable/component';


const routes = {
  routes: [
    {
      roles: ['admin', 'user'],
      getInitialData: true,
      component: loadable(props => import('...relativePathToCmp here...')),
      path: '/',
      exact: true
    }
  ]
}

export default routes;

This is especially useful if you want to wrap your routes config to handle data fetching automatically without having to use a lib like react-async in each component!

It's also convenient to wrap authentication and authorisation.

This can also be used to do statical analysis on routes file afterward.

Babel must be able to parse your files.

You should check for es6 support for your node version before using this (node-green)

If watch mode is on, the results will be cached to enhance performance.

Name convention

Accepted names

You cannot name a file *.js nor a folder *.
If you do they will be ignored.

Furthermore, all filenames with more than one dot will be ignored while constructing routes file.

pages/
  component.js            -> this is ok
  component.styles.js     -> this will be ignored

Dynamic file and folder

We need to distinguish dynamic file/folder from non-dynamic file/folder.
If this sounds complicated, trust me, it's not.
A dynamic file/folder has its name inclosed between brackets!

pages/
  not-dynamic.js
  [dynamic].js

Do not use any brackets for non dynamic files/folders.

// ALL those are considered incorect and will be will be ignored
[nop/
no]p/
n[op].js
[no]p.js
...

Routing

This section assumes you use --inputDir ./pages

Creating a static route

  • To create a static route just create a file in your inputDir
pages/
  home.js

// home component will be accessible on url /home
  • or a folder with an index.js file in it
pages/
  home/
    index.js

// index.js component will be accessible on url /home
  • or a folder with a file in it !
pages/
  home/
    a.js

// a.js component will be accessible on url /home/a
  • You get the idea...

Paths for index.js files are always computed from their parent folder.

pages/
  index.js

// index.js component will be accessible on url /

pages/
  /a
    /b
      index.js

// index.js component will be accessible on url /a/b !

Creating a dynamic route

  • Create a dynamic file
pages/
  [id].js

// [id].js component will be accessible on url /:id
  • or a dynamic folder with an index.js file in it
pages/
  [id]/
    index.js

// index.js component will be accessible on url /:id

The name of your dynamic file/folder matters.
Since we're not wrapping any react function, in the examples above you'll get back your parameter using regular react-router useParams function.
Like so:

  let { id } = useParams();

You can create only one dynamic file and one dynamic folder per directory
Extraneous dynamic files/folders will be ignored with no guarantee of which ones

Creating a catch all route

A catch all route is just like a dynamic routes.
To create one the convention is to name your file [*].
The only difference is that they resolve to path * and not :*

/pages/
	index.js  -> /
	[*].js 	  -> /*

Catch all can also be folders.

/pages/
	index.js  		-> /
	[*]/
		index.js	-> /*
		[id].js		-> /*/:id

Creating nested routes

  • To create a nested route you need a file (the wrapper) and a folder that share the same name!
pages/
  wrapper.js
  wrapper/
    // all components in here will be sub routes of wrapper.js
  • Nesting also works with dynamic files.
pages/
  [id].js                     -> /:id (exact: false)
  [id]/
    nested-component.js       -> /:id/nested-component (exact: true)
    [*].js 					  -> /:id/*
  • and with index.js files even tho this is a special case.

If used, all other files and folders in current directory will be ignored.
This is necessary because the wrapper needs exact: false to make nested routes working properly. Since indexes need to be first on their level this would prevent all other routes from ever showing up anyway.

pages/
  test.js                     -> will be ignored
  folder/
    index.js                  -> will be ignored
  index.js                    -> /
  index/
    index.js                  -> /
    nested-component.js       -> /nested-component

Do not forget to use react-router-config renderRoutes function in your wrappers !
Otherwise your childs won't be rendered.

Edge cases

  • Files implying duplicate routes will be ignored.
pages/
  [id].js
  [param]/
    index.js -> this file will be ignored
  • Avoid using different param names at the same level

Consider the following structure:

pages/
  [id].js
  [param]/
    a.js

[id].js would resolve to /:id a.js would resolve to /:param/a.js

This will work but it lacks clarity, it's much easier to follow if you do this instead:

pages/
  [id]/
    index.js
    a.js

index.js would resolve to /:id
a.js would resolve to /:id/a

It's virtually the same thing but you don't have to track two different names for the same params.

Contribute

If you have any idea to improve this project or any problem using it, feel free to open an issue.

License

This project was developed by Tommy DURAND and is released under the MIT License.