Manage Multiple User Permissions with Yup and Formik

Victor Gonzalez
Yellowme
Published in
5 min readJun 8, 2020

Usually, when creating a form on React, a common approach is to use Formik (a React library to build forms) and yup (a JavaScript schema builder for value parsing and validation). If you follow this approach, after some time coding you’ll end up having a Formik component that uses a Yup schema in order to validate form values when needed, and that’s cool!

But what about handling multiple users with different permissions using the same Form component? For example: some users should see some inputs and some others shouldn’t, or maybe all users should see all inputs but some inputs are optional/required to some of them.

I want to show you a solution that I use: a builder schema function. Also, I’m going to compare some other approaches: using yup .when() mixed function, and multiple schemas, but first let’s set a scenario…

The scenario

Imagine you are developing a piece of software for a car rental company and they want a feature that allows customers to know if their cars are allowed to get into the car rental system, so car owners can get a commission.

And let’s say that after the user uploads their car info to the system, employees from the company can edit or upload more info about the car.

So, for now we have 2 user types: publicUser and companyMechanic, and these users have different permissions to edit the car info on the form with basic inputs: color, year, motor type and motor serial number.

Table with user permissions:

+--------------+--------------------+-----------------+
| Car model | | |
| attributes | publicUser | companyMechanic |
+--------------+--------------------+-----------------+
| color | required | required |
| year | required | required |
| motor type | notRequired | required |
| motor serial | | |
| number | notRequired | required |
+--------------+--------------------+-----------------+

There are some approaches to take

const carSchema = yup.object().shape({
color: yup.string(),
year: yup.number(),
motorType: yup.string(),
motorSerialNumber: yup.string(),
});

Yup mixed.when(…)

Yup has a .when() mixed function that adjusts the schema based on a sibling or sibling children fields, so then you can add a new attribute to the schema userType and concat the .when() to motorSerialNumber and userType, so when userType is equal to “publicUser” then those motorSerialNumber and userType schema attributes are not required.

const carSchema = yup.object().shape({
color: yup.string().required(),
year: yup.number().required(),
motorType: yup.string()
.when("userType", {
is: (val) => val == "publicUser",
then: yup.string().notRequired(),
otherwise: yup.string().required(),
}),
motorSerialNumber: yup.string()
.when("userType", {
is: (val) => val == "publicUser",
then: yup.string().notRequired(),
otherwise: yup.string().required(),
}),
userType: yup.string().nullable(), 👈
});

As you can see in the image above, when a publicUser is using the form, just color and year are required, but when any other user is filling the form all inputs are required.

This is not the best practice because you have to add an input and then hide it. Also, with when() you can only handle 2 user type permissions, and semantically the Form component doesn’t need to know the user type to pass it to the schema... but it kinda works.

Many yup schemas

Another approach is using as many schemas as user types. For our scenario we are going to use publicUserSchema and companyMechanicSchema with a commonSchema object that is shared by the 2 user schemas.

const commonSchema = {
color: yup.string().required(),
year: yup.number().required(),
}
const publicUserSchema = yup.object().shape({
...commonSchema,
motorType: yup.string(),
motorSerialNumber: yup.string(),
});
const companyMechanicSchema = yup.object().shape({
...commonSchema,
motorType: yup.string().required(),
motorSerialNumber: yup.string().required(),
});

This approach consists in setting the Formik validationSchema prop depending on the user type who is filling the form:

const userType = "publicUser";<Formik
validationSchema={ userType === "companyMechanic" ?
companyMechanicSchema : publicUserSchema
}
initialValues={initialValues}
onSubmit={onSubmit}
>
{(props) => {
return (
<Form>
<label
className={props.errors.color && "error"}
>color</label>
<Field name="color" />
{/* More inputs */}
<label
className={
props.errors.motorSerialNumber && "error"
}
>motorSerialNumber</label>
<Field name="motorSerialNumber" />
<button>Submit</button>
</Form>
);
}}
</Formik>

It looks like this:

Pros:

  • It allows handling as many schemas as you need.
  • Better, more readable code.

Cons:

  • If you need to handle a lot of user types, you’ll end up with a lot of different schemas.

Create a builder schema function

This approach uses a function carSchema(userType)that receives a string and returns a yup schema. Inside this function you can overwrite whatever attributes you need to the schemaAttributes object and then create the schema with this object:

const carSchema = (userType) => {
let schemaAttributes = {
color: yup.string().required(),
year: yup.number().required(),
motorType: yup.string(),
motorSerialNumber: yup.string(),
}
// overwrite the attributes if needed
if (userType === "companyMechanic") {
schemaAttributes.motorType = yup.string().required();
schemaAttributes.motorSerialNumber = yup.string().required();
}
//create and return the schema
return yup.object().shape(schemaAttributes)
};

So then, in the Formik component you have:

<Formik
validationSchema={carSchema(userType)}
initialValues={initialValues}
>
{(props) => {
let { errors } = props
return (
<Form>
<label className={errors.color && "error"}>color</label>
<Field name="color" />
{/* More inputs */}
<label className={errors.motorSerialNumber && "error"}>
motorSerialNumber
</label>
<Field name="motorSerialNumber" />
<button>Submit</button>
</Form>
);
}}
</Formik>

Yes, it looks the same as in the last approach but with a cleaner code!

Pros:

  • It allows handling as many schemas as you need.
  • Better, more readable code.
  • Semantically, an external function is the one responsible for creating a schema depending on the user type.
  • Highly configurable schema depending on the user type or any params.
  • You can configure the schema object depending on any params the function who creates the schema receives!

Conclusion

If there is a need to handle many user permissions and forms, I think the builder schema function is the best one, so you don’t end up having a lot of schemas. You can define a basic object schema inside a function and then pass some params to that function to configure the schema and return it.

--

--

Victor Gonzalez
Yellowme
Writer for

Just another software engineer who loves dance latin latin rhythms. Founder and maker of Pau chrome extension http://pauapp.com