anew
v2.8.14
Published
React and Redux Framework with additional power.
Downloads
203
Readme
Anew
A lightweight Redux framework with React/ReactNative utilities
Anew removes the boilerplate overhead Redux provides (for good reasons). In addition, Anew provides additional functionalities that are necessary in most if not all Redux applications. A basic use case for anew is enough to see the code reduction, structured architecture, and robustness that the framework provides.
Anew was built to include only that which is necessary with room made for extendability. Transitioning from an existing Redux project is seamless. Anew also provides React specific utilities for bootstrapping your application. The Redux portion of Anew could be used with any view library that Redux supports (which is any view library). Future development to Anew may include bootstrap utilities for other view libraries as well.
React Native Support? You Bet!
Installation
To install anew
directly into your project run:
npm i anew -S
for yarn users, run:
yarn add anew
Table of Contents
createStore
The createStore
utility allows you to create a Redux store without the need to define action creators, action types, thunks, or even a reducer. Simply provide the createStore
parameters and the rest is taken care of by anew.
// @return Anew Store
createStore({
name : String,
state : Any,
persist : Boolean | Object,
reducers : Functions | Objects,
effects : Functions,
selectors : Functions,
enhancer : Function,
reducer : Function,
actions : Functions
})
Parameters
name
: a unique namespace specific to the store used through out the application to access the store's state object and actions.
state
: the initial state object for the store.
persist
: a user may add persist and rehydrate a stores data by assigning the persist property with the persist config as described in the redux-persist documentation. A use may also assign the persist property a true value which will use the default object created by anew. You don't need to provide the key property for persist since the store name is assigned as the key by anew and cannot be overwritten. Anew, also, if not defined, assigns the default storage object provided by redux-persist.
reducers
: pure functions, defined under a strict namespace, that return the next state tree for a specific store. Reducers receive the store's current state tree with an optional payload list as parameters. In addition, reducers that fall under an outside namepsace can also be defined inside the store and get passed both the current and defined namespace states.
createStore({
name: 'someNameSpace',
reducers: {
someReducer(state, ...payload) {
return nextStateTreeForSomeStore
},
otherNameSpace: {
otherReducer(state, otherState ...payload) {
return nextStateTreeForSomeStore
},
},
},
})
effects
: pure functions that handle operations that fall inside and outside the store. They are mainly used to handle async operations that dispatch actions upon completion. Effects receive an exposed store object with public properties.
createStore({
name: 'someNameSpace',
effects: {
anotherEffect(store, ...payload) {
// Some Logic...
},
someEffect(store, ...payload) {
const {
select, // get state from selectors
dispatch, // Dispatch reducers
batch, // Batched dispatch for reducers
effects, // Call effects
actions, // Dispatch actions
core, // Access ^ aboveproperties at core (root) store level
persistor, // Dispatch persistor action
} = store
effects.anotherEffect(...payload) // no need to pass store
// Core Properties (Example)
core.effects.otherNameSpace.otherEffect(...payload)
core.dispatch.otherNameSpace.otherReducer(...payload)
core.batch.otherNameSpace.otherReducer(...payload)
core.batch.done()
core.select.otherNameSpace.otherSelector(...payload)
core.actions.otherNameSpace.otherAction(...payload)
}
}
})
selectors
: pure functions that return a state slice selector or a derived state selector. Derived state selectors are memoized using reselect.
createStore({
name: 'someNameSpace',
state: {
someStateProp: 1,
anotherStateProp: 2,
},
selectors: {
// Passing a string as the first argument creates a prop selector
// You may also assign any default value of any type
someProp: store => store.create('someProp', 'defaultValue'),
// Passing one function to the store.create method generates a simple object lookup selector
// No memoization is done for these selectors
someStateProp: store => store.create(state => state.someStateProp),
anotherStateProp: store => store.create(
/**
* Simple Selector
* @param { Object } state Local Namespace State
* @param { Object } coreState Core (Root) App State. This prop
* does not exist if no root store
* exits
* @param { ...Any } payload List of arguments
*/
(state, coreState, ...payload) => {
if(coreState.otherNameSpace.otherStateProp) {
return state.someStateProp
}
return true
}
),
// Passing two parameters or a list of functions generates a memoized selector
// Using the reselect api
totalStateProp: store => store.create([
store.select.someStateProp,
store.select.anotherStateProp,
store.select.someProp,
store.core.otherNameSpace.otherSelectorName,
], (someStateProp, anotherStateProp, someProp, otherBoolProp) => {
return otherBoolProp && someProp === 'something'
? someStateProp + anotherStateProp
: 0
})
}
})
enhancer
: An optional parameter to enhance the store using Redux middlewares.
reducer
: an optional property for using a user defined reducer. The createStore utility automatically generates a reducer for you to accommodate the anewStore api. A user could, however, provide their own reducer, which is merged with the generated anewStore reducer and state. This is useful for using packages that follow the standard redux api inside an anew application. A perfect example, is integrating the connected-react-router package inside an anew store.
actions
: are pure functions that return an action object ({ type, payload }
). They are mainly used for a user defined reducer that has user defined action types.
Example
// stores/counter.js
import { createStore } from 'anew'
export const counterStore = createStore({
name: 'counter',
state: {
count: 0,
},
reducers: {
increment(state, addition = 1) {
return {
count: state.count + addition
}
},
// Outside Nampesace
list: {
// Both state objects are passed as paramaters
push(state, list, addition = 1) {
return {
count: state.count + list.items.length + addition
}
},
},
// You can use with third party stores as well
'@@router': {
LOCATION_CHANGE() {
// logic
},
},
},
effects: {
async incrementDelayed(store, addition) {
await setTimeout(() => {
store.dispatch.increment(addition)
// The following is equivalent to:
// 1. store.dispatch({
// type: 'counter:increment',
// payload: [ addition ]
// })
//
// 2. store.batch.increment(addition)
// store.batch.done()
//
// 3. store.batch([
// { type: 'counter:inc', payload: [ addition] }
// ])
// store.batch.done()
//
// 4. store.batch({
// type: 'counter:inc',
// payload: [ addition]
// })
// store.batch.done()
}, 1000)
},
},
})
// returns: { count: 0 }
counterStore.getState()
// new state tree: { count: 1 }
counterStore.dispatch.reducers.increment(1)
// new state tree: { count: 10 }
counterStore.dispatch({ type: 'counter:inc', payload: [ 9 ] })
// new state tree: { count: 11 }
counterStore.dispatch.effects.incrementDelayed(1).then(() => {
console.log('Some Logic')
})
// new state tree: { count: 13 } executes one dispatch (one re-render)
counterStore.dispatch.batch.increment(1)
counterStore.dispatch.batch.increment(1)
counterStore.dispatch.batch.done()
// new state tree: { count: 15 }
counterStore.dispatch.batch([
{ type: 'counter:inc', payload: [ 1 ] },
{ type: 'counter:inc', payload: [ 1 ] },
])
counterStore.dispatch.batch.done()
Example: With User Defined Reducer
import { createStore } from 'anew'
import { routerReducer, routerActions } from 'react-router-redux'
export const routerStore = createStore({
name: 'router',
state: {
isAuthenticated: false,
},
reducer: routerReducer,
actions: routerActions,
reducers: {
authenticated() {
return {
isAuthenticated: true
}
},
},
})
// returns: { isAuthenticated: false, location: { ... } }
// The state is a merge of the two states: anewStore state and routerReducer state.
routerStore.getState()
// new state tree: { isAuthenticated: false, location: { pathname: '/home', ... } }
routerStore.dispatch.actions.push('/home')
// new state tree: { isAuthenticated: true, location: { ... } }
routerStore.dispatch.reducers.authenticated()
combineStores
The combineStores
utility is very similar to the combineReducer
utility provided by Redux. The only difference is it combines multiple stores rather than combining multiple reducers. This combination updates each store's api references to point to the new combined store reference. This however, does not change the store shape for each given store. Methods like getState()
still return the state shape for each store.
// @return Combnined Anew Store
combineStores({
name : String,
stores : Array[AnewStores],
enhancer: Function
})
Parameters
name
: a unique namespace for the newly created store combination.
stores
: an array of anew stores to combine.
enhancer
: An optional parameter to enhance the new combined store using Redux middlewares.
Example
// app/store.js
import { combineStores } from 'anew'
import counterStore from 'stores/counter'
const rootStore = combineStores({
name: 'root',
stores: [
counterStore,
],
})
// returns: { counter: { count: 0 } }
rootStore.getState()
// new state tree: { counter: { count: 1 } }
rootStore.dispatch.reducers.counter.increment(1)
// new state tree: { counter: { count: 10 } }
rootStore.dispatch({ type: 'counter:inc', payload: [ 9 ] })
// After Combination using `counterStore`
// new state tree: { counter: { count: 11 } }
counterStore.dispatch.reducers.increment(1)
createTestEnv
The createTestEnv
utility allows you to track a store's state tree changes. After a sequence of actions executed using the anew store's api you expect a specific state tree. Having action creators and state getters coupled together in the api makes this much easier. The utility, simply returns a function (bound to a specific store) that resets the store to initial state, when called, and returns that store object. You may also execute this function before each test using a method like beforeEach
provided by jest and other testing suites and use the store object directly.
// @return Store Creator
createTestEnv(store)
Parameters
store
: an anew store
Example
// test/stores/counter.test.js
import { createTestEnv } from 'anew'
import counterStore from 'stores/counter'
const newTestEnv = createTestEnv(counterStore)
test('inital state', () => {
const store = newTestEnv()
const state = store.getState()
expect(state).toEqual({
count: 0
})
})
test('increment by one', () => {
const store = newTestEnv()
store.dispatch.reducers.increment(1)
const state = store.getState()
expect(state).toEqual({
count: 1
})
})
Anew
- React Specific Utilities
The default import for anew includes a list of helpful react utilities for bootstrapping your application. This includes methods such as creating routes, settings the applications global store object, and mounting to DOM. The utilities make it much easier to manage a growing application.
Utilities
Note, to use utilities in react-native, import the app as follows:
import App, { AppNativeCore } from 'anew/native'
/**
| ------------------------------------------------------
| React Native Utils
| ------------------------------------------------------
| See each util definition below for more detail.
|
| The only supported utilities for `react-native`,
| currently, are the following:
*/
App.connect(Component)
App.store(Store)
App.mount(NavigationStack)
Connect
The connect method is an alternative to the connect method provided by react-redux.
import React from 'react'
import App from 'anew'
class SomeComponent extends React.Component {
static mapStateToProps(select, state, props) {
return // state mapping
}
static mapDispatchToProps(dispatch) {
return // dispatch mapping
}
render() {
return <div />
}
}
export default App.connect(SomeComponent)
Alternative
import React from 'react'
import App from 'anew'
class SomeComponent extends React.Component {
render() {
return <div />
}
}
export default App.connect({
component: SomeComponent,
mapStateToProps(select, state, props) {
return // state mapping
},
mapDispatchToProps(dispatch) {
return // dispatch mapping
},
})
Redux
import React from 'react'
import { connect } from 'react-redux'
class SomeComponent extends React.Component {
render() {
return <div />
}
}
export default connect(
(state, props) => {
// import store from './path/to/root/store'
//
// To access selectors use root (combined) store object
// store.getState.<storeName>.<selectorName>()
//
// import someStore from './path/to/some/store'
//
// Or directly use store object
// someStore.getState.<selectorName>()
return // state mapping
},
(dispatch) => {
return // dispatch mapping
}
)(SomeComponent)
Routes
Use react-navigation for react native routes.
The routes utility uses react-router-dom
and react-router-config
to define application routes. The utility adopts a cascading syntax (method chaining) to build the config passed to react-router-config
.
Route
import React from 'react' // for JSX
import App from 'anew'
/**
* @param { String } route path
* @param { React Component } route component
* @param { Object } route properties
* @returns { Object } react-router-config route object
*/
App.route('/somePath', SomeComponent, {
title: 'My Component', // Custom property
exact: true // react-router property
})
/*
* React Router Config Equivalent
* [
* {
* path: '/somePath',
* component: SomeComponent,
* name: 'My Component',
* exact: true,
* },
* ]
*/
/**
| ------------------
| Cascading Route
| ------------------
| A cascading route remains a sibling to the previous route
| with its path being appending to the previous path.
*/
App.route('/route1', ComponentOne).route('/route2', ComponentTwo)
/*
* React Router Config Equivalent
* [
* {
* path: '/route1',
* component: ComponentOne,
* },
* {
* path: '/route1/route2',
* component: ComponentTwo,
* },
* ]
*/
/**
| ------------------
| Group
| ------------------
| A group is a cluster of sibling routes defined under one parent.
| The entire application's routes fall under one group, which could include
| sub-groups as children.
*/
/**
* The current component is a routes template component that renders
* the children routes for a given route inside this template wrapper.
*
* @param { Object } props.route The current route config
*/
function GroupTemplate(props) {
return (
<div>
<h1>Group Header</h1>
<div>{ App.render(props.route) }</div>
<div>Group Footer</div>
</div>
)
}
/**
* @param { String } Group Path
* @param { Function } Group Route Builder
* @param { Object } Optional - Group Route Properties
* @param { React Component } Optional React Template Component. By default
* will render childern routes with no component
* wrapper.
*/
App.group('/someGroupPath', Group => {
Group.route('/route1', ComponentOne)
Group.route('/route2', ComponentTwo).route('/route3', ComponentThree)
}, GroupTemplate)
/*
* React Router Config Equivalent
* [
* {
* path: '/someGroupPath',
* component: GroupTemplate,
* routes: [
* {
* path: '/route1',
* component: ComponentOne,
* },
* {
* path: '/route2',
* component: ComponentTwo,
* },
* {
* path: '/route2/route2',
* component: ComponentThree,
* },
* ],
* },
* ]
*/
App.group('/someGroupPath', Group => {
Group.route('/route1', ComponentOne)
Group.route('/route2', ComponentTwo).route('/route3', ComponentThree)
}, {
exact: true,
}, GroupTemplate)
/*
* React Router Config Equivalent
* [
* {
* path: '/someGroupPath',
* component: GroupTemplate,
* exact: true, <------------- ADDITION
* routes: [
* {
* path: '/route1',
* component: ComponentOne,
* },
* {
* path: '/route2',
* component: ComponentTwo,
* },
* {
* path: '/route2/route2',
* component: ComponentThree,
* },
* ],
* },
* ]
*/
Example
// app/routes.js
import App from 'anew'
/**
| ------------------
| Components
| ------------------
*/
import Counter from 'pages/counter'
import List from 'pages/list'
/**
| ------------------
| Routes
| ------------------
*/
App.route('/', Counter, { name: 'Counter', exact: true })
App.route('/list', List, { name: 'List' })
Store
The store app utility provides an anew/redux store object to the application to be accessed using the redux connect
HOC.
import App from 'anew'
import Store from 'app/store'
App.store(Store)
Template
The template app utility defines the applications routes template where all routes will render.
import React from 'react'
import App from 'anew'
class AppTemplate extends React.Component {
render() {
return (
<div>
<div>App Header</div>
<div>{ App.render(this.props.route) }</div>
<div>App Footer</div>
</div>
)
}
}
App.template(AppTemplate)
Final Example
// index.js
import App from 'anew'
import Store from 'app/store'
import Template from 'app/template'
/**
| ------------------
| Components
| ------------------
*/
import SignIn from 'pages/signIn'
import Counter from 'pages/counter'
import List from 'pages/list'
/**
| ------------------
| Bootstrap
| ------------------
*/
App.store(Store)
App.template(Template)
/**
| ------------------
| Routes
| ------------------
*/
// pathname => / || /signin || /login
App.route(/(|signin|login)/, SignIn, { exact: true })
App.route('/', Counter, { name: 'Counter', exact: true })
App.route('/list', List, { name: 'List' })
/**
| ------------------
| Entry Point
| ------------------
*/
/**
* React
* @param { String } Element Id to mount to
* @param { Object } Options (Optional)
*/
App.mount('root'/*, { history, ...routerProps } */)
/**
* React Native
* @param { Object } react-navigation Navigation Stack
* @param { Object } Options (Optional)
*/
App.mount(NavigationStack/*, { provider } */)
Recommend File Structure
This is merely a recommendation for how you could structure your anew application. Anew is composed of all pure functions making it very flexible when it comes to how you architect your application. No need to go through the hassle of binding methods, you can rest assure, each method will always receive the expected parameters.
/stores
: contains all application stores
/stores/index.js
: combination of all application stores
// /stores/index.js
import { combineStores } from 'anew'
/**
| ------------------
| Stores
| ------------------
*/
import someStore from './someStore/someStore'
import otherStore from './otherStore/otherStore'
/**
| ------------------
| Combination
| ------------------
*/
export default combineStores({
name: 'rootStore',
stores: [
someStore,
otherStore,
],
})
/stores/someStore
: contains all someStore related files
/stores/someStore/someStore.js
: someStore creation file
// /stores/someStore/someStore.js
import { createStore } from 'anew'
import state from './someStore.state'
import * as reducers from './someStore.reducers'
import * as effects from './someStore.effects'
export default createStore({
name: 'someStore',
state,
reducers,
effects,
})
/stores/someStore/someStore.state.js
: someStore initial state and object lookups
// /stores/someStore/someStore.state.js
export default {
someBoolProp: false,
}
export const someBoolPropSelector = ({ someStore }) => someStore.someBoolProp
/stores/someStore/someStore.selectors.js
: someStore memoized selectors
// /stores/someStore/someStore.selectors.js
import { createSelector } from 'reselect'
import { someBoolPropSelector } from './someStore.state'
import { otherBoolPropSelector } from 'stores/otherStore/otherStore.state'
export const someAdvancedSelector = createSelector([
someBoolPropSelector,
otherBoolPropSelector,
], (someBoolProp, otherBoolProp) => {
return someBoolProp || otherBoolProp
})
/stores/someStore/someStore.reducers.js
: someStore state reducers
// /stores/someStore/someStore.reducers.js
import * as otherStore from './someStore.reducers.otherStore'
/**
| ------------------
| Inner Reducers
| ------------------
*/
export const someToggleReducer = state => ({
someBoolProp: !state.someBoolProp,
})
/**
| ------------------
| Outer Reducers
| ------------------
*/
export { otherStore }
/stores/someStore/someStore.reducers.otherStore.js
: someStore state reducers defined under another namespace
// /stores/someStore/someStore.reducers.otherStore.js
export const otherReducer = (state, otherState) => ({
someBoolProp: state.someBoolProp || otherState.otherBoolProp,
})
/stores/someStore/someStore.effects.js
: someStore effects
// /stores/someStore/someStore.effects.js
import { someAdvancedSelector } from './someStore.selectors'
export const someEffect = ({ effects, getState }) => {
const state = getState()
const someAdvanced = someAdvancedSelector(state)
effects.someOtherEffect(someAdvanced)
}
export const someOtherEffect = ({ dispatch }, someAdvanced) => {
setTimeout(() => {
dispatch.someToggleReducer(someAdvanced)
}, 1000)
}