React repeated onInput handlers in components - javascript

I have a react component that looks like the one given below.
The form inputs are handled using the onInputChange function and form submit is handled by onFormSubmit
function RegisterForm() {
// formData stores all the register form inputs.
const [formData, setFormData] = useState(registerDefault);
const [errors, posting, postData] = useDataPoster();
function onInputChange(event: ChangeEvent<HTMLInputElement>) {
let update = { [event.target.name]: event.target.value };
setFormData(oldForm => Object.assign(oldForm, update));
}
function onFormSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const onSuccess: AxiosResponseHandler = response => {
setFormData(Object.assign(formData, response.data));
};
postData("/api/register", formData, onSuccess);
}
return (
<form onSubmit={onFormSubmit}>
<FormTextInput
name="full_name"
label="Name"
errors={errors.full_name}
onChange={onInputChange}
/>
<FormTextInput
name="email"
label="Email address"
type="email"
errors={errors.email}
onChange={onInputChange}
/>
<button type="submit" className="theme-btn submit" disabled={posting}>
{posting && <span className="fas fa-spin fa-circle-notch"></span>}
Create
</button>
</form>
);
}
My app has more than 50 similar forms and I wonder if I have to copy paste these two functions on all the other forms. onInputChange won't be changing a bit and the url is the only variable in onFormSubmit.
I am thinking of a class based approach with setFormData and postData as properties and the functions in question as class methods. But in that case, I have to bind the handlers with the class instance, so that handlers have a valid this instance.
Is there any other way to do this? How would you avoid the repetition of these two code blocks in all the form components?
Thanks

you could create a custom hook, something like this:
const [formState, setFormState] = useFormStateHandler({name: ''})
<input value={formState.name} onChange={event => setFormState(event, 'name')} />
where the definition looks like this:
export default function useFormStateHandler(initialState) {
const [state, setState] = useState(initialState)
const updater = (event, name) => {
setState({...state, [name]: event.target.value})
}
return [state, updater]
}

Create an HOC to inject input handlers to the form components with added params for url.
function RegisterForm(props) {
// specific function
const specific = () => {
const formData = props.formData; // use passed state values
// use form data
}
}
function withInputHandlers(Component, params) {
return function(props) {
// states
function onInputChange(...) {...}
function onFormSubmit(...) {
// use params.url when submitting
postData(params.url, formData, onSuccess);
}
// inject input handlers to component and state values
return (
<Component {...props} formData={formData} onChange={onInputChange} onSubmit={onFormSubmit} />
);
}
}
// Usage
const EnhancedRegisterForm = withInputHandlers(
RegisterForm,
{ url: 'register_url' } // params
);
const EnhancedSurveyForm = withInputHandlers(
Survey,
{ url: 'survey_url' } // params
)

This change may help you
function RegisterForm() {
// formData stores all the register form inputs.
const [formData, setFormData] = useState(registerDefault);
const [errors, posting, postData] = useDataPoster();
const onInputChange = name => event => {
let update = { [name]: event.target.value };
setFormData(oldForm => Object.assign(oldForm, update));
}
const onFormSubmit = url => event =>{
event.preventDefault();
const onSuccess: AxiosResponseHandler = response => {
setFormData(Object.assign(formData, response.data));
};
postData(url, formData, onSuccess);
}
return (
<form onSubmit={onFormSubmit("/api/register")}>
<FormTextInput
name="full_name"
label="Name"
errors={errors.full_name}
onChange={onInputChange("full_name")}
/>
<FormTextInput
name="email"
label="Email address"
type="email"
errors={errors.email}
onChange={onInputChange("email")}
/>
<button type="submit" className="theme-btn submit" disabled={posting}>
{posting && <span className="fas fa-spin fa-circle-notch"></span>}
Create
</button>
</form>
);
}

Related

Why is action not updating my component in this todo list?

