Here is modern dynamic solution works by reusing Input component with React Hooks depending on json file.
Here is how it looks:

The benefits of using such paradigm: the input component (having its own hook state) may be reused in any other app part without changing any line of the code.
The drawback it's much more complicate.
here is simplified json (to build Components basing on):
{
"fields": [
{
"id": "titleDescription",
"label": "Description",
"template": [
{
"input": {
"required": "true",
"type": "text",
"disabled": "false",
"name": "Item Description",
"value": "",
"defaultValue": "a default description",
"placeholder": "write your initail description",
"pattern": "[A-Za-z]{3}"
}
}
]
},
{
"id": "requestedDate",
"label": "Requested Date",
"template": [
{
"input": {
"type": "date",
"name": "Item Description",
"value": "10-14-2007"
}
}
]
},
{
"id": "tieLine",
"label": "Tie Line #",
"template": [
{
"select": {
"required": true,
"styles": ""
},
"options": [
"TL625B",
"TL626B-$selected",
"TL627B",
"TL628B"
]
}
]
}
]
}
stateless Input component with Hooks, which may read different input types such as: text, number, date, password and some others.
import React, { forwardRef } from 'react';
import useInputState from '../Hooks/InputStateHolder';
const Input = ({ parsedConfig, className }, ref) => {
const inputState = useInputState(parsedConfig);
return (
<input
//the reference to return to parent
ref={ref}
//we pass through the input attributes and rewrite the boolean attrs
{...inputState.config.attrs}
required={inputState.parseAttributeValue(inputState.config, 'required')}
disabled={inputState.parseAttributeValue(inputState.config, 'disabled')}
className={`m-1 p-1 border bd-light rounded custom-height ${className}`}
onChange={inputState.onChange}
/>
)
};
//we connect this separated component to passing ref
export default forwardRef(Input)
Hook holder InputStateHolder.js file
import { useState } from 'react';
const useInputState = (initialValue) => {
//it stores read the json, proccess it,
//applies modifies and stores input values
const [config, setInputConfig] = useState({
isLoaded: false,
attrs: { ...initialValue }
});
//mutating and storing input values
function changeValue(e) {
const updatedConfig = { ...config };
updatedConfig.attrs.value = e.target.value;
setInputConfig({ ...config })
}
// to apply form configs to input element
//only one time at the first load
function checkTheFirstLoad() {
const updatedConfig = { ...config };
if (config.attrs.value.length === 0) {
updatedConfig.attrs.value = config.attrs.defaultValue;
//defaultValue is not allowed to pass as attribute in React
//so we apply its value depending on the conditions and remove it
delete updatedConfig.attrs.defaultValue;
updatedConfig.isLoaded = true;
setInputConfig(updatedConfig);
}
}
//parsing boolean input attributs such as required or disabled
function parseAttributeValue(newState, attribute) {
return typeof newState.attrs[attribute] === 'string' && newState.attrs[attribute] === 'true'
? true : false
}
!config.isLoaded && checkTheFirstLoad();
//returning the hook storage
return {
config,
onChange: changeValue,
parseAttributeValue
}
}
export default useInputState;
And the parent FormFields component (containing form and submit tags):
import React, { createElement } from "react";
import Input from '../UI/Input';
const FormField = ({ setConfig }) => {
//it receives the parsed json and check to not be empty
if (!!Object.keys(setConfig).length) {
const fieldsConfig = setConfig.fields;
//the array to get created elements in
const fieldsToGetBuilt = [];
// the array to store input refs for created elements
const inputRefs = [];
// the function to store new ref
const setRef = (ref) => inputRefs.push(ref);
fieldsConfig.map(field => {
switch (true) {
//here is we create children depending on the form configs
case (!!field.template[0].input): {
let classes = 'someStyle';
fieldsToGetBuilt.push(
createElement(Input, {
ref: setRef,
parsedConfig: field.template[0].input,
key: field.id,
className: classes
})
);
break
}
//default case needed to build warning div notifying the missed tag
default: {
let classes = 'someOther danger style';
let child = `<${Object.keys(field.template[0])[0]}/> not built`;
fieldsToGetBuilt.push(
createElement('div', {
key: field.id,
className: classes
}, child)
);
}
}
})
const onSubmitHandler = (e) => {
//every time we click on submit button
//we receive the inputs`es values in console
e.preventDefault();
inputRefs.map(e =>
console.log(e.value)
)
}
return (
<div className='m-2 d-flex flex-column'>
<form onSubmit={onSubmitHandler}>
<h5 className='text-center'>{setConfig.title}</h5>
<div className='d-flex flex-row justify-content-center align-items-center'>
{fieldsToGetBuilt.map(e => e)}
</div>
<input type="submit" onClick={onSubmitHandler} className='btn-info' />
</form>
</div >
)
}
// if in json there are no any fields to get built
else return <div>no Page has been built</div>
};
export default FormField;
The result is here
and what we see in the console after input fields are changed and submit button is clicked

PS in my another answer i implemented dymanic module upload basing on json