Options
All
  • Public
  • Public/Protected
  • All
Menu

redux-state-validation

NPM npm version codecov GitHub stars code style: prettier

Why redux-state-validation ?

Add validator to reducer's result handy. Common validator have problem that depends on form or action thus, will be inclined to be weak by modification or increse redundant definitions.This library is simple to extend state array has arbitrary name (default:errors) with checking error and set the messages after each reducer's callback.

This library for react-native rather than react, but we aim to seemless reuse redux state loosely-coupled each applications with react-native and web.

Demo

codesandbox

Install

npm i --save redux-state-validation

stable versions equals or newer than 3.1.0.

Usage

withValidateReducer(reducer:Reducer<State,action:Action>, validator:{id:string,validate(state:State): boolean}[]):Reducer(state:State,action:Action)

Type Parameter Key is corresponded to errorStateId's value in option which is second arguments of watchRootReducer. this API chains a reducer function to validation callback and if the result's not valid rollback state the previous one with save error object to internal state along configuration.

watchRootReducer(reducer:Reducer<State,action:Action>,options?:{errorStateId?:string = 'errors',returnType?:string = 'object'}):Reducer(state:State&Record<Key, Error[]>,action:Action)

This API writes errors of validate methods to redux state, the depth can be arbitrary as how deep you apply watchRootReducer to selected reducer . This must call after all validate methods in the watchRootReducer applied to reducer's scope.This means we must not call watchRootReducer to get errors of parent's reducers or higher than them.This API must use to watch about child reducers or the reducer itself whether validation methods find errors, otherwise it is possible to start flushing errors to state though some validate method still aren't called depends on the order of function invokes. Imagine the tree ,this derived one root node has many child nodes and decendants and depth-first search recuring process. Imagine one of these nodes is reducer,and the reducer executes fastest than the chirdlen and return last its'state. The callback set by watchRootReducer run after the reducer's return values, so the reducer can watch about all decendants and itself whether catch errors, therefore first origin node reducer is able to watch all of reducers it includes as decendants. So I highly recomend to control error states at top reducer.

ValidationWatcherFactory (Advanced)

ValidationWatcherFactory class are validator class can be generated by getInstanceMethod.You can make original vaildator logic, if you follow our abstract method flow, but, this is so advanced and I can't fix this feature and implement perfect DI interface yet.

getInstance():{watchRootReducer,withValidateReducer}

getIncstance can recreate redux-state-validation instances have new self inner state apart from another. This is for you want to watch validating multi Reducers in some special reason like having reducers seperated store application, but I recommend that don't this as possible. I'll explain later at discussion.

Examples

object case(default)

import {
  withValidateReducer,
  watchRootReducer
} from 'redux-state-validation'

const postalReducer = (
  state: {
    postalCode
  },
  action
) => {
  if (action.type === "SET_NUMBER") {
    return {
      ...state,
      postalCode: 123
    };
  }
  if (action.type === "SET_STRING") {
    return {
      ...state,
      postalCode: "123"
    };
  }
  return state;
};

const _validateReducer = withValidateReducer(postalReducer, [
  {
    error: {
      id: "postalCode",
      message: "Invalid PostalCode"
    },
    validate: _state => isNumber(_state.postalCode)
  }
]);

const rootReducer = watchRootReducer(_validateReducer, {
  errorStateId: "hoge"
});

const store = createStore(rootReducer, { postalCode: 0 });

store.dispatch({ type: "SET_STRING" });

/**
*  store.getState()
*  the output:
*  {
*     postalCode:0,
*     hoge: {
*       "postalCode": {
*         id: "postalCode",
*         message: "Invalid PostalCode"
*       }
*     }
*  }
**/

store.dispatch({ type: "SET_NUMBER" });

/**
*  store.getState()
*  the output:
*  {
*     postalCode:123,
*     hoge: {}
*  }
**/

array case

WARNING: this behavior deffer to older version. So ,you need to migrate if you use older versions than 2.2.x.

import {
  withValidateReducer,
  watchRootReducer
} from 'redux-state-validation'

const postalReducer = (
  state: {
    postalCode
  },
  action
) => {
  if (action.type === "SET_NUMBER") {
    return {
      ...state,
      postalCode: 123
    };
  }
  if (action.type === "SET_STRING") {
    return {
      ...state,
      postalCode: "123"
    };
  }
  return state;
};

