This is most clear way for manage state in React.js, with unnecessary of writing extra excess code, allowing to keep you attention on what you do, instead how
- Super small: 870 bytes ungzipped!
- Support both, class and functional components
- Provide uniform bus, used universally throught all you components tree
- Without side dependencies - only React enough
- Have no complex code constructions - very ease to use
npm i react-datanomy
or
yarn add react-datanomy
Datanomy receive initialState, reducers and optionally scenarios and returns array with StoreProvider, StoreHook and StoreContext. StoreProvider supply universal structure, named SAS Bus, and then StoreHook, StoreContext.Consumer, or contextType via StoreContext, consume it in the next unified form:
[currentState, memoizedActions, memoizedScripts]
SAS Bus (SAS from [State, Actions, Scripts]) acts as data bus in the electronic, or like building infrastructere, where independend from architecture complexity, each room can be connected to electricity, water, internet, security system, gas, etc. - to the any network, which independend from others and also paved through whole building. According to that analogy, React context play a cable duct role.
initialState is a starting store state.
reducers is a hash of clear functions, indexed by actions names, which receives one or two arguments: currentState and optional payload and returns newState.
scenarios is a function, which receive getState method and actions in arguments and returns a hash with scripts.
getState is a function, which allways return cureent actual state inside scripts
currentState is an current actual state, returned from useReducer inside Datanomy.
acrions is a memoized hash of methods, which formally wrappers for dispatch, returned from useReducer, called with action name in type
field and optional payload from action argument.
scripts is a memoized hash of methods, which can receives optional payload and returns nothing. They are can be regular, or async functions, and contains a logic of any complexity, which allways have an access to the current actual state by using getState, and call any actions, which are closured from scenarios method scope.
StoreProvider is component, which wrap React Context.Provider and supply SAS Bus to his value argument
StoreHook is a wrapper for a regular React useContext, with StoreContext as argument
StoreContext is a regular React Context, created using createContext, called with initialState as argument
file: src/stores/counterStore.js
// Import createDatanomy from react-datanomy:
import { createDatanomy } from 'react-datanomy'
// Declare you initial state:
const initialState = {
counter: 0,
someData: [],
}
// Declare state reducers:
const reducers = {
increment: (state) => ({ ...state, counter: state.counter + 1 }),
decrement: (state) => ({ ...state, counter: state.counter - 1 }),
add: (state, payload) => ({ ...state, counter: state.counter + payload }),
sub: (state, payload) => ({ ...state, counter: state.counter - payload }),
reset: (state) => ({ ...state, counter: 0 }),
}
// Optionally declare some complex scenarios:
const scenarios = (getState, actions) => {
derivedAdd: async (payload) => {
console.log(getState().counter) // for example 10
await new Promise((resolve) => {
setTimeout(() => resolve(acttions.add(payload)), 100)
})
console.log(getState().counter) // 10 + payload
},
}
// Call createDatanomy with above declarations and export it retuns:
export const [
CounterProvider,
useCounter,
CounterContext
] = createDatanomy(initialState, reducers, scenarios)
// Additionally import Treducers and TScenarios types:
import { TReducers, TScenarios, createDatanomy } from 'react-datanomy';
// Declare you store interface
interface TCounterStore = {
counter: number;
someData: Array<any>;
};
// Type initial state using you store interface
const initialState: TCounterStore = {
counter: 0,
someData: [],
};
// Type reducers, using TReducers with store interface as generic argument
const reducers: TReducers<TCounterStore> = {
increment: (state) => ({ ...state, counter: state.counter + 1 }),
decrement: (state) => ({ ...state, counter: state.counter - 1 }),
add: (state, payload) => ({ ...state, counter: state.counter + payload }),
sub: (state, payload) => ({ ...state, counter: state.counter - payload }),
reset: (state) => ({ ...state, counter: 0 }),
};
// Type scenarios, using TScenarios with store interface as generic argument
const scenarios: TScenarios<TCounterStore> = (getState, actions) => {
derivedAdd: async (payload) => {
console.log(getState().counter) // for example 10
await new Promise((resolve) => {
setTimeout(() => resolve(acttions.add(payload)), 100)
})
console.log(getState().counter) // 10 + payload
},
};
export const [
CounterProvider,
useCounter,
CounterContext
// Probide store interface as generic argument to the createDatanomy
] = createDatanomy<TCounterStore>(initialState, reducers, scenarios)
file: src/App.jsx
import { CounterProvider } from './store/counterStore'
import HookComponent from './components/HookComponent'
import ConsumerComponent from './components/ConsumerComponent'
import ClassComponent from './components/ClassComponent'
function App() {
return (
<CounterProvider>
<div>
<HookComponent/>
<ConsumerComponent/>
<ClassComponent/>
</div>
</CounterProvider>
);
}
export default App;
file: src/components/HookComponent.jsx
import { useEffect } from 'react'
import { useCounter } from '../store/counterStore'
function HookComponent() {
// here we are just use a destructurization instead somethig like selectors in Redux,
// so, when unused store branches will be updated, they will not trigger a rendering
const [
{ counter },
{ increment, decrement },
{ derivedAdd },
] = useCounter()
useEffect(() => {
derivedAdd(15)
// we can use actions and scripts as useEffect, useMemo, or useCallback
// dependency for avoid exhausive deps warning. This will not trigger
// unwanted rerenderings, because actions and scripts are already memoized
// without any dependencies
}, [derivedAdd])
return (
<div>
<div>{counter}</div>
<buttom onClick={increment}>+</button>
<buttom onClick={decrement}>-</button>
</div>
);
}
export default HookComponent;
file: src/components/ConsumerComponent.jsx
import { CounterContext } from '../store/counterStore'
function ConsumerComponent() {
return (
<CounterContext.Consumer>
{/* in SAS Bus we can even skip state, actions, or scripts - just
do not forget to preserve a coma */}
{([
, // <- skip unused state...
{ add, sub }, // <- and skip unused scripts
]) => (
<>
<buttom onClick={() => add(5)}>+5</button>
<buttom onClick={() => sub(5)}>-5</button>
</>
)}
</CounterContext.Consumer>
);
}
export default ConsumerComponent;
file: src/components/ConsumerClassComponent.jsx
import { PureComponent } from 'react'
import { CounterContext } from '../store/counterStore'
class ConsumerClassComponent extends PureComponent {
render() {
return (
<CounterContext.Consumer>
{([
,
{ add, sub },
]) => (
<>
<buttom onClick={() => add(5)}>+5</button>
<buttom onClick={() => sub(5)}>-5</button>
</>
)}
</CounterContext.Consumer>
);
}
}
export default ConsumerClassComponent;
file: src/components/ClassComponent.jsx
import { PureComponent } from 'react'
import { CounterContext } from '../store/counterStore'
class ClassComponent extends PureComponent {
static contextType = CounterContext;
render() {
const [
,
{ add, sub },
] = this.context
return (
<>
<buttom onClick={() => add(10)}>+10</button>
<buttom onClick={() => sub(10)}>-10</button>
</>
);
}
}
export default ClassComponent;
Good news: SST is splitable!
It is not necessary to create one monolithic store for whole application data. When you split one big store on several specialized stores, if you follow condition of avoiding data duplicates, you several stories yet remains as splitted SST.
For example, you can wrap different routes into providers, which connect Store, related only to relevant route, or DOM branch (such as <AdminProvider>
to /admin
route). A lot of discussions about providers location in a DOM tree happens in the internet, so it is a thin quiestion, but sometime it have a sense.
In the scenarios, function getState
always returns actual state, so if you call it, than call action, which update some field and than again call getState
, that field will contains a new value
By the way, you can participate in discussions about scenarios API and scenarios architecture
Plwase read the contribution guide for participate in a project development
Or support us by donation, if you interesting in speedup of evolving a simplest, compact and fastest React state management
Ethereum wallet:
0xc6C5673fa62F85070657F147246c167887Fc918D
// TODO: Complete readme