react-datanomy
TypeScript icon, indicating that this package has built-in type declarations

0.3.1 • Public • Published

logo

react-datanomy

This is not a yet another state manager. This is just a helper library for organize you state management, using features, already present in the React.

Test and build

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

Preferencies:

  1. Super small: 929 bytes ungzipped!
  2. Support both, class and functional components
  3. Provide uniform bus, used universally throught all you components tree
  4. Without side dependencies - only React enough
  5. Have no complex code constructions - very ease to use

Installation:

npm i react-datanomy

or

yarn add react-datanomy

API:

Datanomy receive initialState, reducers and optionally scenarios and returns array with StoreProvider, StoreHook and StoreContext. StoreProvider supply universal structure, named SAS Bus, or just a Context. Then StoreHook, StoreContext.Consumer, or contextType, consume it via StoreContext 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 for tree networks: state, actions and scripts, which present everywhere inside child tree of they store provider

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 hash of functions, which are receive getContext and payload in arguments. That hash transforms to the hash of scripts, each from which receive only payload when called.

getContext is a function, which always returns current actual SAS Bus state in a form [currentState, actions, scripts]

currentState is an current actual state, returned from useReducer inside Datanomy.

actions 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 are 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 context using getContext, which accessible in a scenarios declaration, but hidden in a scenarios usage.

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

Usage:

1. Create you store module:

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 = {
  derivedAdd: async (getContext, payload) => {
    logCounter(getContext) // for example 10
    const [,{add}] = getContext()
    await new Promise((resolve) => { setTimeout(() => resolve(add(payload)), 100) })
    logCounter(getContext) // 10 + payload
  }
}

// some context data can be required several times during scenario will run, 
// so, that cases looks like will better isolate in separate function scope,
// like logCounter, which twice called for show actual counter state.
// other approach (I not sure, that right, but, ...), using var instead const/let
// with var we can redefine variable multiple times and we are becomes to able 
// for reuse destructurization and retake newest state element value, action, or script
const logCounter = getContext => {
  const [{counter}] = getContext()
  console.log("counter:", counter)
}

// Call createDatanomy with above declarations and export it retuns:
export const [ 
  CounterProvider, 
  useCounter,
  CounterContext
] = createDatanomy(initialState, reducers, scenarios)

Same, using typescript:

// Additionally import Treducers and TScenarios types:
import { TReducers, TScenarios, TBulkScenarios, 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> = {
  derivedAdd: async (getContext, payload) => {
    logCounter(getContext) // for example 10
    const [,{add}] = getContext()
    await new Promise((resolve) => { setTimeout(() => resolve(add(payload)), 100) })
    logCounter(getContext) // 10 + payload
  },
};

const logCounter = getContext => {
  const [{counter}] = getContext()
  console.log("counter:", counter)
}

export const [ 
  CounterProvider, 
  useCounter,
  CounterContext
] = createDatanomy<TCounterStore>(initialState, reducers, scenarios)

2. Import and connect You store provider to the DOM branch, or whole app

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;

3. Connect to the Store using hook in functional components:

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;

4. Connect to the Store using Context.Consumer in functional, or class components:

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;

The same as a class component

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;

5. Connect to the Store using contextType in the class components:

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;

Tips and tricks

About Single Source of Truth (SST)

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.

getState function

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

How to contribute:

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

Bitcoin:

bc1q2g3drds728hmmrjsgdu6cm7xkclur277gva7qz

Ethereum:

0xc6C5673fa62F85070657F147246c167887Fc918D

Tron:

TKYjNtNR1Q9ZdgAA4d1TS5fMqBirM2attx

Everscale:

0:e26ebff8531f32de150074de9e7e83afa7aeeb2c8b16d0528d104758dfde31f2

Readme

Keywords

none

Package Sidebar

Install

npm i react-datanomy

Weekly Downloads

0

Version

0.3.1

License

MIT

Unpacked Size

34.7 kB

Total Files

13

Last publish

Collaborators

  • fiberliber