Maleo.js is an un-opinionated framework to enable Universal Rendering in JavaScript using React with no hassle.
We are here to solve the time consuming setups Universal Rendering Required.
Readme below is the documentation for the canary
(prerelease) branch. To view the documentation for the latest stable Maleo.js version change branch to master
- Features
- Setup
- Component Lifecycle
- Routing
- Dynamic Import Component
- Customizable Component
- Custom Configuration
- Core Utilities Functions
- CDN Support
- Plugins
- FAQ
- Contributing
- Universal Rendering
- Plugin based framework
- Customizable
Install Maleo.js
for now change
@airy/maleo
to@airy/maleo@canary
until we publish the stable version of this package
NPM
$ npm install --save @airy/maleo react
Yarn
$ yarn add @airy/maleo react
Add this script to your package.json
{
"scripts": {
"dev": "maleo dev",
"build": "export NODE_ENV=production && maleo build",
"start": "export NODE_ENV=production && maleo run"
}
}
Create a page Root component
// ./src/Root.jsx
import React from 'react';
// Export default is required for registering page
export default class RootComponent extends React.Component {
render() {
return (
<h1>Hello World!</h1>
)
}
}
And lastly, create a routing file on your project root directory called routes.json
and register your page component
[
{
"path": "/",
"page": "./src/Root"
}
]
After that you can now run $ npm run dev
and go to http://localhost:3000
.
You should now see your app running on your browser.
By now you should see
- Automatic transpilation and bundling (with webpack and babel)
Hot code reloading#17- Server rendering
To see how simple this is, check out the sample app!
Maleo.js added a new component lifecycle hook called getInitialProps
, this function is called during Server Side Rendering (SSR) and during route changes on Client Side Rendering (CSR).
This is useful especially for SEO purposes.
Example for stateful component:
import React from 'react';
export default class extends React.Component {
static getInitialProps = async (ctx) => {
const { req } = ctx;
// check if getInitialProps is called on server or on client
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
// the return value will be passed as props for this component
return { userAgent };
}
render() {
return (
<div>
Hello World {this.props.userAgent}
</div>
);
}
}
Example for stateless component:
const Component = (props) => (
<div>
Hello World {props.userAgent}
</div>
);
Component.getInitialprops = async (ctx) => {
const { req } = ctx;
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
return { userAgent };
};
export default Component;
getInitialProps
receives a context object with the following properties:
req
- HTTP request object (server only)res
- HTTP response object (server only)...wrapProps
- Spreaded properties from custom Wrap...appProps
- Spreaded properties from custom Appmatched
- Matched routes contains list of object, which is consists ofmatch
object androute
objectroute
- Component's route attributes onroutes.json
file
Routing is declared in a centralized route config.
Register all the route config in routes.json
file.
If you put the routes.json
files on root directory, Maleo will automatically register your route. Otherwise put path to your routes on Maleo config.
Routes file has to export default the route configuration. The route object expected to have distinct key to indicate the route.
Key | Type | Description |
path |
String! |
Routes path |
page |
String! |
Path to React Component for this route path |
exact |
Boolean? [false ] |
To make url has to match exactly the path in order to render the component. Give false value if the route is a wrapper route component |
routes |
RouteObject? |
Nested route |
For example:
[
{
"page": "./src/MainApp",
"routes": [
{
"path": "/",
"page": "./src/Search",
"exact": true
},
{
"path": "/search",
"page": "./src/Search",
"routes": [
{
"path": "/search/hello",
"page": "./src/Detail",
"exact": true
}
]
},
{
"path": "/detail",
"page": "./src/Detail",
"exact": true
}
]
}
]
Maleo.js supports TC39 dynamic import proposal for JavaScript.
You can think dynamic import as another way to split your code into manageable chunks. You can use our Dynamic
function which utilizes react loadable
For Example
// DynamicComponent.js
import Dynamic from '@airy/maleo/dynamic';
export default Dynamic({
loader: () => import( /* webpackChunkName:"DynamicComponent" */ './component/DynamicLoad'),
})
For optimization purposes, you can also preload a component even before the component got rendered.
For example, if you want to load component when a button get pressed, you can start preloading the component when the user hovers the mouse over the button.
The component created by Dynamic
exposes a static preload
method.
import React from 'react';
import Dynamic from '@airy/maleo/dynamic';
const DynamicBar = Dynamic({
loader: () => import('./Bar'),
loading: LoadingCompoinent
});
class MyComponent extends React.Component {
state = { showBar: false };
onClick = () => {
this.setState({ showBar: true });
};
onMouseOver = () => DynamicBar.preload();
render() {
return (
<div>
<button
onClick={this.onClick}
onMouseOver={this.onMouseOver}>
Show Bar
</button>
{ this.state.showBar && <DynamicBar /> }
</div>
)
}
}
Highly inspired by what Next.js has done on their awesome template customization.
Maleo.js also enable customization on Document
as document's markup. So you don't need to include tags like <html>
, <head>
, <body>
, etc.
To override the default behavior, you'll need to create a component that extends the Document
React class provided by Maleo.
// document.jsx
import React from 'react';
import { Document, Header, Main, Scripts } from '@airy/maleo/document';
export default class extends Document {
render() {
return (
<html>
<Header>
<title>Maleo JS</title>
<meta charset="utf-8" />
<meta name="description" content="Maleo.js is awesome!" />
<style>
{` body { background-color: #fff } `}
</style>
</Header>
<body>
<Main />
<Scripts />
<ReduxScript />
</body>
</html>
);
}
}
Maleo.js uses the Wrap
component to initialize pages. Wrap
contains React Router's Component. You can add HoC here to wrap the application and control the page initialization. Which allows you to do amazing things like:
- Persisting layour between page changes
- Keeping state when navigating pages
- Custom error handling using
componentDidCatch
- Inject additional data into pages (like Redux provider, etc)
To override the default behavior, you'll need to create a component that extends the Wrap
React class provided by Maleo.
// wrap.jsx
import React from 'react';
import { Wrap } from '@airy/maleo/wrap';
// Redux plugin for Maleo.js
// Hoc that creates store and add Redux Provider
import { withRedux } from '@airy/maleo-redux-plugin';
// Custom Wrapper that will be rendered for the whole Application
import Content from './component/Content';
import NavigationBar from './component/NavigationBar';
import { createStore } from './store';
@withRedux(createStore)
export default class extends Wrap {
static getInitialProps = (ctx) => {
const { store } = ctx
// you receive store from context
// you can access or do something with the store here
console.log('Initialized Redux Store', store);
return {}
}
render() {
const { Container, containerProps, App, appProps } = this.props
return (
<Container {...containerProps}>
<NavigationBar />
<Content>
<App {...appProps}/>
</Content>
</Container>
)
}
}
If you put document.jsx
and wrap.jsx
on root directory (the same level with package.json
), then Maleo will automatically register your custom Document and Wrap. Otherwise, you can add the path to your custom Document and Wrap on Maleo config
We are also working on adding default and customizable Error
component page
For more advanced configuration of Maleo.js, like webpack
config, registering plugins
, path to your routes, custom Document and Wrap, and adding path alias
, you can create a maleo.config.js
in the root of your project directory. (same directory with package.json
)
// maleo.config.js
module.exports = {
/* config options here */
}
Here are the API's for the configuration:
Key | Type | Description |
favicon |
String [{project-dir}/favicon.ico ] |
Path to favicon file |
buildDir |
String? [.maleo ] |
Directory to put Maleo.js' build assets |
cache |
Boolean? [true ] |
Enable webpack build caching |
isDev |
Boolean? [process.env.NODE_ENV === 'development' ] |
Enable development build configuration |
sourceMaps |
Boolean? [true ] |
Enable webpack to generate source maps |
alias |
Object? |
A key value pair for aliasing path directory
{ 'component': './src/component' }
|
publicPath |
String? [/_assets/ ] |
To customize webpack's publicPath
Comes in handy if using CDN to put built assets |
analyzeBundle |
Boolean? [false ] |
To enable webpack's bundle analyzer, for analyzing bundle sizes during bundle debugging should Maleo.js' build process got slow |
webpack |
Function? |
To customize webpack configuration, more details here |
routes |
string? [rootDir/routes.jsx ] |
Path to your routes file |
customDocument |
string? [rootDir/document.jsx ] |
Path to your custom document file |
customWrap |
string? [rootDir/wrap.jsx ] |
Path to your custom wrap file |
customApp |
string? [rootDir/app.jsx ] |
Path to your custom app file |
csp |
boolean | cspConfig [false ] |
Config for Content Security Policy, using Helmet-CSP
If set to true it will enable default config: {
directives: {
defaultSrc: [`'self'`],
styleSrc: [`'self'`],
},
}
|
gzip |
boolean? [true ] |
Enable GZIP Compression |
Create a server.js
file on root directory where your package.json
lives.
Here you can customize Maleo's server.
import { Server } from '@airy/maleo/server';
const PORT = process.env.PORT || 3000;
const maleoServer = Server.init({
port: PORT,
runHandler: () => {
console.log('Server running on port :', PORT);
}
});
maleoServer.run();
Here are the API's for the configuration:
Key | Type | Description |
port |
Number? [3000 ] |
Port to run Maleo server |
assetDir |
String? ['/.maleo/client' ] |
Directory for all client related assets |
runHandler |
Function? |
Function called when maleo server starter |
You are able to extend Maleo.js' default webpack configuration by defining a function on maleo.config.js
// maleo.config.js
module.exports = {
webpack(config, context, next) {
// Perform customizations to webpack config
// Important: This call is required in order to let Maleo pass the config to webpack
return next();
},
};
Webpack function will receive three arguments:
Argument | Details | |||||||||||||||
config |
This contains webpack configuration object that you can manipulate | |||||||||||||||
context |
This contains some keys that are useful for the build context
|
|||||||||||||||
next |
A callback required to be called to pass the custom configuration | |||||||||||||||
Example of adding ts-loader
through maleo.config.js
:
// maleo.config.js
// Partly taken and modified from @airy/maleo-ts-plugin source
// for simplicity purposes
module.exports = {
webpack(config, context, next) {
const { isDev } = context
config.module.rules.push({
test: /\.tsx?/,
exclude: /node_modules/,
use: [
require.resolve('@airy/maleo/lib/build/loaders/maleo-babel-loader'),
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
if (isDev) {
config.plugins.push(new ForkTSCheckerWebpackPlugin());
}
return next();
})
},
};
Maleo.js also let you have your own babel config. Just simply add .babelrc
file at the root directory of your app.
You can include Maleo.js' babel preset in order to have latest JavaScript preset.
Here's an example of .babelrc
file:
{
"presets": ["@airy/maleo/babel"],
"plugins": []
}
The @airy/maleo/babel
preset includes everything you need to get your development started. The preset includes:
@babel/preset-env
@babel/preset-react
@babel/plugin-proposal-class-properties
@babel/plugin-proposal-decorators
@babel/plugin-proposal-object-rest-spread
@babel/plugin-transform-runtime
react-loadable/babel
To use you can import the matching routes function like this:
import { getMatchedRoutes } from '@airy/maleo/utils';
The function will return all matched routes and the route object from routes.json
If you are using a CDN, you can set up the publicPath
setting and configure your CDN's origin to resolve to the domain that Maleo.js is hosted on.
// maleo.config.js
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
assetPrefix: isProd && 'https://cdn.example.com';
}
== TO BE DETERMINED ==
Please follow these steps to contribute to Maleo.js
Please follow these steps to contribute to Maleo.js' plugins
This project exists thanks to all the people who contribute. Contribute.
Many thanks to our contributors!
MIT