const _validateReducer = withValidateReducer(postalReducer, [
  {
    error: {
      id: "postalCode",
      message: "Invalid PostalCode"
    },
    validate: _state => isNumber(_state.postalCode)
  }
]);

const rootReducer = watchRootReducer(_validateReducer, {
  errorStateId: "hoge", {
    returnType: 'array'
  }
});

const store = createStore(rootReducer, { postalCode: 0 });

store.dispatch({ type: "SET_STRING" });

/**
*  store.getState()
*  the output:
*  {
*     postalCode:0,
*     hoge: {
*       postalCode:[
*         {
*           id: "postalCode",
*           message: "Invalid PostalCode"
*         }
*       ]
*     }
*  }
**/

store.dispatch({ type: "SET_NUMBER" });

/**
*  store.getState()
*  the output:
*  {
*     postalCode:123,
*     hoge: {}
*  }
**/

action validation

we prepare new feature equlls and more than 2.1.0 if the second argument specific your validator it verify the payload like that

const rootReducer = watchRootReducer(
    combineReducers({
      postalCode: withValidateReducer(initialStateUndefinedReducer, [
        {
          error: {
            id: "postalCode1",
            message: "Invalid PostalCode"
          },
          validate: (_, action: any) => Number(action.value) > 100
        },
        {
          error: {
            id: "postalCode2",
            message: "Invalid PostalCode"
          },
          validate: _ => false
        }
      ])
    })
  );
const store = createStore(rootReducer);
store.dispatch({
  type: "SET_NUMBER",
  value: 123
});
let state = store.getState();
t.truthy(Object.keys(state.errors).length === 1);
t.truthy(state.errors[Object.keys(state.errors)[0]].id ==="postalCode2");
store.dispatch({
  type: "SET_NUMBER",
  value: 0
});
state = store.getState();
t.truthy(Object.keys(state.errors).length === 1);
t.truthy(state.errors[Object.keys(state.errors)[0]].id ==="postalCode1");

this test code imply that with action argument to validate and catch invalid action, reducer doesn't run and return soon current state for optimize by cutting down processing. If you don't prefer this procedure, you can get all results of validators using next state an action validator results specific afterReducer:boolean option. So, let this test added to some modifications are like bellow

const rootReducer = watchRootReducer(
  combineReducers({
    postalCode: withValidateReducer(initialStateUndefinedReducer, [
      {
        afterReduce: true,
        error: {
          id: "postalCode1",
          message: "Invalid PostalCode"
        },
        validate: (_, action: any) => Number(action.value) > 100
      },
      {
        error: {
          id: "postalCode2",
          message: "Invalid PostalCode"
        },
        validate: _ => false
      }])
    })
  );
  const store = createStore(rootReducer);
  store.dispatch({
    type: "SET_NUMBER",
    value: 123
  });
  let state = store.getState();
  t.truthy(state.errors[Object.keys(state.errors)[0]].id === "postalCode2");
  t.truthy(Object.keys(state.errors).length === 1);
  store.dispatch({
    type: "SET_NUMBER",
    value: 0
  });
  state = store.getState();
  t.truthy(Object.keys(state.errors).length === 2);

afterReduce option makes error results both nextState only validator, and with action validator, so you specify afterReduce, you want to do that.

But I recommended that reducers needs verified actions are all with action validators and don't use next state validators. you can satisfy anything processing only either type reducers if you reducers are correct structure and mapping. Using Action validators are trade-off because you must specify validators for each actions though state validator are always one validaton can be applied to same state.

idSelector (version >2.1.0)

This feature is useful for if you have many relationships uis, of mapping redux state,

const rootReducer = watchRootReducer(
    combineReducers({
      postalCode: withValidateReducer(initialStateUndefinedReducer, [
        {
          afterReduce: true,
          error: {
            id: "postalCode1",
            message: "Invalid PostalCode"
          },
          idSelector: (errorId, action: { meta?: { id: string } }) =>
            (action.meta && action.meta.id) || errorId,
          validate: (_, action: any) => Number(action.value) > 100
        },
        {
          error: {
            id: "postalCode2",
            message: "Invalid PostalCode"
          },
          idSelector: (errorId, action: { meta?: { id: string } }) =>
            (action.meta && action.meta.id) || errorId,
          validate: _ => false
        }
      ])
    })
  );
  const store = createStore(rootReducer);
  store.dispatch({
    meta: {
      id: "component1"
    },
    type: "SET_NUMBER",
    value: 0
  });
  const state = store.getState();

