A framework for creating React + Redux apps.
Reactium follows the Domain Driven Design and aims to ease the creation of complex applications by connecting the related pieces of the software into an ever-evolving model.
DDD focuses on three core principles:
- Focus on the core domain and domain logic.
- Base complex designs on models of the domain.
- Constantly collaborate with domain experts, in order to improve the application model and resolve any emerging domain-related issues.
- Eases Communication: With an early emphasis on establishing a common and ubiquitous language related to the domain model of the project, teams will often find communication throughout the entire development life cycle to be much easier. Typically, DDD will require less technical jargon when discussing aspects of the application, since the ubiquitous language established early on will likely define simpler terms to refer to those more technical aspects.
- Improves Flexibility: Since DDD is based around modularity, nearly everything within the domain model will be based on an object and will, therefore, be quite encapsulated. This allows for various components, or even the entire system as a whole, to be altered and improved on a regular, continuous basis.
- Emphasizes Domain Over Interface: Since DDD is the practice of building around the concepts of domain and what the domain experts within the project advise, DDD will often produce applications that are accurately suited for and representative of the domain at hand, as opposed to those applications which emphasize the UI/UX first and foremost. While an obvious balance is required, the focus on domain means that a DDD approach can produce a product that resonates well with the audience associated with that domain.
- Requires Robust Domain Expertise: Even with the most technically proficient minds working on development, it’s all for naught if there isn’t at least one domain expert on the team that knows the exact ins and outs of the subject area on which the application is intended to apply. In some cases, domain-driven design may require the integration of one or more outside team members who can act as domain experts throughout the development life cycle.
- Encourages Iterative Practices: While many would consider this an advantage, it cannot be denied that DDD practices strongly rely on constant iteration and continuous integration in order to build a malleable project that can adjust itself when necessary. Some organizations may have trouble with these practices, particularly if their past experience is largely tied to less-flexible development models, such as the waterfall model or the like.
From your project directory:
$ npm run launch
The intent behind the Reactium Framework is to get you quickly creating React components and applications. With that in mind, we geared the tooling towards automation and ease of use.
There are 3 types of components you can create:
Create a Functional Component if your component will not need to worry about application state or events. Functional components accept a single props
argument and should be very clear and concise in their make-up.
const Hello = (props) => {
return (
<div>
<h1>Hey {props.name}!</h1>
</div>
);
}
The functional style components are nice and simple for purely presentational pieces.
Create a Class Component if your component will need the React Life Cycle Methods or internal state management.
import React, { Component } from 'react';
class Hello extends Component {
constructor(props) {
super(props);
this.state = Object.assign({}, this.props});
}
componentDidMount() {
console.log("Mounted Hello Component");
}
render() {
return (
<div>
<h1>Hey {this.state.name}!</h1>
</div>
);
}
}
Hello.defaultProps = {
name: "Bob"
};
export default Hello;
Create a Redux Class Component if your component will need to interact with the application state.
Redux Class Components work just like Class Components accept you will need to map state to properties and map dispatchers to actions via the react-redux connect
method.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions } from 'appdir/app';
// Map state to properties
const mapStateToProps = (state, props) => {
return Object.assign({}, state['Test'], props);
};
// Map dispatchers to actions
const mapDispatchToProps = (dispatch, props) => ({
test: {
click: () => dispatch(actions.Test.click()),
}
});
class Test extends Component {
constructor(props) {
super(props);
this.state = Object.assign({}, this.props);
}
componentWillReceiveProps(nextProps) {
this.setState((prevState) => {
return Object.assign({}, prevState, nextProps);
});
}
// Use the above mapped click dispatcher on button click
onClick() {
this.props.test.click();
}
render() {
return (
<div>
<div>{this.state.msg}</div>
<button type="button" onClick={this.onClick.bind(this)}>
Click Me
</button>
<div>{this.state.count || 0}</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);
Reactium component architecture is pretty simple when it comes to a function or class component.
- Create the component domain in the
~/src/app/components
directory. - Create an
index.js
file with your component code.
When it comes to a Redux Class Component the following architecture is applied:
File | Description |
---|---|
actions.js | List of action functions. See Redux Actions. Redux Super Thunk actions, based on the (https://github.com/gaearon/redux-thunk), are automatically supported. |
actionTypes.js | List of action filters. See Redux Actions. |
index.js | Main component class. |
reducers.js | Action handlers. See Redux Reducers. |
route.js | Route handler for the component. |
services.js | Ajax requests associated with the component. |
state.js | The default state of the component. |
Don't worry, there's a CLI command that automates component creation.
Note: You will need to restart the build after creating a new component with state, reducers, actions, actionTypes, etc.
Reactium aggregates all action.js
files into the actions
export of the app.js
file.
A typical actions.js
file may look like this:
import { actionTypes } from 'appdir/app';
export default {
mount: params => (dispatch) => {
dispatch({type: actionTypes.TEST_MOUNT, data: params});
},
click: () => (dispatch) => {
dispatch({type: actionTypes.TEST_CLICK});
},
};
To access the actions simply import the actions:
import { actions } from 'appdir/app';
Then use an action by targeting the component domain that created the action:
...
actions.Test.mount({some: "params"});
Q: What's this
appdir
thing all about?
A:
appdir
is a constant defined in the Webpack configuration that references the~/src/app
directory. It helps clarify where you're importing something from by eliminating the need to do something like:import { something } from '../../../components/SomeOtherComponent'
.
Reactium aggregates all actionTypes.js
files in the actionTypes
export of the app.js
file.
A typical actionTypes.js
file may look like this:
export default {
TEST_MOUNT: 'TEST_MOUNT',
TEST_CLICK: 'TEST_CLICK',
};
To access the actionTypes, import them into your component:
import { actionTypes } from 'appdir/app';
Usage:
...
dispatch({type: actionTypes.TEST_MOUNT, data: data});
Reactium aggregates all reducers.js
files into the Redux store using the react-redux combineReducers
method.
A typical reducers.js
file may look like this:
import { actionTypes } from 'appdir/app';
export default (state = {}, action) => {
let newState;
switch (action.type) {
case actionTypes.TEST_MOUNT:
newState = Object.assign({}, state, {...action.data});
return newState;
break;
case actionTypes.TEST_CLICK:
let count = state.count || 0;
newState = Object.assign({}, state, {count: count + 1});
return newState;
break;
default:
return state;
}
};
Reactium aggregates all route.js
files into a list of routes used to render React Router <Route />
components and observed by the the Reactium RouteObserver
component.
A route can have any property that is supported by <Route />
(e.g. path
, exact
, strict
, location
, component
, render
, and children
).
A typical route.js
file in MyComponent
may look like this:
// Import your component
import MyComponent from './index';
// Import the aggregated actions (optional)
import { actions } from 'appdir/app';
export default {
// Make this higher number to have route evaluated later (default 0)
order: 0,
// Route pattern that should route to
path: '/my-component/:paramA/:paramB',
// Should the Route be exact?
exact: true,
// the component to load for this route
component: MyComponent,
// (optional) a Redux thunk action to load data for this component
load: params => actions.MyComponent.mount(params),
};
In addition to <Route />
properties, you can also provide a Redux thunk action function to the load
property. The Reactium RouteObserver
component will automatically dispatch your load
thunk, passing along route parameters. Use this for asynchronous loading of data on observed route changes.
You can also support multiple routes for a module, in one of two methods:
- provide multiple paths for a single route
import MyComponent from './index';
import { actions } from 'appdir/app';
export default {
// both of these will resolve to this component
path: ['/first/path', '/second/path'],
exact: true,
component: MyComponent,
};
- provide multiple route objects
import MyComponent from './index';
import { actions } from 'appdir/app';
// export an array of routes
export default [
{
order: 1,
path: '/base-route',
exact: true,
component: MyComponent,
load: params => actions.MyComponent(params)
},
{
order: 0,
path: '/base-route/:param',
exact: true,
component: MyComponent,
load: params => actions.MyComponent(params)
},
];
Reactium aggregates all services.js
files into the services
export of the app.js
file.
A typical services.js
file may look like this:
import axios from 'axios';
import { restHeaders } from "appdir/app";
const restAPI = 'http://demo3914762.mockable.io';
const fetchHello = () => {
let hdr = restHeaders();
return axios.get(`${restAPI}/hello`, {headers: hdr}).then(({data}) => data);
};
const fetchGoodBye = () => {
let hdr = restHeaders();
return axios.get(`${restAPI}/goodbye`, {headers: hdr}).then(({data}) => data);
};
export default {
fetchHello,
fetchGoodBye,
}
In your actions.js file you would do something like:
import { actionTypes } from 'appdir/app';
import { services } from 'appdir/app';
export default {
mount: params => (dispatch) => {
services.Test.fetchHello().then((data) => {
dispatch({type: actionTypes.TEST_MOUNT, data: data});
});
},
}
Reactium uses axios to make XMLHttpRequests from the browser. You can swap that out with whatever you want.
To access the services, import them into your component:
import { services } from 'appdir/app';
Usage:
...
services.Test.fetchHello().then((result) => {
// Do something with the result
});
Reactium aggregates all state.js
files into the Redux store
for the application.
A typical state.js
file may look like this:
export default {
some: "value",
another: 1,
};
- Before creating a component, grab the Atomic Reactor CLI:
$ npm i -g atomic-reactor-cli
- Change directory into your project:
$ cd /MyAwesome/Project
- Input the new component command:
$ arcli re:gen class --open
Flags:
-n, --name <name> The name of the component.
-o, --overwrite [overwrite] Overwrite if the component already exists.
-p, --path [path] Absolute path to where the component is created. Default ~/src/app/components.
-c, --component [component] The parent component when creating a child component.
-r, --route [route] The route to associate with the component.
--open [open] Open the new file(s) in the default application.
--no-actions [actions] Exclude the actions.js file.
--no-types [types] Exclude the actionsTypes.js file.
--no-reducers [reducers] Exclude the reducers.js file.
--no-services [services] Exclude the services.js file.
--no-router [router] Exclude the route.js file.
--no-state [state] Exclude the state.js file.
-h, --help Output usage information.
- Follow the prompts:
- Name: The name of the component that you wish to create.
- Route: If you're building a
Single Page App
(SPA) and your new component uses the Router class, you'll want to specify theroute
that will navigate to this component. - Use Redux?: Determines if your new component's state will be managed by Redux.
Now that you've created your first component, it's time to use it.
Open the ~/src/index.html
file and add the component to the layout using the custom element <Component />
.
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Reactium</title>
<meta name="description" content="A framework for creating React + Redux apps using the domain driven design (DDD) paradigm.">
<link rel="stylesheet" href="/assets/style/style.css">
</head>
<body>
<Component type="Fubar"></Component>
<script src="/assets/js/main.js"></script>
</body>
</html>
Alternatively, you can just start using the component in your app like you would any other React component.
Reactium will now scan your markup for the <Component />
tags and bind the specified type
to the element.
You can pass initial state to the component via attributes but that's not necessary if you're using Redux for state management.
Reactium uses the combination of Gulp, Webpack 3, & Browsersync to build and run the development environment.
You might ask: Why not use just Webpack or just Gulp?
We identified 4 major requirements for our workflow:
- Bundling and transpiling Javascript.
- Moving assets from one location to another.
- Sass compilation and optimization.
- Hot reload of the development environment.
Without a doubt we felt that Webpack was the best solution for this requirement. The deciding factor was Webpack's dependency handling and tree shaking (removing unused modules).
Gulp provides a frictionless and very simple pattern for moving assets. The deciding factor was Gulp's ability to transport multiple file types to multiple destinations with very little setup or additional libraries or configuration.
Gulp's ability to compile Sass is pretty straight forward and doesn't require a lot of configuration. We found that with Webpack, this wasn't obviously clear and took a lot of configuration to get the right set up. Webpack also restricts the delivery of the compiled style sheet to a single destination, making it hard to save the file to multiple directories if needed.
Browsersync offers a large variety of simple configurations that allows you to serve the development environment locally. The deciding factor was Browsersync's ability to serve from a proxy instead of the application directory. This could come in handy if you need to run a node instance as well as your application within your development environment.
Source paths and destinations should be managed in the gulp.config.js
file, giving you a single place to alter build behaviors without directly altering Gulp Tasks.
Determines if this is a Single Page App or not.
If set to true
all .html routes will be redirected to index.html
.
If set to a valid url string, routes will be redirected to the specified url.
Default: false
The environment type: development/production.
Default: development
List of files that will be bundled and transpiled by Webpack.
Default: src/app/*.js
Top level .js files in the
~/src/app
directory
Babel browser support when transpiling.
Default: last 1 version
The previous and current version of all major browsers.
List or ports used when running the development environment.
- port.browsersync: The port to run Browsersync on. Default:
3030
If you're running a proxy with Browsersync, you will want to define the port the proxy runs on, then reference it in your serve task.
Determines which CSS pre-processor to use. Valid values: sass/less.
Default: sass
List of source locations for the default build task.
- src.js: The source location of js files.
- src.markup: The source location of html files.
- src.style: The source location of .scss or .less files.
- src.assets: The source location of asset files such as images, web fonts and other support files.
- src.includes: The node modules location that gets included by Webpack. Default:
./node_modules
. - src.appdir: The path to the application that gets defined for global usage by Webpack. Default:
~/src/app
. - src.rootdir: The root application path that gets defined for global usage by Webpack. Default:
~
.
You can define more source locations and use them for your own tasks.
List of watch locations for the default defined Gulp tasks.
- watch.js: The locations to watch for js file changes.
- watch.markup: The locations to watch for html file changes.
- watch.style: The locations to watch for
.scss
or.less
file changes. - watch.assets: The locations to watch for asset files such as images, web fonts, and other support files.
You can define more watch locations and use them for your own tasks.
Destination paths for the default defined Gulp tasks.
Reactium only uses Webpack for bundling and transpiling javascript. If you need it to do more, the webpack.config.js
file is the place to do it.
The Webpack config receives the Gulp Config as an argument.
By default, the following Gulp Tasks are defined:
Compiles javascript using Webpack. If config.env
is production
, the output files are optimized and minified.
Compiles .scss
files into .css
files by default. If config.env
is production
, the output files are optimized and minified.
You can switch this to .less
by specifying 'less' as the Gulp Config cssPreProcessor
value.
Transports asset files such as images, web fonts, and other support files to their corresponding location.
Transports html files to their corresponding location.
Removes all files from the config.dest.dist
directory.
Runs the development environment via Browsersync.
Runs the clean
, assets
, markup
, scripts
, and styles
tasks.
If the config.env
is development
, the build
task is run followed by the serve
. Otherwise, the build
task is run.
When the serve
task is run, the following watches are started:
When changes to the config.watch.style
files are detected, the styles
task is run.
Browsersync does a hot reload of the style sheet.
When changes to the config.watch.js
files are detected, the scripts
task is run.
Browsersync does a full reload of the page.
When changes to the config.watch.markup
files are detected, the markup
task is run.
Browsersync does a full reload of the page.
When changes to the config.watch.assets
files are detected, the assets
task is run.
Browsersync does a full reload of the page.