Submit a form with data using a custom React hook - javascript

I'm having trouble figuring this out. I want to create a hook that's called to submit a form using fetch.
This is what I have right now. The component holding the form:
const MyForm = (): ReactElement => {
const [status, data] = useSubmitForm('https://myurl-me/', someData);
return <>
<div className='Feedback-form'>
<div className='body'>
<form>
<input type='text' name='username' placeholder='name' required />
<input type='email' name='email' placeholder='email' required />
<button className='submit-feedback-button' type='button'>Send feedback</button>
</form>
</div>
</div>
</>
}
The custom hook:
import { useState, useEffect } from 'react';
const useSubmitForm = (url: string, data: URLSearchParams): [string, []] => {
const [status, setStatus] = useState<string>('idle');
const [responseData, setData] = useState<[]>([]);
useEffect(() => {
if (!url) return;
const fetchData = async () => {
setStatus('fetching');
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'text/html',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data
});
const data = await response.json();
setData(data);
setStatus('fetched');
};
fetchData();
}, [url]);
return [status, responseData];
};
export default useSubmitForm;
My problem is that I think this hook is being called right away. How do I make this hook and call it in such a way that it's only called when the form is submitted and all the data I need to send in the request body is there to be included?

You are correct, the effect runs once when the component mounts and since url is truthy, it skips the early return and invokes fetchData.
How do I make this hook and call it in such a way that it's only
called when the form is submitted and all the data I need to send in
the request body is there to be included?
You need to also return a function for the component to invoke and pass along the form field values. I think you've a couple basic options.
Convert the form fields to be controlled inputs and store the field state in the component and invoke a "fetch" function returned from the useSubmitForm hook.
Return an onSubmit handler from the useSubmitForm to attach to your form element. The onSubmit handler would need to know what fields to access from the onSubmit event though, so passing an array of field names to the hook (i.e. a "config") makes sense.
Solution 1 - Use controlled inputs and returned fetch function
Unwrap the fetchData function from the useEffect hook and add a form field data parameter to it. Since fetch and response.json() can both throw errors/rejections you should surround this block in a try/catch. Return the custom fetchData function for the form to invoke.
useSubmitForm
const useSubmitForm = (
url: string,
data: URLSearchParams
): [function, string, []] => {
const [status, setStatus] = useState<string>("idle");
const [responseData, setData] = useState<[]>([]);
const fetchData = async (formData) => {
setStatus("fetching");
try {
const response = await fetch(url, {
method: "POST",
headers: {
Accept: "text/html",
"Content-Type": "application/x-www-form-urlencoded"
},
body: JSON.stringify(formData)
});
const data = await response.json();
setData(data);
setStatus("fetched");
} catch (err) {
setData(err);
setStatus("failed");
}
};
return [fetchData, status, responseData];
};
MyForm
const MyForm = (): ReactElement => {
const [fields, setFields] = useState({ // <-- create field state
email: '',
username: '',
});
const [fetchData, status, data] = useSubmitForm(
"https://myurl-me/",
someData
);
useEffect(() => {
// handle successful/failed fetch status and data/error
}, [status, data]);
const changeHandler = (e) => {
const { name, value } = e.target;
setFields((fields) => ({
...fields,
[name]: value
}));
};
const submitHandler = (e) => {
e.preventDefault();
fetchData(fields); // <-- invoke hook fetchData function
};
return (
<div className="Feedback-form">
<div className="body">
<form onSubmit={submitHandler}> // <-- attach submit handler
<input
type="text"
name="username"
placeholder="name"
onChange={changeHandler} // <-- attach change handler
value={fields.username} // <-- pass state
/>
<input
type="email"
name="email"
placeholder="email"
onChange={changeHandler} // <-- attach change handler
value={fields.email} // <-- attach state
/>
<button className="submit-feedback-button" type="submit">
Send feedback
</button>
</form>
</div>
</div>
);
};
Solution 2 - Return an onSubmit handler and pass an array of fields to the useSubmitForm
useSubmitForm
const useSubmitForm = (
url: string,
data: URLSearchParams,
fields: string[],
): [function, string, []] => {
const [status, setStatus] = useState<string>("idle");
const [responseData, setData] = useState<[]>([]);
const fetchData = async (formData) => {
setStatus("fetching");
try {
const response = await fetch(url, {
method: "POST",
headers: {
Accept: "text/html",
"Content-Type": "application/x-www-form-urlencoded"
},
body: JSON.stringify(formData)
});
const data = await response.json();
setData(data);
setStatus("fetched");
} catch (err) {
setData(err);
setStatus("failed");
}
};
const onSubmit = e => {
e.preventDefault();
const formData = fields.reduce((formData, field) => ({
...formData,
[field]: e.target[field].value,
}), {});
fetchData(formData);
}
return [onSubmit, status, responseData];
};
MyForm
const MyForm = (): ReactElement => {
const [onSubmit, status, data] = useSubmitForm(
"https://myurl-me/",
someData,
['email', 'username'] // <-- pass field array
);
useEffect(() => {
// handle successful/failed fetch status and data/error
}, [status, data]);
return (
<div className="Feedback-form">
<div className="body">
<form onSubmit={onSubmit}> // <-- attach submit handler
<input
type="text"
name="username"
placeholder="name"
/>
<input
type="email"
name="email"
placeholder="email"
/>
<button className="submit-feedback-button" type="submit">
Send feedback
</button>
</form>
</div>
</div>
);
};
In my opinion the second solution is the cleaner solution and requires less on consuming components to use.