From the extent of my knowledge, action in mobx is supposed to cause the observer to rerender, right? However, even though I'm invoking action on the handleSubmit method in my AddTask component, it does not rerender the observer(TaskList). Do I have to wrap AddTask in an observable as well? But when I tried that, it didn't render any of the data at all. I'm genuinely perplexed and have tried so many different things for hours. Please help.
AddTask:
export default function AddTask() {
const [task, setTask] = useState('');
const handleSubmit = action(async (event: any) => {
event.preventDefault();
try {
const response = await axios.post('http://localhost:5000/test', { task });
} catch (error: Error | any) {
console.log(error);
}
});
const onChange = (e: any) => {
const value = e.target.value;
if (value === null || value === undefined || value === '') {
return;
}
setTask(value);
};
return (
<div className="task">
<form onSubmit={handleSubmit}>
<input type="text" name="task" value={task} onChange={onChange}></input>
<br></br>
<input type="submit" name="submit" value="Submit" />
</form>
</div>
);
}
TaskList:
const TaskList = () => {
const [update, setUpdate] = useState<string>('');
useEffect(() => {
TaskStore.fetchTasks();
}, []);
const onChangeValue = (e: any) => {
setUpdate(e.target.value);
};
return (
<div>
<p>
update input <input onChange={onChangeValue} value={update} />
</p>
{TaskStore.tasks.map((value: any, key) => {
console.log(value);
return (
<div key={key}>
<p>
{value.task}
<DeleteTask value={value} taskList={TaskStore} />
<UpdateTask value={update} current={value} taskList={TaskStore} />
</p>
</div>
);
})}
</div>
);
};
export default observer(TaskList);
taskStore:
interface Task {
task: string;
}
class TaskStore {
constructor() {
makeAutoObservable(this);
}
tasks = [] as Task[];
#action fetchTasks = async () => {
try {
const response: any = await axios.get('http://localhost:5000/test');
this.tasks.push(...response.data.recordset);
} catch (error) {
console.error(error);
}
};
}
export default new TaskStore();
As far as I can see you are not doing anything in your submit action to handle new task.
You are making request to create new task, but then nothing, you don't do anything to actually add this task to your store on the client nor make store refetch tasks.
const handleSubmit = action(async (event: any) => {
event.preventDefault();
try {
// Request to create new task, all good
const response = await axios.post('http://localhost:5000/test', { task
// Now you need to do something
// 1) Either just add task to the store manually
// Add some action to the store to handle just one task
TaskStore.addTask(response.data)
// 2) Or you can do lazy thing and just make store refetch everything :)
// Don't forget to adjust this action to clear old tasks first
TaskStore.fetchTasks()
});
} catch (error: Error | any) {
console.log(error);
}
});
P.S. your action decorators are a bit useless, because you cannot use actions like that for async function, for more info you can read my other answer here MobX: Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed

How to use react/tanstack query useMutation in my component