/**
*  store.getState()
*  the errors output:
*  errors:{
*    component1: {
*      postalCode1: {
*        id: "postalCode1",
*        message: "Invalid PostalCode"
*      },
*      postalCode2: {
*        id: "postalCode2",
*        message: "Invalid PostalCode"
*      }
*    }
*  }
**/

So, you can set arbitral ids for getting errors through action input value.

strict option with validator

(warning is obsoleted, and default is same behavior of it, because we use more frequently warning than strict validation)

Strict option can enable us to update state even if your validate rules are violated unless there are no violated validators without strict option.

See the test example.

test("if useing strict option of validator, result are set by payload ", async t => {
  const rootReducer = watchRootReducer(
    combineReducers({
      postalCode: withValidateReducer(initialStateUndefinedReducer, [
        {
          error: {
            id: "postalCode1",
            message: "Invalid PostalCode"
          },
          validate: (_, action: any) => Number(action.value) < 100,
          strict: true
        }
      ])
    })
  );
  const store = createStore(rootReducer);
  store.dispatch({
    type: "SET_NUMBER",
    value: 123
  });
  const state = store.getState();
  t.truthy(state.errors[Object.keys(state.errors)[0]].id === "postalCode1");
  t.truthy(Object.keys(state.errors).length === 1);
  t.deepEqual(state.postalCode, 123);
});

this options for the case if input mounting your redux state and you want to disallow users to input keys until submitting.

manual and force writing validation results

You can force writing Errors set filtered validators like

  const rootReducer = watchRootReducer(_validateReducer);
  const store = createStore(rootReducer, { postalCode: 0 });
  const validatorsLocal = [
    {
      error: {
        id: "postalCode",
        message: "Invalid PostalCode"
      },
      validate: _state => isNumber(_state.postalCode)
    }
  ];
  const validate = createStaticValidator({ state: validatorsLocal });
  const target = {state:{ postalCode: "hoge" }}
  store.dispatch(validate(target));
  const errors = store.getState().errors;
  console.log(result, errors);
/* output:
{
  postalCode: {
    id: "postalCode",
    message: "Invalid PostalCode"
  },
}
*/

when you want to trigger getErrors whenever or force surpressing errors already exists unavoidably, use that.

And you can force to set arbital error messages as long as this library's format.

test("can set action for errors", async t => {
  const action = setValidatorResults({ foo: { id: "bar", message: "error" } });
});
/* output:
{
  foo: {
    id: "foo",
    message: "error"
  },
}
*/

This is not recommended except for when you are inevitable.

See more detail here.

Discussion

Though I recommend that watchRootReducer should set rootRedducer state, you can create error state at deeper located in nested state.Most case are enough at root only because dispatch methods are always synchronous unless odd acync actions breakes dispatch mutation rules (ordinaly you don't care mutch. the case does't happen if you use normally redux, async callbacks waits until process done if it is being executed ).

Tips

If you use redux, this library easily is able to be introduced in same reducers directory. Redux structures hav many valiation. Our redux structures.

redux/StateName/ - (sagas)
                 - actions
                 - reducers
                 - index

and export all of sets of redux layer from redux directory. so we applied this layer to

redux/StateName/ - (sagas)
                 - actions
                 - reducers
                 - validators
                 - index

It helps us to make redux each state to be able to cast on and off however these application is so big. validators and reducers examples are like bellow (we use typescript so, this example written typescript)

//validators.ts
//...
export const period: Validator<ProfileTypes.Episode['period'], PeriodValidatorId>[] = [
  {
    error: {
      id: 'Episode/period/',
      message: 'Internal: Invalid DateFormat'
    },
    validate: (value) => {
      let valid = moment(value.from, 'YYYY/MM').isValid()
      valid = moment(value.by, 'YYYY/MM').isValid() && valid
      return valid
    }
  }
]
//reducers.ts
import * as validators from '.validators/'
//...
const period = withValidateReducer(
  handleActions(
    {
      [actionTypes.SET_PERIOD]: (state, { payload }: actionTypes.EpisodeActions['setPeriod']) => payload!,
    },
    initialState.period
  ),
  validators.period
)

This way is pretty in the point of that validators and reducers are not so tightly-coupled and , no-coupled actionCreator form and the other reducers.

we make the snippets reducers with handle actions of redux-actions.

How You Contribute

Anyone welcome if you want to help or use it better.Please contact me or create issue freely.

License

MIT

Generated using TypeDoc