Conventional-redux.js is a library for small and medium applications, it wraps the react-redux and provides API based on convention over configuration pattern. It is NOT new flux implementation so everything is in 100% compatible with standard redux approach.
- Remove boilerplate code by adding conventions
- Don't break any redux rule/idea
- Handle basic stuff automatically with ability to override
// ------------------------------------
// Constants
// ------------------------------------
export const COUNTER_INCREMENT = 'COUNTER_INCREMENT'
// ------------------------------------
// Actions
// ------------------------------------
export function increment (value = 1) {
return {
type: COUNTER_INCREMENT,
payload: value
}
}
export const doubleAsync = () => {
return (dispatch, getState) => {
return new Promise((resolve) => {
setTimeout(() => {
dispatch(increment(getState().counter))
resolve()
}, 200)
})
}
}
export const actions = {
increment,
doubleAsync
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[COUNTER_INCREMENT]: (state, action) => state + action.payload
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = 0
export default function counterReducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
class CounterInteractor {
// initial state
defaultState() {
return 0;
}
// actions:
doubleAsync() {
setTimeout(() => { this.dispatch('counter:double') }, 500)
}
// reduce methods:
onIncrement() {
return this.state + 1;
}
onDouble() {
return this.state * 2;
}
}
const mapActionCreators = {
increment: () => increment(1),
doubleAsync
}
const mapStateToProps = (state) => ({
counter: state.counter
})
const Counter = (props) => (
<div>
<h2>{props.counter}</h2>
<button onClick={props.increment}>
Increment
</button>
<button onClick={props.doubleAsync}>
Double (Async)
</button>
</div>
)
export default connect(mapStateToProps, mapActionCreators)(Counter)
const Counter = (props, dispatch) => (
<div>
<h2>{props('counter')}</h2>
<button onClick={dispatch('counter:increment')}>
Increment
</button>
<button onClick={dispatch('counter:doubleAsync')}>
Double (Async)
</button>
</div>
)
}
export default connectInteractors(Counter, ['counter']);
- Define component
- Define actions
- Define reducer
- Define mapActionCreators
- Define mapStateToProps
- Define container
- Connect!
- Define component
- Define interactor
- Connect!
Remember that you can combine these two approaches! Use convetional way for simple parts of your application, but when you need more control over what is going on, pure redux should be better!
Check out an example app with code snippets here. The code is available here.
The library automatically defines actions based on reduce methods. Check out live example.
class CounterInteractor {
defaultState() {
return 0;
}
// You can still define increment by your own (but not need to)!
// increment() {
// console.log('test');
// }
onIncrement(by = 1) {
return this.state + by;
}
onDouble() {
return this.state * 2;
}
}
// dispatch examples:
// this.dispatch('counter:increment');
// this.d('counter:increment', 10);
// this.d('counter:double');
Automatically handling for promises resolve and reject. Check out live example.
class GithubUserdataInteractor {
defaultState() {
return 0;
}
fetch(userName) {
// need to return promise
return fetchResource('https://github.com/gitapi/users/' + userName)
}
onFetch(userName) {
return { loading: true }
}
fetchSuccess(userResponse) {
console.log(userResponse);
}
onFetchSuccess(userResponse) {
return { user: userResponse }
}
onFetchError(error) {
return { error: error.message }
}
}
You can define an array of external dependencies to modify interactor state after non interactor actions. Check out live example.
class ExampleInteractor {
externalDependencies() {
return [
{ on: ['LOGOUT'], call: 'onClear' }
]
}
onClear(logoutActionArgs) {
return {};
}
}
You can define a computed actions array to call additional dispatch after specific actions. In the following example the action always fires after projects:fetch
or gists:fetch
.
Check out live example.
class FiltersInteractor {
computedActions() {
return [
{ after: ['projects:fetch', 'gists:fetch'],
dispatch: 'filters:update',
with: ['projects.languages', 'gists.languages']
}
]
}
onUpdate(projectLanguages, gistLanguages) {
// it fires after gists or projects fetch with resolved args
}
}
Interactors can be static or dynamic. You cannot remove once registered static interactor. Dynamic interactors can be remvoed or replaced, the right moment to manage dynamic interactors is ROUTE_CHANGE
action. A good example of static interactor would be CurrentUserInteractor
, of dynamic - route based interactors like ProjectsInteractor
, SettingsInteractor
etc.
Check out live example.
// static, somewhere before connect:
registerInteractors({
currentUser: new CurrentUserInteractor(),
currentLanguage: new CurrentLanguageInteractor(),
});
// dynamic
onRouteChange() {
replaceDynamicInteractors({
counter: new CounterInteractor()
});
}
// dynamic setup, configuring store
setRecreateReducerFunction(() => store.replaceReducer(createReducer(store.injectedReducers)));
It connects interactors (state) to a specific component. Check out live example.
class Counter extends React.Component {
render () {
return (
<div>
<h2>{this.p('counter')}</h2>
<button onClick={() => this.d('counter:increment')}>
Increment
</button>
<button onClick={() => this.d('counter:doubleAsync')}>
Double (Async)
</button>
</div>
)
}
}
export default connectInteractors(Counter, ['counter']);
// or connect all (not recommended)
export default connectInteractors(Counter);
npm install conventional-redux --save
import { conventionalReduxMiddleware } from 'conventional-redux';
const middleware = [conventionalReduxMiddleware, thunk, routerMiddleware(history)]
Code example: https://github.com/mjaneczek/conventional-redux-demo/blob/master/app/configureStore.js#L12
setRecreateReducerFunction(() => store.replaceReducer(createReducer()));
Code example: https://github.com/mjaneczek/conventional-redux-demo/blob/master/app/configureStore.js#L35
import { createConventionalReduxRootReducer } from 'conventional-redux';
return createConventionalReduxRootReducer({
route: routeReducer,
}, combineReducers);
// the first parameter is a default combined reducers hash
// the second is a pure combineReducers function from redux or redux-immmutable
Code example: https://github.com/mjaneczek/conventional-redux-demo/blob/master/app/reducers.js#L20
https://mjaneczek.github.io/conventional-redux-demo/
The previous version of the library 0.2.2 has been deprecated. There are many breaking changes while migrating to 1.0, please be careful while updating. The outdated code is here: https://github.com/mjaneczek/conventional-redux/tree/0.2.2-outdated
- Fork it ( https://github.com/mjaneczek/conventional-redux/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request