Related

Sending form data from react client side to node.js server side

I have a login form at the client side (react) that I try to submit and pass the credentials to the login function at the server side (node.js)
when I use postman to send raw json object with the user name and password it works fine, but when I sent it through the client side the req.body contains only this: [[Prototype]]:
Object
what am I doing wrong here?
here is the code of the component that contains the form:
import React from 'react';
import '../signIn/signIn.component.css'
import { Link } from "react-router-dom";
import { useState, useEffect } from "react";
export default function SignIn() {
const [UserName, setUsername] = useState(null);
const [PassWord, setPassWord] = useState(null);
const [FormData, setFormData] = useState({});
useEffect(() => {
setFormData({ UserName: UserName, PassWord: PassWord });
}, []);
const submitFormSignIn = () => {
const testURL = "http://localhost:3100/login";
const myInit = {
method: "POST",
mode: 'no-cors',
body: JSON.stringify(FormData),
headers: {
'Content-Type': 'application/json'
},
};
const myRequest = new Request(testURL, myInit);
fetch(myRequest).then(function (response) {
return response;
}).then(function (response) {
console.log(response);
}).catch(function (e) {
console.log(e);
});
}
return (
<React.Fragment>
<form onSubmit={(e) => { submitFormSignIn(); e.preventDefault(); }}>
<div className="signIn-form-container">
<h1 className="welcome-header">Welcome</h1>
<div className="userName-form-container">
<input className="input-user-name" type="text" name="userName" placeholder='User name'
//should start with an alphabet so. All other characters can be alphabets, numbers or an underscore so.
required
pattern="^[A-Za-z][A-Za-z0-9_]{7,29}$"
minLength={"6"}
maxLength={"20"}
onChange={(e) => setUsername(e.target.value)}
></input>
</div>
<div className="password-form-container">
<input className="input-password" type="password" name="passWord" required
//Minimum eight characters, at least one uppercase letter, one lowercase letter and one number:
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
autoComplete="on"
minLength={"9"}
maxLength={"20"}
placeholder='Password'
onChange={(e) => setPassWord(e.target.value)}
></input>
</div>
<div className="forgot-remember-container">
<Link className="userName-forgot-link" to="/userNameRecovery">Forgot user name?</Link>
<Link className="password-forgot-link" to="/passwordRecovery">Forgot password?</Link>
</div>
<div className="form-submit-btn-container">
<button className="form-submit-btn">Sign in</button>
</div>
<div className="sign-up-container">
<a>Don't have an account?</a>
<Link className="signUp-link" to="/register">Sign up</Link>
</div>
<hr></hr>
</div>
</form>
</React.Fragment>
);
}
Your useEffect is fired only once - after initial render, because it's dependency array is empty. It means, you don't set for formData state with proper data.
I see two solutions:
Either fill the dependency array with UserName and PassWord states:
useEffect(() => {
setFormData({ UserName: UserName, PassWord: PassWord });
}, [UserName, PassWord]);
Or - and I would recommend this - easily create your body Object directly from UserName and PassWord states to :
body: JSON.stringify({UserName, PassWord}),
Small underline notice: states are variables, so their name should be camelCase, with lowercase at the beginning. Variables with UpperCase are intended to be React Components.
useEffect in this case absolutely unnecessary, so you have both and submit handler and useEffect that actually make you app rerender several extra times through setState, so Id build this something like that
import React from 'react';
import '../signIn/signIn.component.css'
import { Link } from "react-router-dom";
import { useState } from "react";
export default function SignIn() {
const [username, setUsername] = useState(null);
const [password, setPassword] = useState(null);
const submitFormSignIn = () => {
const testURL = "http://localhost:3100/login";
const myInit = {
method: "POST",
mode: 'no-cors',
body: JSON.stringify({ username, password }),
headers: {
'Content-Type': 'application/json'
},
};
const myRequest = new Request(testURL, myInit);
fetch(myRequest).then(function (response) {
return response;
}).then(function (response) {
console.log(response);
}).catch(function (e) {
console.log(e);
});
}
return (
<React.Fragment>
//same jsx
</React.Fragment>
);
}

