redux-revalidate
v0.1.0
Published
Validate your Redux store state with revalidate
Downloads
7
Maintainers
Readme
redux-revalidate
Validate your Redux store state with revalidate.
The typical use case for revalidate is creating validation function Redux Form components. However, if you're not using Redux Form but still want to validate your Redux store, then you can use redux-revalidate to automatically perform the validations. Redux-revalidate validates every new state produced by your reducer function(s) according to the validate function you create with revalidate itself. Redux-revalidate manages the error messages by adding its own state to the store.
Install
$ npm install --save redux-revalidate
Usage
Redux-revalidate exports a few functions to allow seamless integration with revalidate.
validateStore
The simplest approach is to validate your entire store thanks to the nested
field support of revalidate. You can use validateStore
as a store enhancer to
validate your entire store according to the provided validate function.
validateStore
takes as its first argument your validate function and an
optional options object as the second argument. validateStore
will add an
object to the root of your store that will contain any possible error messages
for other properties in your store. The default key for this object is errors
.
You can provide a different key if you're already using errors
as a key
yourself. Just provide the errorKey
option in the optional options argument:
e.g.validateStore(validate, { errorKey: 'myErrors' })
.
The API for validateStore
is still new, but for now it will immediately
validate your store when Redux runs your reducer function the first time. This
means error messages may immediately appear in your store before you've
dispatched any actions. If you want something more robust that handles whether a
particular field value is touched and when it should be validated, then you
should consider using Redux Form with revalidate.
Additionally, validateStore
will validate your store after any dispatched
action. Basically, if Redux calls your reducer function, then redux-revalidate
will validate the new state right after.
// ES2015 imports
import { createStore } from 'redux';
import { validateStore } from 'redux-revalidate';
import {
combineValidators,
composeValidators,
isAlphabetic,
isNumeric,
isRequired,
} from 'revalidate';
// CJS imports
const createStore = require('redux').createStore;
const validateStore = require('redux-revalidate').validateStore;
const r = require('revalidate');
const combineValidators = r.combineValidators;
const composeValidators = r.composeValidators;
const isAlphabetic = r.isAlphabetic;
const isNumeric = r.isNumeric;
const isRequired = r.isRequired;
// Usage
const UPDATE_DOG_NAME = 'UPDATE_NAME';
const UPDATE_DOG_AGE = 'UPDATE_AGE';
const updateDogName = (payload) => ({ payload, type: UPDATE_DOG_NAME });
const updateDogAge = (payload) => ({ payload, type: UPDATE_DOG_AGE });
const INITIAL_STATE = {
name: '',
age: '',
};
function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case UPDATE_DOG_NAME:
return { ...state, name: action.payload };
case UPDATE_DOG_AGE:
return { ...state, age: action.payload };
default:
return state;
}
}
const validate = combineValidators({
name: isRequired('Dog Name'),
age: composeValidators(
isRequired,
isNumeric
)('Dog Age'),
});
const store = createStore(reducer, validateStore(validate));
console.log(store.getState());
// { name: '',
// age: '',
// errors: { name: 'Dog Name is required',
// age: 'Dog Age is required' } }
store.dispatch(updateDogName('Tucker'));
console.log(store.getState());
// { name: 'Tucker',
// age: '',
// errors: { age: 'Dog Age is required' } }
store.dispatch(updateDogAge('abc'));
console.log(store.getState());
// { name: 'Tucker',
// age: 'abc',
// errors: { age: 'Dog Age must be numeric' } }
store.dispatch(updateDogAge('10'));
console.log(store.getState());
// { name: 'Tucker', age: '10', errors: {} }
With optional errorKey
option:
const store = createStore(
reducer,
validateStore(validate, { errorKey: 'myErrors' })
);
console.log(store.getState());
// { name: '',
// age: '',
// myErrors: { name: 'Dog Name is required',
// age: 'Dog Age is required' } }
With other enhancers like middleware
import { applyMiddleware, compose, createStore } from 'redux';
import thunkMiddlware from 'redux-thunk';
// ...
function delayUpdateDogName(payload, time) {
return (dispatch) => new Promise(resolve => {
setTimeout(() => {
dispatch(updateDogName(payload));
resolve();
}, time);
});
}
// ...
const store = createStore(
reducer,
compose(
validateStore(validate),
applyMiddleware(thunkMiddlware)
)
);
console.log(store.getState());
// { name: '',
// age: '',
// errors: { name: 'Dog Name is required',
// age: 'Dog Age is required' } }
store.dispatch(delayUpdateDogName('Tucker', 1000))
.then(() => {
console.log(store.getState());
// { name: 'Tucker',
// age: '',
// errors: { age: 'Dog Age is required' } }
});
With nested properties
As previously mentioned, arbitrarily nested properties in the store can be validated too:
import { combineReducers, createStore } from 'redux';
import {
combineValidators,
composeValidators,
isAlphabetic,
isNumeric,
isRequired,
} from 'revalidate';
import { validateStore } from 'redux-revalidate';
const UPDATE_CONTACT_NAME = 'UPDATE_CONTACT_NAME';
const UPDATE_CONTACT_AGE = 'UPDATE_CONTACT_AGE';
const UPDATE_DOG_BREED = 'UPDATE_DOG_BREED';
const UPDATE_DOG_NAME = 'UPDATE_DOG_NAME';
const UPDATE_DOG_AGE = 'UPDATE_DOG_AGE';
const updateContactName = (payload) => ({ payload, type: UPDATE_CONTACT_NAME });
const updateContactAge = (payload) => ({ payload, type: UPDATE_CONTACT_AGE });
const updateDogBreed = (payload) => ({ payload, type: UPDATE_DOG_BREED });
const updateDogName = (payload) => ({ payload, type: UPDATE_DOG_NAME });
const updateDogAge = (payload) => ({ payload, type: UPDATE_DOG_AGE });
const INITIAL_CONTACT = {
name: '',
age: '',
};
function contactReducer(state = INITIAL_CONTACT, action) {
switch (action.type) {
case UPDATE_CONTACT_NAME:
return { ...state, name: action.payload };
case UPDATE_CONTACT_AGE:
return { ...state, age: action.payload };
default:
return state;
}
}
const INITIAL_DOG = {
breed: '',
name: '',
age: '',
};
function dogReducer(state = INITIAL_DOG, action) {
switch (action.type) {
case UPDATE_DOG_BREED:
return { ...state, breed: action.payload };
case UPDATE_DOG_NAME:
return { ...state, name: action.payload };
case UPDATE_DOG_AGE:
return { ...state, age: action.payload };
default:
return state;
}
}
const validate = combineValidators({
'contact.name': isRequired('Name'),
'contact.age': composeValidators(
isRequired,
isNumeric
)('Age'),
'dog.breed': isAlphabetic('Breed'),
'dog.name': isRequired('Name'),
'dog.age': composeValidators(
isRequired,
isNumeric
)('Age'),
});
const reducer = combineReducers({
contact: contactReducer,
dog: dogReducer,
});
const store = createStore(
reducer,
validateStore(validate)
);
store.subscribe(() => console.log(store.getState()));
console.log(store.getState());
// { contact: { name: '', age: '' },
// dog: { breed: '', name: '', age: '' },
// errors:
// { contact: { name: 'Name is required', age: 'Age is required' },
// dog: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateContactName('Joe'));
// { contact: { name: 'Joe', age: '' },
// dog: { breed: '', name: '', age: '' },
// errors:
// { contact: { age: 'Age is required' },
// dog: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateContactAge('abc'));
// { contact: { name: '', age: 'abc' },
// dog: { breed: '', name: '', age: '' },
// errors:
// { contact: { name: 'Name is required', age: 'Age must be numeric' },
// dog: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateContactAge('30'));
// { contact: { name: 'Joe', age: '30' },
// dog: { breed: '', name: '', age: '' },
// errors:
// { contact: {},
// dog: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateDogBreed('123'));
// { contact: { name: 'Joe', age: '30' },
// dog: { breed: '123', name: '', age: '' },
// errors:
// { contact: {},
// dog:
// { breed: 'Breed must be alphabetic',
// name: 'Name is required',
// age: 'Age is required' } } }
store.dispatch(updateDogBreed('Sheltie'));
// { contact: { name: 'Joe', age: '30' },
// dog: { breed: 'Sheltie', name: '', age: '' },
// errors:
// { contact: {},
// dog: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateDogName('Tucker'));
// { contact: { name: 'Joe', age: '30' },
// dog: { breed: 'Sheltie', name: 'Tucker', age: '' },
// errors: { contact: {}, dog: { age: 'Age is required' } } }
store.dispatch(updateDogAge('abc'));
// { contact: { name: 'Joe', age: '30' },
// dog: { breed: 'Sheltie', name: 'Tucker', age: 'abc' },
// errors: { contact: {}, dog: { age: 'Age must be numeric' } } }
store.dispatch(updateDogAge('10'));
// { contact: { name: 'Joe', age: '30' },
// dog: { breed: 'Sheltie', name: 'Tucker', age: '10' },
// errors: { contact: {}, dog: {} } }
Caveat:
If you use combineReducers
like the previous example, you might see a warning
like this:
Unexpected key "errors" found in previous state received by the reducer.
Expected to find one of the known reducer keys instead: "contact", "dog".
Unexpected keys will be ignored.
To fix this issue, import the function errorsReducer
and mount it at the
errors
key or whatever custom key you use to hold your error message state:
import { errorsReducer } from 'redux-revalidate';
// ...
// With default key
const reducer = combineReducers({
contact: contactReducer,
dog: dogReducer,
errors: errorsReducer,
});
// With custom `errorKey` supplied to `validateStore`
const reducer = combineReducers({
contact: contactReducer,
dog: dogReducer,
myErrors: errorsReducer,
});
validateReducer
If you want finer-grain control over what portions of your store are validated
and where to store the error message state, you can opt to wrap your reducer
function(s) with the validateReducer
function.
validateReducer
is a curried function. The first invocation takes the validate
function as the first argument and optional options argument as the second
argument. The returned function takes a reducer function as the first argument
and preloaded/initial state as the second argument. In fact, validateStore
uses validateReducer
internally to just wrap your root reducer. Here is one of
the previous examples with the contact and dog but using validateReducer
to
validate the dog state and store any dog validation errors in the same state as
the dog:
import { combineReducers, createStore } from 'redux';
import {
combineValidators,
composeValidators,
isAlphabetic,
isNumeric,
isRequired,
} from 'revalidate';
import {
errorsReducer,
validateReducer,
validateStore,
} from 'redux-revalidate';
// ...
const validate = combineValidators({
'contact.name': isRequired('Name'),
'contact.age': composeValidators(
isRequired,
isNumeric
)('Age'),
});
const validateDog = combineValidators({
breed: isAlphabetic('Breed'),
name: isRequired('Name'),
age: composeValidators(
isRequired,
isNumeric
)('Age'),
});
const reducer = combineReducers({
contact: contactReducer,
dog: validateReducer(
validateDog, { errorKey: 'dogErrors' }
)(dogReducer),
errors: errorsReducer,
});
const store = createStore(
reducer,
validateStore(validate)
);
store.subscribe(() => console.log(store.getState()));
console.log(store.getState());
// { contact: { name: '', age: '' },
// dog:
// { breed: '',
// name: '',
// age: '',
// dogErrors: { name: 'Name is required', age: 'Age is required' } },
// errors: { contact: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateContactName(''));
// { contact: { name: '', age: '' },
// dog:
// { breed: '',
// name: '',
// age: '',
// dogErrors: { name: 'Name is required', age: 'Age is required' } },
// errors: { contact: { name: 'Name is required', age: 'Age is required' } } }
store.dispatch(updateContactName('Joe'));
// { contact: { name: 'Joe', age: '' },
// dog:
// { breed: '',
// name: '',
// age: '',
// dogErrors: { name: 'Name is required', age: 'Age is required' } },
// errors: { contact: { age: 'Age is required' } } }
store.dispatch(updateContactAge('abc'));
// { contact: { name: 'Joe', age: 'abc' },
// dog:
// { breed: '',
// name: '',
// age: '',
// dogErrors: { name: 'Name is required', age: 'Age is required' } },
// errors: { contact: { age: 'Age must be numeric' } } }
store.dispatch(updateContactAge('30'));
// { contact: { name: 'Joe', age: '30' },
// dog:
// { breed: '',
// name: '',
// age: '',
// dogErrors: { name: 'Name is required', age: 'Age is required' } },
// errors: { contact: {} } }
store.dispatch(updateDogBreed('123'));
// { contact: { name: 'Joe', age: '30' },
// dog:
// { breed: '123',
// name: '',
// age: '',
// dogErrors:
// { breed: 'Breed must be alphabetic',
// name: 'Name is required',
// age: 'Age is required' } },
// errors: { contact: {} } }
store.dispatch(updateDogBreed('Sheltie'));
// { contact: { name: 'Joe', age: '30' },
// dog:
// { breed: 'Sheltie',
// name: '',
// age: '',
// dogErrors: { name: 'Name is required', age: 'Age is required' } },
// errors: { contact: {} } }
store.dispatch(updateDogName('Tucker'));
// { contact: { name: 'Joe', age: '30' },
// dog:
// { breed: 'Sheltie',
// name: 'Tucker',
// age: '',
// dogErrors: { age: 'Age is required' } },
// errors: { contact: {} } }
store.dispatch(updateDogAge('abc'));
// { contact: { name: 'Joe', age: '30' },
// dog:
// { breed: 'Sheltie',
// name: 'Tucker',
// age: 'abc',
// dogErrors: { age: 'Age must be numeric' } },
// errors: { contact: {} } }
store.dispatch(updateDogAge('10'));
// { contact: { name: 'Joe', age: '30' },
// dog:
// { breed: 'Sheltie',
// name: 'Tucker',
// age: '10',
// dogErrors: {} },
// errors: { contact: {} } }