Cover image: Bunbeg Beach, Co. Donegal, Ireland
When I started writing React applications all the codebase was under a single repository. No code sharing, no context separation.
As soon as I gain interested in exploring new solutions, I wanted to build a small dashboard at home as a playground to test new libraries, React hooks or integration with other frameworks like Vue.
Creating the Dashboard backbone was a straightforward operation: a few components for the scaffold, a basic authentication module, a Routes component with few switch cases and an apps
folder to contain the different projects.
Coming from a Python/Django background, I wanted to organize my code in different repositories as I was doing with all my previous projects. Unfortunately this operation has not been as straightforward as expected.
In Python there are a couple of tools which I am familiar with and that helped me to manage this requirement: virtualenvwrapper
is one of these.
One of its functionalities (add2virtualenv
) is the ability to link different repositories together under the same environment and still being able to modify them without any re-installation or deployment - another option would be pip install -e
from the repository folder.
Unfortunately, it's not the same with npm/yarn and create-react-app
; they both allows to link
but each repository must resolve it's own dependencies and have them installed.
Project structure
The currenct project structure is based on the standard create-react-app
example.
package.json
src/
apps/
...
libs/
...
scaffold/
...
App.js
index.js
My idea was to share the code inside libs
folder to all the applications and to keep each application in it's own repository.
The first thing I tried was yarn link
, but it didn't worked well. This approach assumes that the code inside the application is already packaged with it's own dependencies resolved.
Something was missing!
Avoid ejecting
The first thing I did was to eject
to explore the configuration opportunities.
This is actually a good way to test out configurations if you can revert (which is pretty easy if you are under a version controlled folder).
Ejecting (and keeping each config file) however is not my preferred solution because when you go manual for a long period and starts to customize a lot of things you can't easily go back, and you need to maintain the dependencies one by one.
On the opposite side, create-react-app
does not allow you to modify the configuration files in order to keep the project as generic and standard as possible.
Few solutions has been suggested on the web: switch to razzle.js, use next.js, rethink the project structure with lerna (the monorepo approach - one owner only) or fork create-react-app
.
In order to keep the project simple I didn't want to introduce next.js or razzle.js. I hope to reuse my dashboard code for other projects and using a framework might not be the best solution.
Fork
Of all solutions I opted for forking create-react-app
repository, following this guide.
On react-scripts
package, I added the following lines to config/webpack.config.js
.
- saving the content to a variable instead of returing the configuration directly:
// remove the return statement and save the content
- return {
+ let config = {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
- checking if a file called
customWebpack.config.js
exists in the project root folder and, if it has amodify
function, override the config with the function's result:
// our own hints via the FileSizeReporter
performance: false,
};
+ console.log('Checking for custom webpack config');
+ if (fs.existsSync('./customWebpack.config.js')) {
+ console.log(' -- custom config found!');
+ const customWebpackConfig = require(path.resolve(
+ __dirname,
+ '../../../../customWebpack.config'
+ ));
+ if (customWebpackConfig.modify) {
+ config = customWebpackConfig.modify(config, { webpackEnv });
+ }
+ }
+
+ return config;
};
This similar approach is used by razzle
(in a much fancier way).
On my project then I had to do three things:
- Adding a
customWebpack.config.js
file:
module.exports = {
modify: (config, { webpackEnv }) => {
// List of external repositories that have to be added
// to the testers to being correctly processed
let externalRepositories = [];
if (process.env.REACT_APP_EXTERNAL_REPOSITORIES) {
externalRepositories = process.env.REACT_APP_EXTERNAL_REPOSITORIES.split(',');
}
// Set a list of repositories required for this project
const projectRepositories = [
'my-test-repo'
];
// Validate that all repositories have been set before starting
projectRepositories.forEach(repo => {
if (externalRepositories.filter(eRepo => eRepo.endsWith(repo)).length !== 1)
throw new Error(`==> Repository ${repo} must be included in ` +
`.env.local REACT_APP_EXTERNAL_REPOSITORIES variable`);
});
config.module.rules[2].oneOf.forEach((test, index) => {
if (test.include) {
config.module.rules[2].oneOf[index].include = [
...(Array.isArray(test.include) ? test.include : [test.include]),
...externalRepositories,
];
}
});
}
return config;
};
- add the repositories to
REACT_APP_EXTERNAL_REPOSITORIES
in .env.local file:
REACT_APP_EXTERNAL_REPOSITORIES=~/repositories/my-test-repo
- and finally created a link
ln -s ~/repositories/my-test-repo dashboard-ui/src/apps/
Final considerations
This approach is not a standard approach in JS/React applications/modules development, but allows me to have the advantages I have when I develop python applications:
- every module is in it's own repository: different repositories might have different visibility, permissions, follow different merging strategies and have it's own wiki (or even different team);
- changes are picked up immediately by CRA without extra compile steps;
- the application is bundled from one single point (however, this is also a disadvantage in terms of CI/CD, but
node_modules
can be cached to speed up build operations).
There are also some disadvantages, but for my pipeline they are not a problem:
- as mentioned, one change in a single repository requires the full application to be bundled again. A mixed approach can be adopted by adding the external repositories only in development leveraging
webpackEnv
variable, building single modules with their own pipeline; - setup for a new repository is not straightforward: we need to create a link and add the repository to an env variable (this might be automated as well), and we need to do this in order to build.
Can you see a simpler way to achieve this result? You think this is a terrible approach? Please share your opinion!
Top comments (0)