Formik passing context to yup validation - javascript

I've searched the internet literally everywhere, and also the docs but I couldn't find a solution that works in my current project. Quick bit of context:
We have a form, which displays additional fields depending on whether you're adding or editing. When adding, we have 2 different fields. When editing, we have 5 different fields.
Now when we are adding, I do not want these remaining three fields to be validated. Because they will be required when you edit your form. This is where the context will come in.
Currently I'm using the validate prop on the <Formik /> component like so:
<Formik
{...}
validate={(values: FormValues) => {
try {
validateYupSchema(values, getVersionsSchema, true, {
mode: "testing"
});
} catch (e) {
return yupToFormErrors(e);
}
return {};
}}
>
The validateYupSchema function accepts these 4 parameters which I've filled in:
values: T, schema: any, sync?: boolean, context?: any
Now in my validationSchema, I'd like to access this context object. Currently googling for the past 6 hours it constantly recommends me this approach:
export const getVersionsSchema = (t: TFunction<Namespace<"en">>) =>
Yup.object().shape({
{...}
optIn: Yup.string().when("mode", (mode, schema) => {
console.log(mode, schema);
return schema.required("test");
}),
{...}
});
So here you see I'm using when and apparently I have to pass the variable as a string name as the first argument, and a function as the second argument with the context variable name and the schema as arguments.
However, when I log mode in this case, it's undefined...
Can anybody point me in the right direction perhaps?
Here's a codesandbox:
https://codesandbox.io/s/condescending-water-3fmvsv?file=/src/App.js

Related

Is it possible to show some kind of warning if a prop value doesn't match a Regex

Let's say I want id to be a string that also matches a Regex, for example /^[A-Z]_[0-9]{4}$/.
interface MyComponentProps {
id: string
}
const MyComponent = ({ id }: MyComponentProps) => {
...
}
As far as I know Typescript doesn't allow you to use a Regex as a type. One of the solutions I found was declaring the type as something like
id: `${string}_${number}`, but that's not specific enough.
Is there any other way I could show some kind of warning if it does not match? Maybe using a linter or some other tool?
You can do something like this:
import * as yup from 'yup';
interface MyComponentProps {
id: string;
}
const idSchema = yup.string().matches(/^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$/);
const MyComponent = ({ id }: MyComponentProps) => {
try {
idSchema.validateSync(id)
} catch (err) {
throw new Error(`Invalid id: ${id}`);
}
// ...
}
Yup is a JavaScript object schema validation library. It allows you to define a schema for an object, which describes the structure and types of the properties of the object, and then validate an object against that schema. It provides a simple, expressive way to validate objects, and it can also be used to ensure that the data you receive from user input or external sources meets certain constraints.
In the code above, I've imported yup library and defined idSchema as a yup string schema that matches the given regex. And then I've used the validateSync method of yup to validate the id prop of the component against the schema. If the validation fails it throws an error.
Note that this approach will only give you a warning at runtime, but it will not provide you with compile-time type checking.

React: Cannot update object property in state in function component