I'm currently converting the logic in my mern (with typescript) project to use React/Tanstack query to learn this tool better.
I want to use useMutation to handle the post request logic from the details inputted in the form, in this login component but can't figure out how to do this. Any tips would be appreciated thanks. Below is the code from my login component
const Login = () => {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [state, setState] = useContext(UserContext);
const handleSubmit = async (e: { preventDefault: () => void }) => {
e.preventDefault();
let response;
const { data: loginData } = await axios.post("http://localhost:5001/auth/login", {
email,
password,
});
response = loginData;
if (response.errors.length) {
return setErrorMsg(response.errors[0].msg);
}
setState({
data: {
id: response.data.user.id,
email: response.data.user.email,
stripeCustomerId: response.data.user.stripeCustomerId,
},
loading: false,
error: null,
});
localStorage.setItem("token", response.data.token);
axios.defaults.headers.common["authorization"] = `Bearer ${response.data.token}`;
navigate("/dashboard");
};
return (
<div className="login-card">
<div>
<h3>Login</h3>
</div>
<form onSubmit={handleSubmit}>
<div className="login-card-mb">
<label>Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div className="login-card-mb">
<label>Password</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
{errorMsg && <p>{errorMsg}</p>}
<button type="submit">Submit</button>
</form>
</div>
);
};
After setting up your project to use React Query ( Check the docs if you have not). You want to extract your api call to a separate function that takes an object. This object will hold the values you would like to post.
const Login = (dataToPost) => {
let res = await axios.post('url', dataToPost)
return res.data
}
Now that you have that, you can import useMutation from React Query. Once imported you can now use the hook. UseQuery, useMutation both contain a data variable so no need to create state for the data returned from your endpoint. In this example, I'm deconstructing the data and loading state. But most importantly the mutate function. Which allows you to fire off your api call. We add our api call to the hook. I'm renaming the mutate function to doLogin. It's a habit
const {data,isLoading,mutate:doLogin} = useMutation(Login)
Finally we can just call mutate(objectWithValues) wherever you want in your code. The data will initially be null and isLoading will be true once called. To tie it all together. Your handleSubmit could look as follows
const handleSubmit = () => {
e.preventDefault();
doLogin({email,password})
}
You also have the option of running functions on a success or error of the mutation
const {data,isLoading,mutate: doLogin} =
useMutation(Login, {
onError: (err) => console.log("The error",err),
onSuccess:(someStuff)=>console.log("The data being returned",someStuff)
})

Dynamically creating and getting inputs

I need to dynamically add new input fields on button click as well as get the user input of those inputs in an array. This is what I have and Im not sure how to do the array. Also on the screen the components only update when I change the state. Not on button click.
This is what I have:
import React, { useState } from 'react'
const CreatePoll = () => {
const [formData, setFormData] = useState({
question: '',
options: ['hi', 'there']
});
const {
question,
options
} = formData;
const addOption = e => {
e.preventDefault();
options.push([''])
console.log(options.length);
}
const handleQuestionChange = (e) => setFormData({
...formData,
[e.target.name]: e.target.value
})
const handleOptionChange = e => setFormData({
...formData
// options: []
})
const handleSubmit = async e => {
e.preventDefault();
console.log(formData)
}
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter your question"
type="text"
onChange={handleQuestionChange}
name="question" />
{options.map(() => {
return (
<input
placeholder="option"
type="text"
onChange={handleOptionChange}
name="option" />
)
})}
<input type="button" value="Add new option" onClick={addOption} />
<input type="submit" value="Submit" />
</form>
)
}
export default CreatePoll
I tried when addOption button is clicked, I add to the options state an empty string. The length of the array updates but the components on the screen dont until I type in the input box and the state changes. Also I need to map the values of the input boxes to their respective place in the array. They should also be able to edit at any time. How is this done?
Several things are wrong here :
You don't set your state in addOption, don't modify direcly the state object, prefere to destructure array, modify it and set the state.
Your map function don't take any parameter, so it will be the same input names every time, use parameter and index to now which option to change in handleQuestionChange
Your addOption could be improved by using question property directly in you setFormData (it worked like you did it, but it seems to me more clean with this)
import React, { useState } from 'react';
const CreatePoll = () => {
const [formData, setFormData] = useState({
question: '',
options: ['hi', 'there'],
});
const {
question,
options,
} = formData;
const addOption = e => {
e.preventDefault();
const newOptions = [...options];
newOptions.push('');
setFormData({ ...formData, options: newOptions });
console.log(options.length);
};
const handleQuestionChange = e => {
setFormData({
...formData,
question: e.target.value,
});
};
const handleOptionChange = (e, index) => {
const newOptions = [...options];
newOptions[index] = e.target.value;
setFormData({
...formData,
options: newOptions,
});
};
const handleSubmit = async e => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter your question"
type="text"
onChange={handleQuestionChange}
name="question"
/>
{options.map((opt, index) => (
<input
value={opt}
key={`option_${index}`}
placeholder="option"
type="text"
onChange={e => handleOptionChange(e, index)}
name={opt}
/>
))}
<input type="button" value="Add new option" onClick={addOption} />
<input type="submit" value="Submit" />
</form>
);
};
export default CreatePoll;
to add new options on button click you need to change this function:
const addOption = e => {
e.preventDefault();
options.push([''])
console.log(options.length);
}
to be
const addOption = e => {
e.preventDefault();
const newOptions = {...formData.options}
newOptions.concat([''])
setFormData({...formatData, options: newOptions)}
}

