React - optimal way to create and submit form data
In this article, we would like to show you an optimal way to create and submit form data in React.
The approach used in the below example is useful when we want to create a form with a big amount of fields avoiding adding change handlers or references to each one field separately. The solution works even form fields are nested in other components.
The main idea of the below approach is to use object property paths as field names that are used to generate data object on form
submit event or to bind fields for indicated form data.
Usage example
The main concept is to provide Form
component that binds/gets field values automatically what looks following way when we use it (all useful logic is composed inside Form
component):
const handleSubmit = (data) => {
// some code here...
};
return (
<div>
{/* Form component definition in the below code */}
{/* */}
<Form data={data} onSubmit={handleSubmit}>
<input type="text" name="user.username" />
<input type="password" name="user.password" />
<input type="text" name="report.title" />
<input type="text" name="report.goal" />
<button type="submit">Submit</button>
</Form>
</div>
);
Notes:
- go to this article to read more about
setProperty
function (setObjectProperty
),- go to this article to read more about
getProperty
function (getObjectProperty
).
Practical example
// ONLINE-RUNNER:browser;
// Note: Uncomment import lines in your project.
// import React from 'react';
// import ReactDOM from 'react-dom';
const getProperty = (object, path) => {
if (object == null) { // undefined or null
return object;
}
const parts = path.split('.');
for (let i = 0; i < parts.length; ++i) {
if (object == null) { // undefined or null
return undefined;
}
const key = parts[i];
object = object[key];
}
return object;
};
const setProperty = (object, path, value) => {
const parts = path.split('.');
const limit = parts.length - 1;
for (let i = 0; i < limit; ++i) {
const key = parts[i];
object = object[key] ?? (object[key] = {});
}
const key = parts[limit];
object[key] = value;
};
const iterateElements = (form, callback) => {
const elements = form.elements;
for (let i = 0; i < elements.length; ++i) {
const element = elements[i];
const tag = element.tagName;
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
callback(element);
}
}
};
const getData = (form) => {
const data = {};
iterateElements(form, element => {
setProperty(data, element.name, element.value);
});
return data;
};
const bindData = (form, data) => {
iterateElements(form, element => {
const value = getProperty(data, element.name);
element.value = String(value ?? '');
});
};
const Form = React.forwardRef(({data, children, onSubmit}, ref) => {
const formRef = ref ?? React.useRef();
React.useEffect(() => {
const form = formRef.current;
if (form) {
bindData(form, data ?? {});
}
}, [data]);
const handleSubmit = e => {
e.preventDefault();
if (onSubmit) {
onSubmit(getData(formRef.current));
}
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
{children}
</form>
);
});
// Usage example:
const areaStyle = { border: '1px solid gray' };
const App = () => {
const data = {
user: {
username: 'john',
password: 'Secret$$'
},
report: {
title: 'My raport title',
goal: 'My raport goal'
}
};
const handleSubmit = (data) => {
const json = JSON.stringify(data, null, 4);
console.clear();
console.log(json);
};
return (
<div>
<Form data={data} onSubmit={handleSubmit}>
<div style={areaStyle}>
<div>User:</div>
<div>
<label>Username: </label>
<input type="text" name="user.username" />
</div>
<div>
<label>Password: </label>
<input type="password" name="user.password" />
</div>
</div>
<div style={areaStyle}>
<div>Report:</div>
<div>
<label>Title: </label>
<input type="text" name="report.title" />
</div>
<div>
<label>Goal: </label>
<input type="text" name="report.goal" />
</div>
</div>
<button type="submit">Submit</button>
</Form>
</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);
It is possible to get form data
from outside of Form
component in the following way:
// ...
const App = () => {
const formRef = React.useRef();
// ...
// It is possible to get data from outside of the Form component when reference is ready:
// const data = getData(formRef.current);
// It is possible to bind data too:
// bindData(formRef.current, data);
return (
<div>
<Form ref={formRef} data={data} onSubmit={handleSubmit}>
{/* ... */}
</Form>
</div>
);
};
const root = document.querySelector('#root');
ReactDOM.render(<App />, root);