Problem I'm trying to solve: Form validation for an app built in React that takes in user input and generates a CV. This is for learning purposes and part of The Odin Project curriculum.
How I'm trying to solve form validation: I have my project organized with a large formData object set as state in App.js and I'm sharing that state to all child components via useContext.
For validation, I have given each piece of CV data 2 properties in my formData object. Example below:
{firstname: '', firstNameValid: true}
I am trying to write a function (see attached code) that sets the validity of each propertyValid and set it to false.
What I expect: When a field is invalid (like firstName), it sets firstNameValid: false when I run the invalidateField('firstName') function.
What happens instead: Logging the formData reveals that firstNameValid is still set to true.
What I have tried: As seen in the attatched code, I am trying to make a copy of my formData object, set only the value I want, and then just manually set it using setFormData(formCopy). However, when I log them together, I see that while formCopy looks like what I want it to be, the formData in state still has not changed. I am updating state just fine from my grandchildren components, but I'm unable to do it here and I don't understand why.
//my object declared in state
const [formData, setFormData] = React.useState({
firstName: '',
lastName: '',
email: '',
firstNameValid: true,
lastNameValid: true,
emailValid: true
//a lot more than this but you get the idea
});
//function in question that is not working
function invalidateField(string){
//takes the name of the key as a string
let key = `${string}Valid`;
let value = false;
let formCopy = {...formData};
formCopy[key] = value;
setFormData(formCopy);
console.log(formCopy, formData);
}
//function I'm writing to check validity of input fields
//called by button click elsewhere in code
function formIsValid(formData){
let validity = true;
if(formData.firstName.length < 1){ //firstName = '' (field is empty)
console.log('your first name is too short');
invalidateField('firstName');
validity = false;
}
return validity;
}
//context then passed to child components in the return statement.
The setFormData method is an async function. It takes a while until the state is updated. You console.log right after calling setFormData, thats why it looks as if your setState didnt work properly when it just needs a little more time to complete.
Above your invalidateField function you could write an useEffect to print out when your state has changed:
import { useEffect } from "react";
useEffect(() => {
console.log(formData);
}, [formData]);
This will execute the console.log as soon as formData has changed.

Typescript and react conditional render

I am new to typescript and not an expert in FE development. I've encountered issue that seems pretty basic, but I failed to found any solution. Maybe I just don't know how to google it properly.
In react component I have a button, that is disabled on some condition, which triggers a component's function:
import React, {Component} from 'react';
type DraftCompany = {
id: null
name: string,
};
type Company = Omit<DraftCompany, 'id'> & {
id: number;
};
type Props = {
company: Company | DraftCompany,
onDeleteCompany: (companyId: number) => void,
}
class CompanyRow extends Component <Props> {
handleDeleteCompany = () => {
this.props.onDeleteCompany(this.props.company.id);
};
render = () => {
return (
<div>
<div>{this.props.company.name}</div>
<div>
<button disabled={this.props.company.id === null} onClick={this.handleDeleteCompany}/>
</div>
</div>
)
}
}
export default CompanyRow;
I am getting typescript error on calling this.props.onDeleteCompany(this.props.company.id); that says that there is a chance I will pass null as a parameter. I fully understand why typescript gives me this error, the question is: what would be the best way to deal with this error?
I have found 3 ways:
1) Add 'if' guard
handleDeleteCompany = () => {
if (this.props.company.id) {
this.props.onDeleteCompany(this.props.company.id);
}
};
It works, but I don't like the idea of adding such guards into every function, if someone removes disabled logic, I want to receive console error telling me about it immediately, not to have it be silently swallowed. In my project I have a lot of such code that relies on render, I doubt it is a best practice to add such checks everywhere. Maybe I am wrong.
2) Apply as to field operator:
handleDeleteCompany = () => {
this.props.onDeleteCompany(this.props.company.id as number);
};
It works, but looks kinda hacky.
3) Apply as operator to whole object and pass it to function:
<button disabled={this.props.company.id === null}
onClick={() => this.handleDeleteCompany(this.props.company as Company)}/>
handleDeleteCompany = (company: Company) => {
this.props.onDeleteCompany(company.id as number);
};
It works, but it looks like I am unnecessary passing the value I could have grabbed in function itself from props. I am not sure it is best practice to do such things.
I am sure there should be some pure typescript solution like defining Props type as a union or using conditional types with some combination of any and never. But I haven't figured it out .
Here is a playground:
playground
You can force the compile to assume a value is never null or undefined with the ! operator:
handleDeleteCompany = () => {
this.props.onDeleteCompany(this.props.company.id!);
};
I think based on your requirement
if someone removes disabled logic, I want to receive console error telling me about it immediately
There is a very simple solution that makes perfect sense, simply change your onDeleteCompany type from (companyId: number) => void to (companyId: number | null) => void, then TypeScript will be happy.
It also semantically make sense to you as you want the runtime report this error when companyId is null. Then you should allow companyId with null to be passed in as parameter.

React/Redux: is it okay to access action payload in mapDispatchToProps?