unable to fetch a get request and print the output using fetch api

I am using react to get the data from an API using fetch API and print it but i was unable to retrieve data from after doing a fetch request. Here is the codelink. The input should be CC(C)(C)Br and the output is success message
import React, { useState } from "react";
import TextField from "#mui/material/TextField";
import Button from "#mui/material/Button";
export default function App() {
const [solutestate, setSolutestate] = useState("");
const [fetchData, setFetchData] = useState("");
console.log(solutestate);
console.log(fetchData);
let params = new URLSearchParams({
solute: solutestate
});
const onSubmit = (e) => {
fetch(
`https://fastapi-n7b7u.app/predict_two?${params}`,
{
method: "GET"
}
)
.then((res) => res.json())
.then((result) => {
setFetchData(result);
});
};
return (
<div className="App">
<>
<form noValidate onSubmit={onSubmit}>
<div>
Input: CC(C)(C)Br
<TextField
label="Solute"
variant="outlined"
onChange={(e) => setSolutestate(e.target.value)}
/>
<Button variant="contained">Submit</Button>
<TextField label="Result" variant="outlined" value={fetchData} />
</div>
</form>
</>
</div>
);
}
Couple of issues here.
Your submit button is not of type submit, so submit method is never called.
You will also want to put a preventDefault() on your submit handler, as the default will reload the page.
so changes are->
<Button type="submit" variant="contained">Submit</Button>
and
const onSubmit = (e) => {
e.preventDefault();
fetch(.....
Updated Sandbox
ps. This is not specifically about React, this is how forms work in
HTML.
const onSubmit = (e) => {
e.preventDefault()
let params = new URLSearchParams({
solute: solutestate
});
fetch(
`https://fastapi-ihub-n7b7u.ondigitalocean.app/predict_two?${params}`,
{
method: "GET",
content-type : "application/json"
}
)
.then((res) => res.json())
.then((result) => {
setFetchData(result);
});
};

fetch api happens before I submit the form

I use react js as frontend framework and laravel as backend framework. I also use proxy. I have a function that hanlde submit of my form. Everytime I load my form page I got two responses on my console although I haven't hit the submit button yet. This is my component CreateDaerah. I use function component
import React, { useEffect, useState} from 'react';
const CreateDaerah = () => {
const [nama, setNama] = useState('');
const handleSubmit = (url = '', data = {}) => {
fetch(url, {
method: "POST",
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => console.log(response))
.catch(err => console.error(err));;
}
return(
<div className="card-create">
<form onSubmit={handleSubmit(`/api/provinsi`, nama)}>
<h1>Form Provinsi</h1>
<label htmlFor="nama">Nama</label>
<input type="text"
className="nama-provinsi"
name="nama"
placeholder="masukkan nama"
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
Also how can I get the nama value from the input? Should I use onChange attribute?
This is because you are calling handleSubmit immediately at each render rather than providing a function to be called at submit. Instead you should update it to something like:
import React, { useEffect, useState} from 'react';
const CreateDaerah = () => {
const [nama, setNama] = useState('');
const handleSubmit = (url = '', data = {}) => {
fetch(url, {
method: "POST",
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => console.log(response))
.catch(err => console.error(err));;
}
return(
<div className="card-create">
<form onSubmit={() => handleSubmit(`/api/provinsi`, nama)}>
<h1>Form Provinsi</h1>
<label htmlFor="nama">Nama</label>
<input type="text"
className="nama-provinsi"
name="nama"
placeholder="masukkan nama"
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
You're calling the handleSubmit function immediately on the render, so you either need to pass a reference to the function instead with a callback, or just pass the function reference without those arguments.
I probably wouldn't pass those arguments. I wouldn't even use a form element as elements don't need to be wrapped in it. I'd would declare the URL in the component and place the handleSubmit on the button. That way you don't have to deal with the form updating the page.
Place an onChange handler on your input which calls a function that updates the state, and have the value of that input be the result of the state.
Here's a shorter working example.
const { useState } = React;
function Example() {
const [ numa, setNuma ] = useState('');
const url = `/api/provinsi`;
function handleInput(e) {
setNuma(e.target.value);
}
function handleClick() {
console.log('Button clicked');
console.log(`Final state: ${numa}`);
// fetch(url) using numa state
}
return (
<div>
<input value={numa} onChange={handleInput} />
<button onClick={handleClick}>Click me</button>
</div>
);
};
// Render it
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to input an existing value into an input field in React for an edit form

I have an existing Task with a title and a description, and I want to navigate to an edit form. By default I want the existing title and description values to populate the inputs using React. The important piece of this code I'm asking about is value={task.title}. Please ignore how the data is being pulled in (I'm new to React and I'm experimenting). The onChange and onSubmit handles work correctly, but the error obviously indicates I'm doing it wrong and it does cause occasional bugs.
I've tried abstracting those values into some sort of formValues state as well, but no matter how the values are being input, if the value={howeverIDoIt} is being directly manipulated I get the error.
import React, { useEffect, useState } from 'react';
import { HEADERS, TODO_URL } from '../urls';
import { useHistory, useParams } from 'react-router-dom';
const TaskEdit = () => {
const [task, setTask] = useState({});
const { id } = useParams();
const history = useHistory();
useEffect(() => {
fetch(`${TODO_URL}/api/tasks/${id}/`, {headers: HEADERS})
.then(response => response.json())
.then(responseJson => {
setTask(responseJson);
});
}, []);
const handleChange = (e) => {
setTask(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault();
const body = {
'title': e.target.form[0].value,
'description': e.target.form[1].value
}
fetch(
`${TODO_URL}/api/tasks/${id}/edit/`,
{
headers: HEADERS,
method: 'PUT',
body: JSON.stringify(body)
}
).then(res => res).catch(err => err);
history.push(`/task/${id}`)
}
return (
<form>
<div>
<label>Title</label>
<input type="text" onChange={handleChange} value={task.title} />
</div>
<div>
<label>Description</label>
<textarea onChange={handleChange} value={task.description}></textarea>
</div>
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
export default TaskEdit;
I have tried putting in a default value for useState like so: useState({title: 'title', description: 'description'}) but that doesn't prevent the error, nor does adding this edit form into the Task.js component, where task is most definitely defined.
You have:
<input type="text" onChange={handleChange} value={task.title} />
Your handleChange method is:
const handleChange = (e) => {
setTask(e.target.value)
}
When your onChange fires, your task state will be set to a String (the value of <input />)
So when you are referencing task.title after your onChange fires, it will be undefined. The same is true for task.description.
Try this:
const handleTitleChange = (e) => {
setTask({...task, title: e.target.value})
}
const handleDescriptionChange = (e) => {
setTask({...task, description: e.target.value})
}
<input type="text" onChange={handleTitleChange} value={task.title} />
<textarea onChange={handleDescriptionChange} value={task.description} />
Alternatively, you could split up the task state to title and description, respectively.

How to have conditional defaultValues from useEffect in react-select within react-hook-form?

I'm working on a form using react-hook-form that contains a react-select CreatableSelect multiselect input. The multiselect is used for tags of a given post and it is conditional based on if the user selects to update the tags of an existing post.
My issue is that the defaultValue for the multiselect is not working when a user selects an existing post that contains tags.
The overall flow is: User selects existing post (in PublicShareNetworkSelect in my example) > onChange function changes the post ID stored in hook (selectedNetwork in my example) > change in selectedNetwork fires getNetworkData function that sets the tags variable (networkTags) used as the multiselect defaultValue
Also the getTags() function is used to populate the options in the multiselect.
I believe that the issue as something to do with getting the data from the APIs because I tried to create a minimum reproducible example, but it works exactly how I want it to without the axios calls. However, when I console.log the allTags and networkTags in my full example, there are matching objects in the arrays (the matches should be the defaultValue).
Code example: Main/Parent form component
import React, { useState, useEffect } from "react";
import axios from "axios";
import Form from "react-bootstrap/Form";
import { useForm, Controller } from "react-hook-form";
import CreatableSelect from "react-select/creatable";
import Button from "react-bootstrap/Button";
import PublicShareNetworkSelect from "./publicShareNetworkSelect";
function PublicShareForm(props) {
const {
register,
handleSubmit,
reset,
control,
errors,
watch,
onChange,
} = useForm();
const [loading, setLoading] = useState(false);
const [selectedNetwork, setSelectedNetwork] = useState([]);
const [allTags, setAllTags] = useState();
const [networkTags, setNetworkTags] = useState([]);
//Create axios instance
const axiosSharedNetwork = axios.create();
async function getTags() {
const getAllTagsApi = {
url: "/public-share/get-all-tags",
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
method: "GET",
};
await axiosSharedNetwork(getAllTagsApi)
.then((response) => {
const resData = response.data;
const tags = resData.map((tag, index) => ({
key: index,
value: tag.tag_id,
label: tag.name,
}));
setAllTags(tags);
setLoading(false);
})
.catch((error) => {
console.log(error.response);
});
}
async function getNetworkData(networkId) {
const getNetworkDataApi = {
url: "/public-share/get-network/" + networkId,
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
method: "GET",
};
const getNetworkTagsApi = {
url: "/public-share/get-network-tags/" + networkId,
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
method: "GET",
};
await axiosSharedNetwork(getNetworkDataApi)
.then(async (response) => {
const resData = response.data;
//Set some variables (i.e. title, description)
await axiosSharedNetwork(getNetworkTagsApi)
.then(async (response) => {
const tagResData = response.data;
const tags = tagResData.map((tag, index) => ({
key: index,
value: tag.tag_id,
label: tag.name,
}));
setNetworkTags(tags);
setLoading(false);
})
.catch((error) => {
console.log(error.response);
});
})
.catch((error) => {
console.log(error.response);
});
}
useEffect(() => {
getTags();
getNetworkData(selectedNetwork);
reset({ tags: selectedNetwork });
}, [reset]);
async function onSubmit(data) {
//Handle submit stuff
}
console.log(allTags);
console.log(networkTags);
return (
<Form id="public-share-form" onSubmit={handleSubmit(onSubmit)}>
<Form.Group>
<Form.Label>Create New Version of Existing Shared Network?</Form.Label>
<PublicShareNetworkSelect
control={control}
onChange={onChange}
setSelectedNetwork={setSelectedNetwork}
/>
<Form.Label>Tags</Form.Label>
<Controller
name="tags"
defaultValue={networkTags}
control={control}
render={({ onChange }) => (
<CreatableSelect
isMulti
placeholder={"Select existing or create new..."}
onChange={(e) => onChange(e)}
options={allTags}
defaultValue={networkTags}
classNamePrefix="select"
/>
)}
/>
</Form.Group>
<Button variant="secondary" onClick={props.handleClose}>
Cancel
</Button>
<Button variant="primary" type="submit">
Share
</Button>
</Form>
);
}
export default PublicShareForm;
PublicShareNetworkSelect - the select component that triggers the function to set the existing post id (selectedNetwork):
import React, { useState, useEffect } from "react";
import axios from "axios";
import { Controller } from "react-hook-form";
import Select from "react-select";
function PublicShareNetworkSelect(props) {
const [loading, setLoading] = useState(false);
const [networks, setNetworks] = useState([]);
//Create axios instance
const axiosNetworks = axios.create();
// Add a request interceptor
axiosNetworks.interceptors.request.use(
function (config) {
// Do something before request is sent
setLoading(true);
return config;
},
function (error) {
// Do something with request error
setLoading(false);
return Promise.reject(error);
}
);
// Add a response interceptor
axiosNetworks.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
setLoading(true);
return response;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
setLoading(false);
return Promise.reject(error);
}
);
async function getNetworks() {
const getNetworksApi = {
url: "public-share/get-user-networks/" + props.username,
method: "GET",
};
await axiosNetworks(getNetworksApi)
.then(async (response) => {
setNetworks(
response.data.map((network, index) => ({
key: index,
value: network.network_id,
label: network.title,
}))
);
setLoading(false);
})
.catch((error) => {
console.log(error.response);
});
}
useEffect(() => {
getNetworks();
}, []);
function handleChange(data) {
console.log(data);
if (data) {
props.setSelectedNetwork(data.value);
props.getNetworkData(data.value);
} else {
props.setNetworkTitle("");
props.setNetworkDesc("");
}
}
if (!loading) {
if (networks.length === 0) {
return (
<React.Fragment>
<br />
<p className="font-italic text-muted">
You haven't created any public networks yet.
</p>
</React.Fragment>
);
} else {
return (
<Controller
name="tags"
defaultValue={[]}
control={control}
render={(props) => (
<CreatableSelect
isMulti
placeholder={"Select existing or create new..."}
onChange={(e) => onChange(e)}
// defaultValue={networkTags}
options={allTags}
classNamePrefix="select"
{...props}
/>
)}
/>
);
}
} else {
return <React.Fragment>Loading...</React.Fragment>;
}
}
export default PublicShareNetworkSelect;
Edit 1: console.log output for allTags (options) and networkTags (defaultValue)
The problem is, defaultValue is cached at the first render. The same applies to defaultValues property passed to useForm.
Important: defaultValues is cached at the first render within the custom hook. If you want to reset the defaultValues, you should use the reset api.
As quote from the docs suggests - you have to use reset. I've modified your example accordingly. Take a look here. As you can see I'm asynchronously resetting the form and it works.
Also, pay attention to render prop of the Controller - I'm passing down all props given, not only onChange. It's so because there are other important thingies in here (like value). By wrapping your component in Controller you have to provide onChange and value pair at least.
If you want to read more about reset take a look here.

Categories

Resources