React Axios Input Undefined

I must post {input} data to http://localhost:4000/prediction with Axios. But {input} turns undefined.
I am using const instead of class Main extends component. onChange, it sets form data.
const Main = ({ value, suggestions, auth: { user } }) => {
const [formData, setFormData] = useState("");
const [messages, setMessages] = useState([]);
const { input } = formData;
const onChange = e => setFormData(e.target.value);
const onSubmit = event => {
event.preventDefault();
setMessages(prevMsgs => [...prevMsgs, formData]);
console.log({ input });
Axios post.
axios
.post(
`http://localhost:4000/prediction`,
{ input },
{ crossdomain: true }
)
.then(res => {
console.log(res.data);
//setMessages(prevMsgs => [...prevMsgs, formData]);
})
.catch(error => {
console.log(error.message);
});
};
Return (form) with onSubmit, onChange.
return (
<div className="true">
<br />
<form noValidate onSubmit={e => onSubmit(e)}>
<div className="input-group mb-3">
<input
name="input"
type="text"
className="form-control"
placeholder="Type text"
onChange={e => onChange(e)}
/>
)}
<div className="input-group-append">
<button className="btn btn-outline-secondary">Send</button>
</div>
</div>
</form>
</div>
);
};
As I have mentioned in the comment section formData is a string as I see which does not have a property called input what you try to destructure and that's why it is undefined always.
If you really need that format for axios then you can try change the structure of formData with useState as the following first:
const [formData, setFormData] = useState({input: null});
Then maybe you can try updating as:
const onChange = e => setFormData({input: e.target.value});
I hope that helps!

How to validate form using react hooks?

How to validate form using react hooks ?
I used class components and it worked great, but now decided to use functional components with hooks and don't know the best way to validate form.
My code:
const PhoneConfirmation = ({ onSmsCodeSubmit }) => {
const [smsCode, setSmsCode] = useState('');
const onFormSubmit = (e) => {
e.preventDefault();
if (smsCode.length !== 4) return;
onSmsCodeSubmit(smsCode);
}
const onValueChange = (e) => {
const smsCode = e.target.value;
if (smsCode.length > 4) return;
setSmsCode(smsCode);
};
return (
<form onSubmit={onFormSubmit}>
<input type="text" value={smsCode} onChange={onValueChange} />
<button type="submit">Submit</button>
</form>
)
};
It works but I don't think it's good idea to use handler function inside functional component, because it will be defined every time when the component is called.
Your code is fine, but you can slightly improve it because you don't need a controlled component.
Moreover, you can memorize the component so it won't make unnecessary render on onSmsCodeSubmit change due to its parent render.
const FORM_DATA = {
SMS: 'SMS'
}
const PhoneConfirmation = ({ onSmsCodeSubmit, ...props }) => {
const onSubmit = e => {
e.preventDefault();
const data = new FormData(e.target);
const currSmsCode = data.get(FORM_DATA.SMS);
onSmsCodeSubmit(currSmsCode);
};
return (
<form onSubmit={onSubmit} {...props}>
<input type="text" name={FORM_DATA.SMS} />
<button type="submit">Submit</button>
</form>
);
};
// Shallow comparison by default
export default React.memo(PhoneConfirmation)

Categories