I've got this mapDispatchToProps function for a MediaUpload component. Upon adding a file, the onChange handler is triggered. The handler dispatches two actions: first it creates new media entries for files and returns an array of media objects. Then it updates the form data in the state with an array of media ids.
My question is: is it okay to read the action data in this position or do we preferably write to state via a reducer first?
const mapDispatchToProps = (dispatch, { form, name, multiple }) => ({
onChange: files => {
if (isEmpty(files)) return;
return dispatch(createMedia(files)).then(
media => {
// Get created media ids from action payload. Is this correct?
const mediaIds = media.map(item => item.payload.id);
return dispatch(updateFormData({
form,
fields: [ {
name: name,
value: multiple ? mediaIds : mediaIds[0]
} ]
}));
}
);
}
});
I'm a Redux maintainer. Seems fine to me, assuming that createMedia() is itself a thunk action creator that returns a promise.
That said, I also recommend moving that function definition to be standalone, and using the "object shorthand" form of mapDispatch rather than the function form.

React-Redux: Passing arguments from handler to action creator

I have scoured the web and various sources; none seem to apply to my question. The closest might be this (which doesn't have an answer):
React + Redux function call
So: I am attempting to pass arguments along to one of my action creator fields, a function called update which will determine if the blurred row had a value changed, and if so it will call my api to update. The arguments I wish to pass are the event (which contains the row I need as target.ParentElement) and an integer that represents the index of the row in my state's projects property.
Action creator in my redux store:
export const actionCreators = {
update: (e: React.FocusEvent<HTMLInputElement> | undefined, i: number): AppThunkAction<KnownAction> => (dispatch, getState) => {
let test = event;
// Will put logic and api call in here and dispatch the proper action type
}
}
And trying to call it like so:
// Inside a function rendering each row in my form
...
<input key={project.number} name={name} className='trackerCell' onBlur={(event) => { this.props.update(event, i) }} defaultValue={project.number}/>
Where i is the index value, passed to the rendering function.
This all compiles find, however when I execute and get into the update function, e and i are both undefined; event is defined though, and looks as I would expect e to look.
FWIW, the format I'm attempting here works elsewhere in my application:
requestProjects: (programNumber: number, programString: string): AppThunkAction<KnownAction> => (dispatch, getState) => {
when called by componentWillUpdate() properly receives a number and string that I am able to use in my logic.
Bonus: In all my action creator functions constructed this way, arguments has 3 objects in it: dispatch, getState and undefined. Why don't the arguments in the call signature show up? Am I thinking about these arguments differently?
And yes, I know I can just attach the index value to an attribute in my input and that will appear in the event object, but this seems hacky, and I want to actually understand what is going on here.
Thanks
UPDATE
In response to Will Cain's comment: The index variable, i, is passed to the row rendering function from it's parent, as such:
private renderProjectRow(project: ProjectTrackerState.Project, i: number) {
let cells: JSX.Element[] = [];
let someKey = project.number + '_started', name = project.number + '_number';
cells.push(<input key={project.number} name={name} className='trackerCell' onBlur={ this._handleBlur.bind(this) } defaultValue={project.number}/>);
// Rendering continues down here
It's a valid number type up to the event point (I can tell as I debug in the browser).
The event variable in the update function comes from.. I don't know where? That's a mystery I would love to solve. Even though it is not a defined parameter to the update function, when I enter the update method, event is defined as such in the debugger:
Event {isTrusted: false, type: "react-blur", target: react, currentTarget: react, eventPhase: 2, …}
It is clearly the event that triggered the handler, but how it reaches the update function is beyond me.
Answering this in case anyone comes across this same issue (unlikely, but want to be helpful):
The reason my arguments e and i were undefined at runtime is because they were not referenced in the function execution. My guess (still looking for documentation to verify) is that typescript cleans up references to unused parameters. Makes sense from an optimization standpoint. Adding a read reference to e and i inside the update function solved my issue.
You can add "noUnusedParameters": true to your compilerOptions in your tsconfig file and this will throw a typescript error for all these parameters so you can catch when these cleanups would be done.

Categories