
Research
Malicious npm Packages Impersonate Flashbots SDKs, Targeting Ethereum Wallet Credentials
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
leaf-validator
Advanced tools
Declarative state progression & validation for React apps.
MongoDB uses a very similar concept called Dot Notation.
Redis encourages a best practice very similar to this concept called JSONPath
Advantages in:
Other related features include:
Refer to this example model in the example usage below:
{
"person": {
"firstName": "Stewart",
"lastName": "Anderson",
"contact": {
"email": "stewie1570@gmail.com",
"phoneNumber": "0123456789"
}
}
}
Using leaf-validator, you could allow a user to edit phone number in the above model like this:
import { Leaf } from "leaf-validator";
const [model, setModel] = useState({});
<Leaf
model={model}
onChange={setModel}
location="person.contact.phoneNumber">
{(phoneNumber, setPhoneNumberInModel) => (
<label>
Phone Number
<TextInput value={phoneNumber} onChange={setPhoneNumberInModel} />
</label>
)}
</Leaf>;
In the above example, calling setPhoneNumberInModel is roughly the same as writing and calling this funtion (below) except it will even work when model is null or undefined etc...
function setPhoneNumberInModel(updatedPhoneNumber) {
setState({
...model,
person: {
...model.person,
contact: {
...model.person.contact,
phoneNumber: updatedPhoneNumber,
},
},
});
}
This function could have been written inside a reducer. The point is, you can just declare the location to update and leaf-validator will handle the immutable state progression for you.
The problems this solves:
This API/abstraction does not suffer from the problems that set state callback functions and reducers of complex models suffer from:
If the phoneNumber is invalid that means contact is invalid. If contact is invalid then person is invalid. So the shape of the validation model needs to mirror the shape of the model it validates.
Lets declaratively update the validation model:
import { Leaf, useValidationModel } from "leaf-validator";
const isRequired = (value: string) =>
(!value || value.trim() === "") && ["Value is required"];
const isValidPhoneNumber = (value: string) =>
!/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(value) && [
`"${value || ""}" is not a valid phone number`,
];
const [model, setModel] = useState({});
const validationModel = useValidationModel();
<Leaf
model={model}
onChange={setModel}
location="person.contact.phoneNumber"
validationModel={validationModel}
validators={[isRequired, isValidPhoneNumber]}
>
{(phoneNumber, setPhoneNumber, showErrors, errors) => (
<label>
Phone Number
<TextInput
value={phoneNumber}
onChange={setPhoneNumber}
onBlur={showErrors}
/>
{errors.length > 0 && (
<ul>
{errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
)}
</label>
)}
</Leaf>;
So the above code will track the validation state of each leaf. Now you have a validation model with an API that can tell you if there are errors at any location in the model and if there any errors downstream of a given location.
Validation Queries
validationModel.getAllErrorsForLocation("person.contact");
//will return all errors (or []) at that location or downstream of that location.
validationModel.get("person.contact");
//will return all errors at that location.
//Note: since in this example there would only be errors
//at the leaf values you would likely use example (below) instead:
validationModel.get("person.contact.phoneNumber");
//keep in mind that the errors for a leaf are passed via the
//children render function parameters as in the example above.
Async Validation
The functions that you pass to the validators attribute can by async. That is supported. However, a lot of the time when you have asynchronous validators you don't want the validators to be run every time the input changes. For example, say you have a text input for user name on a new user registration form. You might have an async validator to make sure the user name isn't already taken. You probably don't want that to run on each keystroke. So we have deferredValidators. The deferredValidators run deferMilliseconds trailing the last onChange. The default deferMilliseconds is 500 milliseconds.
import { Leaf, useValidationModel } from "leaf-validator";
const [model, setModel] = useState({});
const validationModel = useValidationModel();
<Leaf
model={model}
onChange={setModel}
location="person.userName"
validationModel={validationModel}
deferredValidators={[isUserNameAvailable]}
>
{(userName, setUserName, showErrors, errors) => (
<label>
User Name
<TextInput value={userName} onChange={setUserName} onBlur={showErrors} />
{errors.length > 0 && (
<ul>
{errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
)}
</label>
)}
</Leaf>;
How do I know if any of the async validators are still in-flight?
validationModel.isValidationInProgress(); //returns boolean
Let's say you update your server side code and your requests to the server are now returning a different shape of model. Using this declarative API you can support backward compatibility because it will support multiple shapes of the model. See example below.
import { Leaf } from "leaf-validator";
const [model, setModel] = useState({});
<Leaf
model={model}
onChange={setModel}
location="currentLocation.phoneNumber"
failOverLocations={[
"oldLocation.phoneNumber",
"olderLocation.phoneNumber",
"oldestLocation.phoneNumber",
]}
>
{(phoneNumber, setPhoneNumber) => (
<label>
Phone Number
<TextInput value={phoneNumber} onChange={setPhoneNumber} />
</label>
)}
</Leaf>;
This will try to read from the location in the model first. If location in the model is not available then it will start looking at the failOverLocations. Updates are done to location in the model.
Now you can run DB migration whenever you want.
useLoadingState
const [isRunning, showRunningWhile] = useLoadingState();
//or
const [isRunning, showRunningWhile] = useLoadingState({ minLoadingTime: 250 });
//all options are optional...
.
.
.
//showRunningWhile will return a promise that rejects or resolves the same value
//that the getDataFromServer() would've resolved on its own.
const response = await showRunningWhile(getDataFromServer());
.
.
.
{isRunning && <span>Running...</span>}
useErrorHandler
Error Boundaries are meant to recover the application from an un-renderable (crashed) state.
Very often and especially for most asynchrounous errors you'll want the error handling behavior to simply inform the user that an attempted operation has failed and allow the user to acknowledge that failure.
const { errorHandler, clearError, errors } = useErrorHandler();
await errorHandler(submitData());
// or
const response = await errorHandler(getData());
// or
await errorHandler(getData().then(useIt));
// or
async function useData(){
useIt(await getData());
}
await errorHandler(useData());
{errors?.length > 0 && <ul>
{errors.map(error => <li key={error.message}>
<button onClick={() => clearError(error)}>X</button>
{error.message}
</li>)}
</ul>}
useDeferredEffect
Same as useEffect but will only fire once per configured milliseconds timeout.
useDeferredEffect(
() => {
//whatever effect(s) you want
},
deferMilliseconds || 500,
[targetValue, location, deferMilliseconds]
);
useMountedOnlyState
(exactly the same as useState except it will not set state when the component is not mounted)
useLocalStorageState
Similar to useState but as the name implies it stores to local storage under the given key. This hook also uses storage events to sync state as local storage is edited. So if (for example) the user has two windows or tabs open, both windows or tabs states will be updated at the same time. The useLocalStorageState hook uses useMountedOnlyState as the underlying local state so it will not make un-mounted updates.
Note: updates to this state are expensive as they need to be JSON stringified and parsed on each update.
const [state, setState] = useLocalStorageState("StorageKey");
createManagedState
This is an easier way to manage state in a context. It offers:
const useUser = () => {
const [user, setUser] = useState({ firstName: "", lastName: "" });
return {
user,
setFirstName: (firstName: string) =>
setUser((user) => ({ ...user, firstName })),
setLastName: (lastName: string) =>
setUser((user) => ({ ...user, lastName }))
};
};
const [UserContextProvider, useUserContext] = createManagedContext(useUser);
const App = () => {
return (
<UserContextProvider>
<User />
</UserContextProvider>
);
};
const User = () => {
const { user, setFirstName, setLastName } = useUserContext();
return <form>
[User Form Here]
</form>;
}
Advanced Usages of createManagedContext
:
const useUser = ({ firstName, lastName }) => {
const [user, setUser] = useState({ firstName, lastName });
return {
user,
setFirstName: (firstName: string) =>
setUser((user) => ({ ...user, firstName })),
setLastName: (lastName: string) =>
setUser((user) => ({ ...user, lastName }))
};
};
const [UserContextProvider, useUserContext, UserContext] = createManagedContext(useUser);
<UserContextProvider firstName="Stewart" lastName="Anderson">
<EditUser />
<DisplayUser />
</UserContextProvider>
const UserContextPassThrough = ({ children }) => {
const parentContext = useContext(UserContext);
return parentContext
? children
: <UserContextProvider>
{children}
</UserContextProvider>;
};
Example:
const theObject = {
prop1: {
prop1: {
target: "original value",
},
prop2: {},
},
prop2: {},
};
const progession = set("prop1.prop1.target").to("updated value").in(theObject);
will return the equivalent of:
{
...(theObject || {}),
prop1: {
...(theObject?.prop1 || {}),
prop1: {
...(theObject?.prop1?.prop1 || {}),
target: "updated value"
}
}
}
Example:
let obj = ({
level1: {
prop1: "expected value"
}
};
get("level1.prop1").from(obj);
will return the equivalent of:
obj?.level1?.prop1;
The difference being that the location of the target value is a string and therefore allows for more dynamic access to the target value.
The diffs are coheasive with the set method such that you could run the diffs via the set method on the original object to re-construct the updated object. (see example below)
Example (from a unit test):
const original = {
outer: [
{
wrapper: {
changed: "p1 value 1",
original: "p2 value 1",
},
},
],
};
const updated = {
outer: [
{
wrapper: {
changed: "p1 value 2",
new: "p2 value 1",
},
},
],
};
const diffs = diff.from(original).to(updated);
const constructed = [original, ...diffs].reduce((currentValue, diff) =>
set(diff.location).to(diff.updatedValue).in(currentValue)
);
expect(constructed).toEqual(updated);
The diff method is especially useful when you need to send a diff of what the user last loaded vs. the user updated model to the server. When updates are done via diffs instead of sending full user updated objects you can avoid race-conditions that could cause concurrent users to overwrite each other.
Example (from a unit test):
expect(
diff.from(null).to({
some: {
complex: {
object: {
with: ["values"],
and: ["other", "values"],
},
},
},
})
).toEqual([
{
location: "",
updatedValue: {
some: {
complex: {
object: {
with: ["values"],
and: ["other", "values"],
},
},
},
},
},
]);
expect(
leafDiff.from(null).to({
some: {
complex: {
object: {
with: ["values"],
and: ["other", "values"],
},
},
},
})
).toEqual([
{ location: "some.complex.object.with.0", updatedValue: "values" },
{ location: "some.complex.object.and.0", updatedValue: "other" },
{ location: "some.complex.object.and.1", updatedValue: "values" },
]);
NOTE: See this example for a simple way to use these diffs to operate Mongo updates.
FAQs
Declarative state progression & validation
The npm package leaf-validator receives a total of 43 weekly downloads. As such, leaf-validator popularity was classified as not popular.
We found that leaf-validator demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
Security News
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
Security News
Following last week’s supply chain attack, Nx published findings on the GitHub Actions exploit and moved npm publishing to Trusted Publishers.