I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.
Related
I have three components First, Second and Third that need to render one after the other.
My App looks like this at the moment:
function App() {
return (
<First/>
)
}
So ideally, there's a form inside First that on submission (onSubmit probably) triggers rendering the Second component, essentially getting replaced in the DOM. The Second after some logic triggers rendering the Third component and also passes a value down to it. I'm not sure how to go on about it.
I tried using the useState hook to set a boolean state to render one of the first two components but I would need to render First, then somehow from within it change the set state in the parent which then checks the boolean and renders the second. Not sure how to do that. Something like below?
function App() {
const { isReady, setIsReady } = useState(false);
return (
isReady
? <First/> //inside this I need the state to change on form submit and propagate back up to the parent which checks the state value and renders the second?
: <Second/>
);
}
I'm mostly sure this isn't the right way to do it.
Also need to figure out how to pass the value onto another component at the time of rendering it and getting replaced in the DOM. So how does one render multiple components one after the other on interaction inside each? A button click for example?
Would greatly appreciate some guidance for this.
then somehow from within it change the set state in the parent which then checks the boolean and renders the second.
You're actually on the right track.
In React, when you're talking about UI changes, you have to manage some state.
So we got that part out of the way.
Now, what we can do in this case is manage said state in the parent component and pass functions to the children components as props in-order to allow them to control the relevant UI changes.
Example:
function App() {
const { state, setState } = useState({
isFirstVisible: true,
isSecondVisible: false,
isThirdVisible: false,
});
const onToggleSecondComponent = (status) => {
setState(prevState => ({
...prevState,
isSecondVisible: status
}))
}
const onToggleThirdComponent = (status) => {
setState(prevState => ({
...prevState,
isThirdVisible: status
}))
}
return (
{state.isFirstVisible && <First onToggleSecondComponent={onToggleSecondComponent} /> }
{state.isSecondVisible && <Second onToggleThirdComponent={onToggleThirdComponent} /> }
{state.isThirdVisible && <Third/> }
);
}
Then you can use the props in the child components.
Example usage:
function First({ onToggleSecondComponent }) {
return (
<form onSubmit={onToggleSecondComponent}
...
</form
)
}
Note that there are other ways to pass these arguments.
For example, you can have one function in the parent comp that handles them all, or you can just pass setState to the children and have them do the logic.
Either way, that's a solid way of achieving your desired outcome.
Seen as your saying there are stages, rather than having a state for each stage, just have a state for the current stage, you can then just increment the stage state to move onto the next form.
Below is a simple example, I've also used a useRef to handle parent / child state, basically just pass the state to the children and the children can update the state. On the final submit I'm just JSON.stringify the state for debug..
const FormContext = React.createContext();
const useForm = () => React.useContext(FormContext);
function FormStage1({state}) {
const [name, setName] = React.useState('');
state.name = name;
return <div>
Stage1:<br/>
name: <input value={name} onChange={e => setName(e.target.value)}/>
</div>
}
function FormStage2({state}) {
const [address, setAddress] = React.useState('');
state.address = address;
return <div>
Stage2:<br/>
address: <input value={address} onChange={e => setAddress(e.target.value)}/>
</div>
}
function FormStage3({state}) {
const [hobbies, setHobbies] = React.useState('');
state.hobbies = hobbies;
return <div>
Stage3:<br/>
hobbies: <input value={hobbies} onChange={e => setHobbies(e.target.value)}/>
</div>
}
function Form() {
const [stage, setStage] = React.useState(1);
const state = React.useRef({}).current;
let Stage;
if (stage === 1) Stage = FormStage1
else if (stage === 2) Stage = FormStage2
else if (stage === 3) Stage = FormStage3
else Stage = null;
return <form onSubmit={e => {
e.preventDefault();
setStage(s => s + 1);
}}>
{Stage
? <React.Fragment>
<Stage state={state}/>
<div>
<button>Submit</button>
</div>
</React.Fragment>
: <div>
{JSON.stringify(state)}
</div>
}
</form>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Form/>);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
I'm using Antd Input library, whenever I type in the start or in the middle of the word my cursor jumps to the end.
const handleOpenAnswer =( key, value )=>{
handleFieldChange({
settings: {
...settings,
[key]: value
}
})
}
return (
<Input
required
size='default'
placeholder='Label for Diference Open Answer Question'
value='value'
onChange={({ target: { value } }) => {
handleOpenAnswer('differenceOpenAnswerLabel', value)
}}
/>
The reason why your cursor always jumps to the end is because your parent component gets a new state and therefore re-renders its child components. So after every change you get a very new Input component. So you could either handle the value change within the component itself and then try to pass the changed value up to the parent component after the change OR (and I would really recommend that) you use something like React Hook Form or Formik to handle your forms. Dealing with forms on your own can be (especially for complex and nested forms) very hard and ends in render issues like you face now.
Example in React-Hook-Form:
import { FormProvider, useFormContext } = 'react-hook-form';
const Form = () => {
const methods = useForm();
const { getValues } = methods;
const onSubmit = async () => {
// whatever happens on submit
console.log(getValues()); // will print your collected values without the pain
}
return (
<FormProvider {...methods}>
<form onSubmit={(e) => handleSubmit(onSubmit)(e)>
{/* any components that you want */}
</form>
</FormProvider>
);
}
const YourChildComponent = () => {
const { register } = useFormContext();
return (
<Input
{...register(`settings[${yourSettingsField}]`)}
size='default'
placeholder='Label for Diference Open Answer Question'
/>
)
}
I am building a simple blog app and I am trying to update title of the blog But it is not updating, it is just showing the current state.
I have tried many times by changing the method of setting state but it is still showing that error.
App.js
function BlogDetail() {
const [blogName, setBlogName] = useState("");
axios.get("/api/blog_detail/70/").then(res => {
setBlogName(res.data[0].blog_name)
})
const handleChange = (e) => {
console.log(e.target.value)
setBlogName({
...blogName,
[e.target.name]: e.target.value
})
}
const saveBlog = (e) => {
// sending to API
console.log(blogName)
}
return (
<div>
<form>
{blogName}
<input type="text" name="blogName" value={blogName} onChange={e => handleChange} />
<button type="submit" onClick={e => saveBlog(e)}>Save</button>
<form>
</div>
)
}
And When I update on change instead of updating on submit
onChange=(e => setBlogName(e.target.value))
Then it is showing
A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined
I have tried many times but it is still not working.
input requires a string as a value, but you are trying to pass an object:
setBlogName({
...blogName,
[e.target.name]: e.target.value
})
instead pass a string:
setBlogName(e.target.value)
Also, you need to execute handleChange function and pass the event param.
onChange={e => handleChange(e)}
Edit:
Looked at it second time and it should be like this:
function BlogDetail() {
const [blogName, setBlogName] = useState("");
// without this you override state every time you press a key
useEffect(() => {
axios.get("/api/blog_detail/70/").then(res => {
setBlogName(res.data[0].blog_name)
})
}, [])
const handleChange = (e) => {
// just use value here
setBlogName(e.target.value)
}
const saveBlog = (e) => {
// sending to API
console.log(blogName)
}
return (
<div>
<form>
{blogName}
{ /* remember to run the function */ }
<input type="text" name="blogName" value={blogName} onChange={e => handleChange()} />
<button type="submit" onClick={e => saveBlog(e)}>Save</button>
<form>
</div>
)
}
Besides the problem that within handleChange you need to pass an an string value to setBlogName you also need to wrap your axios fetch call in a useEffect.
The problem is that everytime you trigger a rerender while calling setBlogName you are calling your API point again and set the value back to the fetched value.
You should prevent that by doing the following ->
useEffect(() => {
axios.get("/api/blog_detail/70/").then(res => {
setBlogName(res.data[0].blog_name)
}), [])
Don't forget to install { useEffect } from 'react'.
And well of course update handleChange ->
const handleChange = (e) => {
const newBlogPostName = e.target.value
console.log(newBlogPostName)
setBlogName(newBlogPostName)
}
you have not any action in this method. where is the update state?
const saveBlog = (e) => {
// sending to API
console.log(blogName)
}
and in this method you change the string to an object
const handleChange = (e) => {
console.log(e.target.value)
setBlogName({
...blogName,
[e.target.name]: e.target.value
})
}
so the problem is that your function updates your state to an object and then you want to display that object(not a string property of that object) in the DOM. its wrong because you cant display objects in the DOM in react. in this case, you even get an error because you cant use spread operator on strings. you cant do something like this: ...("test")
const handleChange = (e) => {
console.log(e.target.value)
//the state value will be an object. its wrong. you even get an error
because of using spread operator on a string
setBlogName({
...blogName //this is a string at the init time,
[e.target.name]: e.target.value
})
}
so whats the solution?
you should update your state to a string or use a string property of the object.
something like this:
const handleChange = (e) => {
console.log(e.target.value)
setBlogName("string")
}
return (<>{blogName}</>)
thats it.
So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.
App takes user options and creates an array objects randomly, and based on user options. (it's a gamer tag generator, writing to learn react.js). As is, App is a functional component and I use useState to store array of objects (gamertags) and the current selected options.
I use formik for my simple form. It takes two clicks to get a new item with updated options. I know why, options in state of App doesn't not update until it rerenders as the function for form submission is async. Therefore, all of my options are updated, after the first click, and are correct with the second because they were updated with the rerendering and after I needed them.
I know the solution is to use a useEffect hook, but despite reading over other posts and tuts, I don't understand how to apply it. It's my first instance of needing that hook and I'm still learning.
I wrote a simplified App to isolate the problem as much as possible and debug. https://codesandbox.io/s/morning-waterfall-impg3?file=/src/App.js
export default function App() {
const [itemInventory, setItemInventory] = useState([
{ options: "apples", timeStamp: 123412 },
{ options: "oranges", timeStamp: 123413 }
]);
const [options, setOptions] = useState("apples");
const addItem = (item) => {
setItemInventory([item, ...itemInventory]);
};
const createItem = () => {
return { options: options, timeStamp: Date.now() };
};
class DisplayItem extends React.Component {
render() { // redacted for brevity}
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
addItem(createItem());
};
const UserForm = (props) => {
return (
<div>
<Formik
initialValues={{
options: props.options
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(values);
props.onUpdate(values);
}}
>
{({ values }) => (
<Form> //redacted for brevity
</Form>
)}
</Formik>
</div>
);
};
return (
<div className="App">
<div className="App-left">
<UserForm options={options} onUpdate={onFormUpdate} />
</div>
<div className="App-right">
{itemInventory.map((item) => (
<DisplayItem item={item} key={item.timeStamp} />
))}
</div>
</div>
);
}
This is probably a "layup" for you all, can you help me dunk this one? Thx!
Solved problem by implementing the useEffect hook.
Solution: The functions that create and add an item to the list, addItem(createItem()), become the first argument for the useEffect hook. The second argument is the option stored in state, [options]. The callback for the form, onFormUpdate only updates the option in state and no longer tries to alter state, i.e. create and add an item to the list. The useEffect 'triggers' the creation and addition of a new item, this time based on the updated option because the updated option is the second argument of the hook.
Relevant new code:
useEffect( () => {
addItem(createItem());
}, [options]);
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
//addItem used to be here
};