This project was built from a MERN Starter kit (original URLs broken). Discussions are still active here. MERN is Mongo, Express, React, and NodeJS.
See the JavaScript language guide for CircleCI here.
npm install -g mern-cli
mern init your_new_app
cd your_new_app
npm install
npm start
Note: Please make sure your MongoDB is running. For MongoDB installation guide see this. Also npm3
is required to install dependencies properly.
-
npm run start
- starts the development server with hot reloading enabled -
npm run bs
- bundles the code and starts the production server -
npm run test
- start the test runner -
npm run watch:test
- start the test runner with watch mode -
npm run cover
- generates a test coverage report -
npm run lint
- runs linter to check for lint errors
MERN uses Webpack for bundling modules. There are four types of Webpack configs provided webpack.config.dev.js
(for development), webpack.config.prod.js
(for production), webpack.config.server.js
(for bundling server in production), and webpack.config.babel.js
(for babel-plugin-webpack-loaders for server rendering of assets included through webpack).
The Webpack configuration is minimal and beginner-friendly. You can customize and add more features to it for a production build.
MERN uses an express web framework. Our app sits in server.js where we check for NODE_ENV.
If NODE_ENV is developed, we apply Webpack middlewares for bundling and Hot Module Replacement.
We use React Router's match function for handling all page requests so that browser history works.
All the routes are defined in client/routes.js
. React Router renders components according to route requested.
// Server Side Rendering based on routes matched by React-router.
app.use((req, res) => {
match({
routes,
location: req.url
}, (err, redirectLocation, renderProps) => {
if (err) {
return res.status(500).end('Internal server error');
}
if (!renderProps) {
return res.status(404).end('Not found!');
}
const initialState = {
posts: [],
post: {}
};
const store = configureStore(initialState);
fetchComponentData(store.dispatch, renderProps.components, renderProps.params).then(() => {
const initialView = renderToString(
<Provider store = {store} >
<RouterContext {...renderProps}/>
</Provider>
);
const finalState = store.getState();
res.status(200).end(renderFullPage(initialView, finalState));
}).catch(() => {
res.end(renderFullPage('Error', {}));
});
});
});
match
takes two parameters, first is an object that contains routes, location, and history and second is a callback function which is called when routes have been matched to a location.
If there's an error in matching we return a 500 status code, if no matches are found we return a 404 status code. If a match is found then, we need to create a new Redux Store instance.
Note: A new Redux Store has populated afresh on every request.
fetchComponentData
is the essential function. It takes three params: first is a dispatch function of Redux store, the second is an array of components that should be rendered in the current route and the third is the route params. fetchComponentData
collects all the needs (need is an array of actions that are required to be dispatched before rendering the component) of components in the current route. It returns a promise when all the required actions are dispatched. We render the page and send data to the client for client-side rendering in window.__INITIAL_STATE__
.
The client directory contains all the shared components, routes, and modules.
This folder contains all the common components which are used throughout the project.
Index.js simply does client-side rendering using the data provided from window.__INITIAL_STATE__
.
Modules are the way of organizing different domain-specific modules in the project. A typical module contains the following
| - Post
| - __tests__ // all the tests for this module goes here
| - components // Sub components of this module
| - Post.spec.js
| - PostList.spec.js
| - PostItem.spec.js
| - PostImage.spec.js
| - pages
| - PostPage.spec.js
| - PostViewPage.spec.js
| - PostReducer.spec.js
| - PostActions.spec.js
| - components // Sub components of this module
| - Post.js
| - PostList.js
| - PostItem.js
| - PostImage.js
| - pages // React Router Pages from this module
| - PostPage
| - PostPage.js
| - PostPage.css
| - PostViewPage
| - PostViewPage.js
| - PostViewPage.css
| - PostReducer.js
| - PostActions.js
Assets can be kept where you want and can be imported into your js files or CSS files. Those fill be served by webpack in development mode and copied to the dist folder during production.
We use Babel to transpile code in both server and client with stage-0
plugin. So, you can use both ES6 and experimental ES7 features.
There are docker configurations for both development and production.
To run docker for development,
docker-compose -f docker-compose-development.yml build
docker-compose -f docker-compose-development.yml up
To run docker for production,
docker-compose build
docker-compose up
In this version, we enabled the mern-cli
to clone not only this project but also the variants of mern-starter
like one project with MaterialUI or JWT auth. To make your version of MERN, follow these steps
-
Clone this project
git clone https://github.com/Hashnode/mern-starter
-
Make your changes. Add a package, add authentication, modify the file structure, replace Redux with MobX, or anything else.
-
In this version, we also added code generators. Blueprints for those generators are located at
config/blueprints
, and config is located atmern.json
. Make sure to edit them if necessary after your made modifications in the previous step. There is a section below which explains how to modify generators. -
Next clone
mern-cli
projectgit clone https://github.com/Hashnode/mern-cli
-
Add your project details to
variants.json
in the cloned project and send a pull request.
It contains a blueprint array. Each object in it is the config for a generator. A blueprint config contains the name, description, usage, and file array. An example blueprint config
{
"name": "dumb-s",
"description": "Generates a dumb react component in shared components",
"usage": "dumb-s [component-name]",
"files": [
{
"blueprint-path": "config/blueprints/dumb-component.ejs",
"target-path": "client/components/<%= helpers.capitalize(name) %>.js"
}
]
}
A file object contains
-
blueprint-path
- location of the blueprint file -
target-path
- the location where the file should be generated -
parent-path
- optional parameter, used if you want to generate the file inside an already existing folder in your project.
Also, target-path
supports ejs and the following variables will be passed while rendering,
-
name
-<component-name>
input from the user -
parent
- in particular special cases where you need to generate files inside an already existing folder, you can obtain this parent variable from the user. A config using that will look like this,{ "name": "dumb-m", "description": "Generates a dumb react component in a module directory", "usage": "dumb-m <module-name>/<component-name>", "files": [ { "blueprint-path": "config/blueprints/dumb-component.ejs", "parent-path": "client/modules/<%= helpers.capitalize(parent) %>", "target-path": "components/<%= helpers.capitalize(name) %>/<%= helpers.capitalize(name) %>.js" } ] }
Here, notice the usage. In
<module-name>/<component-name>
,<module-name>
will be passed asparent
, and<component-name>
will be passed as<name>
. -
helpers
- a helper object is passed which includes common utility functions. For now, it containscapitalize
. If you want to add more, send a PR to mern-cli.
Blueprints are basically ejs templates that are rendered with the same three variables(name
, optional parent
, and helpers
object) as above.
To make the hot reloading of CSS work, we are not extracting CSS in development. Ideally, during server rendering, we will be extracting CSS, and we will get a .css file, and we can use it in the html template. That's what we are doing in production.
In development, after all, the scripts get loaded, react loads the CSS as BLOBs. That's why there is a second FOUC in development.
This warning is visible only in development and totally harmless. This occurs to hash difference in react-router
. To solve it, react-router docs ask you to use the match
function. If we use match
, react-hot-reloader
stops working.
MERN is released under the MIT License.