Refine
Sometimes, the built-in validation is not enough. You can run your own validation logic, including async code, to enforce exactly the refinement logic you need with .refine() and .refineBatch().
x.string().refine(
(value) => value.startsWith("ACC-"),
{ message: 'Must start with "ACC-"' }
)Async Validator
You can also make async calls for things like database lookups, API calls, or checking external systems:
x.string().refine(async (value) => {
const exists = await checkDatabaseId(value);
return { valid: exists, message: `ID "${value}" not found` };
})Batch validation strongly recommended
Be aware that this will run once for every cell in the column. If at all possible, prefer batch validation for expensive async work.
Batch Validation
.refineBatch() validates values from the column in batches. The function receives an array of values from the column and must return a result array of the same length. This is ideal for batching lookups that would be expensive to run for each value individually.
x.string().refineBatch(async (values) => {
// One batched DB lookup for the whole column chunk, not one per cell.
const found = await bulkCheckExists(values);
return found.map((exists, i) => ({
valid: exists,
message: exists ? undefined : `Not found: "${values[i]}"`,
}));
})Suggested Fixes
Both .refine() and .refineBatch() can return a suggestedFix object. The widget presents these as one-click fixes the user can apply during the review step.
x.string().refine((value) => {
const trimmed = value.trim();
if (trimmed === value) {
return { valid: true };
}
return {
valid: false,
message: "Remove leading or trailing whitespace",
suggestedFix: {
id: "trim-whitespace",
value: trimmed,
description: "Trim whitespace",
},
};
})