react-devstack
v0.9.0
Published
An ultimate development stack built on top of React with Redux
Downloads
4
Readme
react-devstack
An ultimate opinionated universal React devstack. Don't hassle with project build and other stuff, just do what matters: start developing your application, right now!
Getting started
In order to get started as easily as possible, we have prepared CLI utility to make app bootstrapping a breeze.
- Install
react-devstack-cli
globally using:npm install react-devstack-cli -g
- Create an empty React project using the CLI (it's called
rdc
):rdc create project-name
- After generating the project, CLI will guide you, however in case you wondered just:
cd project-name
- And start the app using
npm run dev
It will automatically open your browser upon compilation.
Enjoy all the hot-reloading, server side rendering etc. :-)
State management (using redux)
react-devstack
is packed with Redux for dealing with state. Redux provides single store instance which holds all the state information in one place. Start using redux
is as simple as creating root reducer in src/reducers/rootReducer.js
.
export default (state = 0, { type }) {
switch (type) {
case 'Increment':
return state + 1;
default:
return state;
}
}
And then just simply connect
any component you need. For instance, you can connect
the Root
. State corresponding with the rootReducer.js
is actually nested under key root
in global application state passed to connect. Therefore you can either access the field directly using root
key, or use getRootState
selector from react-devstack
.
import React from 'react';
import { connect } from 'react-redux';
import { Selectors } from 'react-devstack';
import logo from '../assets/salsita-logo.svg';
const mapStateToProps = state => ({
clicked: state.root // Or simply use Selectors.getRootState(state)
});
const mapDispatchToProps = dispatch => ({
increment: () => ({ type: 'Increment' })
})
export default connect(mapStateToProps, mapDispatchToProps)(({ clicked, increment }) => (
<div>
<h1>Hello from react-devstack!</h1>
<img src={logo} width={200} />
<button onClick={increment}>Button clicked {clicked} times</button>
</div>
));
react-devstack-cli
offers easy way to create root reducer for you. Just use rdc add redux
in your project.
Routing (using router5)
Fundamental part of every real application is routing. react-devstack
utilizes router5
as underlying routing library and it's automatically synchronized with redux
. Adding routing to your project involves creating routing schema file as defined by router5
. Just create a file named src/routing/routes.js
and export list of routes:
export default [{
name: 'index',
path: '/'
}, {
name: 'cats',
path: '/cats'
}];
Now, whenever in your code, you can just use Selectors.getRoute
which returns active route object. The object contains field name
so that you can conditionally check in any component.
import { Selectors } from 'react-devstack';
const Root = ({
isRouteFound,
activeRoute
}) => (
<div>
{isRouteFound && (
<div>
{activeRoute.name === 'cats' && <Cats />}
{activeRoute.name === 'index' && <Index />}
</div>
)}
{!isRouteFound && <div>404</div>}
</div>
);
const mapStateToProps = state => ({
isRouteFound: !Selectors.isNotFoundRoute(state),
activeRoute: Selectors.getRoute(state)
});
export default connect(
mapStateToProps
)(Root);
react-devstack-cli
offers easy way to create routing file for you. Just use rdc add routing
in your project.
Links
react-devstack
exposes Link
component. You need to provide route name as the only required property. Child of the component should be a function returning markup. This function is called with three important arguments: onClick
, href
, active
and props
originally passed to the component, so that you can define your own markup.
import { Link } from 'react-devstack';
const Root = () => (
<div>
<Link name="cats">{(onClick, href, active) => active ? <span>Cats</span> : <a href={href} onClick={onClick}>Cats</a>}</Link>
</div>
);
export default Root;
However, it's better to create re-usable component which could look as follows:
import React from 'react';
import { Link } from 'react-devstack';
// It's better to have this function stored so that
// it's not being re-created with each render
// unlike anonymous function
const render = (onClick, href, active, props) => active ?
<span>{props.originalChildren}</span> :
<a href={href} onClick={onClick}>{props.originalChildren}</a>;
const MyApplicationLink = (props) => <Link {...props} originalChildren={props.children}>{render}</Link>;
Now it can be used like a simple component:
const Root = () => (
<div>
<MyApplicationLink name="cats">Cats</MyApplicationLink>
</div>
);
export default Root;
It's highly recommended to read through router5 documentation.
Data fetching (using redux-saga)
Generally react-devstack
utilizes redux-saga
to deal with side effects. You don't need to install nor setup anything. Everything is already plugged in and it's up to you to just create src/sagas/rootSaga.js
and export generator function which will act as your root saga.
You can also use react-devstack-cli
to create the file by calling rdc add saga
.
It's possible to use redux-saga
exactly the way it's proposed in the documentation, for example you can fetch user from the server as easily as:
import { takeEvery, put, call } from 'redux-saga/effects';
const fetchUser = fetch('/user').then(result => result.json());
export default function*() {
yield takeEvery('FetchUser', function*() {
try {
const user = yield call(fetchUser);
yield put({ type: 'UserFetched', payload: user });
} catch (ex) {
console.error(ex);
yield put({ type: 'UserFetchingFailed' });
}
});
}
However, this would not work because react-devstack
waits until all the tasks in sagas are resolved before sending response from the server and since takeEvery
waits indefinitely it would just hang. Therefore we have wrapped simple concept for server-side-rendering as proposed in (redux-saga docs) and very trivial implementation of takeEveryUniversal
saga is exposed.
import { put, call } from 'redux-saga/effects';
import { SagaEffects } from 'react-devstack';
const fetchUser = fetch('/user').then(result => result.json());
export default function*() {
yield SagaEffects.takeEveryUniversal('FetchUser', function*() {
try {
const user = yield call(fetchUser);
yield put({ type: 'UserFetched', payload: user });
} catch (ex) {
console.error(ex);
yield put({ type: 'UserFetchingFailed' });
}
});
}
So just swapping takeEvery
with SagaEffects.takeEveryUniversal
from react-devstack
will do the trick.
Route based data fetching
Typical use case for any web application is fetch some data based on active route. It's very simple with react-devstack
because onEnterRouteUniversal
saga is exposed.
import { call, fork, put } from 'redux-saga/effects';
import { SagaEffects } from 'react-devstack';
const fetchGif = topic => fetch(`http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}`)
.then(response => response.json());
export default function* () {
yield fork(SagaEffects.onEnterRouteUniversal, 'cats', function*() {
const data = yield call(fetchGif, 'funny cats');
yield put({ type: 'CatsFetched', payload: data.data.image_url });
});
}
Now, whenever user enters /cats
route, it will automatically fetch random gif. This is of course happens on the server when user opens the app from /cats
url, and the request is sent just once (on the server), once it loads on the client, the request is not sent again, they would need to click on different link and get back to /cats
again to re-trigger API call.
Custom HTML template (using react-helmet)
You can use react-helmet
whenever you need in your application to control content of whole HTML. So if you just need to change title
on specific page. Just use Helmet
provided from react-helmet
:
import React from 'react';
import { Helmet } from 'react-helmet';
const Cats = () => (
<div>
<Helmet>
<title>Kewl cats</title>
</Helmet>
<div>
<h1>Cats</h1>
<p>Lorem ipsum....</p>
</div>
</div>
);
export default Cats;
This will automatically work on the server as well. You can nest Helmets
as described in react-helmet docs to override default values with specifics.