In react best practice is Data flow from parent to child, event's will be passed from child to parent.
In this UI we have a parent component which contains 2 child components with forms. We now have to gather data from child components when the user clicks on the submit button in the parent.
Possible solutions (Bad solutions | anti pattern):
Pass ref from child and trigger child method from parent to collect data (Accessing child method from parent not a good idea)
<Parent>
onFormData(data) {
//here collect data from child
}
onSubmit() {
//Trigger a method onData in child
aRef.collectData()
}
<child-A ref={aRef} onData={onFormData}></Child-A>
<Child-B ref={bRef} onData={onFormData}></Child-B>
<button onClick={onSubmit}>Submit</Submit>
</Parent>
Bind a props to child, on submit click change value of prop with dummy value. Observe same prop in useEffect hook ( Really bad solution)
<Parent>
onFormData(data) {
//here collect data from child
}
onSubmit() {
//update randomValue, which will be observed in useEffect that calls onData method from child
randomValue = Math.random();
}
<child-A triggerSubmit={randomValue} onData={onFormData}></Child-A>
<Child-B triggerSubmit={randomValue} onData={onFormData}></Child-B>
<button onClick={onSubmit}>Submit</Submit>
</Parent>
Is there any other best approach for handling these scenario? How to a avoid anti pattern for this UI?
What I usually do is to "lift the state up" to the parent. This means I would not break the natural React flow, that is passing props from the parent to the children. Following your example, I would put ALL the logic in the parent (submit function, form state, etc)
Const Parent = () => {
const [formData, setFormData] = useState({})
const onSubmitForm = () => {
// send formData to somewhere
}
return (
<ChildrenForm onChange={setFormData} formData={formData} />
<button onSubmit={() => onSubmitForm()}>my button</button>
)
}
Now I would use the onChange function inside the Children to update the formData everytime an input in the ChildrenForm changes. So all my state will be in the parent and I don't need to worry about having to pass everything up, from children to parent (antipattern as you mentioned)
there is the third option (which is the standard way): you don't collect data, you pass your formData and setFormData to each Child as props. using lift state approach.
Each Child populates its input values with formData and uses setFormData to update formData that lives on Parent. Finally, at on submit you only have to set formDatato you request call.
below an example:
const ChildA = ({ formData, setFormData }) => {
const { name, age } = formData
const onChange = ({ target: { name, value } }) => { // destructuring 'name' and 'value'
setFormData(formData => ({ ...formData, [name]: value })) // spread formData, update field with 'name' key
}
return (
<>
<label>Name<input type="text" onChange={onChange} name="name" value={name} /></label>
<label>Age<input type="number" onChange={onChange} name="age" value={age} /></label>
</>
);
}
const ChildB = ({ formData, setFormData }) => {
const { email, acceptTerms } = formData
const onChange = ({ target: { name, value } }) => {
setFormData(formData => ({ ...formData, [name]: value }))
}
const onClick = ({ target: { name, checked } }) => {
setFormData(formData => ({ ...formData, [name]: checked }))
}
return (
<>
<label>email<input type="email" onChange={onChange} name="email" value={email} /></label>
<label>Accept Terms<input type="checkbox" onChange={onClick} name="acceptTerms" checked={acceptTerms} /></label>
</>
);
}
const Parent = () => {
// used one formData. you could break down into more if you prefer
const [formData, setFormData] = useState({ name: '', age: null, acceptTerms: false, email: undefined })
const onSubmit = (e) => {
e.preventDefault()
// here you implement logic to submit form
console.log(formData)
}
return (
<>
<ChildA formData={formData} setFormData={setFormData} />
<ChildB formData={formData} setFormData={setFormData} />
<button type="submit" onClick={onSubmit}>Submit</button>
</>
);
}
Approach 2) with random is Not Really So Bad! And only this one preserves encapsulation not involving DOM/withRef round trip. Suggested Up State way leads to hard coded dependency and undermines whole idea of re-usability. The only thing I have to mention is: code as it was typed above will not work - because usual variables will not trigger. Right way to make it like that:
// Parent component
let [randomValue, setRandomValue] = React.useState(Math.random());
const onSubmit = () => {
setRandomValue(Math.random());
console.log("click");
}
const onFormData = data => {
console.log(`${data}`);
}
...
<Child triggerSubmit={randomValue} onData={onFormData}/>
// Child component
useEffect(() => {
console.log("child id here");
prop.onData(`Hey Ho! Lets go!`)
}, [prop.triggerSubmit]);
At least this works for me!
Related
The problem is that my child component is not being re-rendered after I update name state using chrome.storage.sync.get.
So even though name="John" radio button is not checked.
storage.ts:
export function getName(): Promise<nameInterface> {
return new Promise((resolve) => {
chrome.storage.sync.get("name", (response: nameInterface) => {
resolve(response)
})
})
}
popup.tsx:
const [name, setName] = useState<string>()
useEffect(() => {
getName().then((storedName) => {
setName(storedName)
})
}, [])
return (
<div>
<RadioButton
name={name}
></RadioButton>
)
child.tsx:
const RadioButton: React.FC<AppProps> = ({ name}) => {
const [buttonChecked, setButtonChecked] = useState<string>(name)
return (
<input type="radio" checked={buttonChecked==="John"} />
)
}
It's re-rendering.
But what happens when this component re-renders? Nothing much. It already captured state from props the first time it rendered, and internally it only ever uses that state, and nothing ever changes that state. Since the state never changed, nothing in the UI changed.
If you want the component to use the prop instead of maintain internal state, get rid of the internal state:
const RadioButton: React.FC<AppProps> = ({name}) => {
return (
<input type="radio" checked={name==="John"} />
)
}
Since react useState works like a queue ,I have issue accessing the data passed to the parent component by child component.
Input.js
const Input = (props) => {
const [isValid, setIsValid] = useState(false);
const [value, setValue] = useState("");
<input id={props.id} className="self-start justify-self-start border-gray-400 border-solid border rounded shadow"
name={props.name}
type={props.type}
onChange={(e) => {
let valid = isNaN(value);
setIsValid(valid);
props.handleChange(e, valid);
}}/>
}
parent.js contains a function which access the data from the child component
const handleChange = (e, valid) => {
setFormData({
...formData,
[addr.name]: { value: e.target.value, isValid: valid },
});
};
The parent component always gets the previous validity of the input component. Is there any other way of passing data to parent component with latest state immediately after a state change in child component.
Update the valid argument in the handleChange callback based on the current value of the input field.
onChange={(e) => {
let valid = isNaN(e.target.value);
setIsValid(valid);
props.handleChange(e, valid);
}}
After the first render, the useReducer hook doesn't react to changes in its initialArg (second positional) argument. It makes it hard to properly sync it with an external value, without having to rely on an extra cycle by dispatching a reset action inside a useEffect hook.
I built a minimal example. It's a simple, formik-like, form provider. Here's what it looks like:
// App.js
const users = {
1: {
firstName: 'Paul',
lastName: 'Atreides',
},
2: {
firstName: 'Duncan',
lastName: 'Idaho',
},
};
const App = () => {
const [id, setId] = useState(1);
return (
<>
<div>Pick User</div>
<button onClick={() => { setId(1); }} type="button">User 1</button>
<button onClick={() => { setId(2); }} type="button">User 2</button>
<FormProvider initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
const [values, dispatch] = useReducer(reducer, initialValues);
const handleChange = useCallback((evt) => {
dispatch({
field: evt.target.name,
type: 'UPDATE_FIELD',
value: evt.target.value,
});
}, []);
return (
<FormContext.Provider value={{ handleChange, values }}>
{children}
</FormContext.Provider>
);
};
// Editor.js
const Editor = () => {
const { handleChange, values } = useContext(FormContext);
return (
<>
<div>First name:</div>
<input
name="firstName"
onChange={handleChange}
value={values.firstName}
/>
<div>First name:</div>
<input
name="lastName"
onChange={handleChange}
value={values.lastName}
/>
</>
);
};
If you open the demo and click on the User 2 button, you'll notice that nothing happens. It's not surprising since we know that the useReducer hook gets initialised once using the provided initialArg argument and never reads its value again.
What I expect is the useReducer state to reflect the new initialArg prop, i.e. I want to see "Duncan" in the First name input after clicking on the User 2 button.
From my point of vue, I can see two options:
1. Passing a key prop to the FormProvider component.
// App.js
const App = () => {
// ...
return (
<>
{/* ... */}
<FormProvider key={id} initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
This will indeed fix the problem by destroying and re-creating the FormProvider component (and its children) every time the id changes. But it feels like a hack to me. Plus, it seems inefficient to rebuild that entire part of the tree (which is substantial in the real application) just to get that input values updated. However, this seems to be a common fix for such problems.
2. Dispatch a RESET action whenever initialValues changes
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return action.values;
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
// ...
const isFirstRenderRef = useRef(true);
useEffect(() => {
if (!isFirstRenderRef.current) {
dispatch({
type: 'RESET',
values: initialValues,
});
}
}, [initialValues]);
useEffect(() => {
isFirstRenderRef.current = false;
}, []);
// ...
};
This will work as well, but, because it's happening inside a useEffect hook, it will require an extra cycle. It means that there'll be a moment where the form will contain stale values. If the user types at that moment, it could cause a race condition.
3. Idea
I read in this article by Mark Erikson that:
Function components may call setSomeState() directly while rendering, as long as it's done conditionally and isn't going to execute every time this component renders. [...] If a function component queues a state update while rendering, React will immediately apply the state update and synchronously re-render that one component before moving onwards.
So it seems that I should be able to call dispatch({ type: RESET, values: initialValues }); directly from the body of the function, under the condition that initialValues did change (I'd use a ref to keep track of its previous value). This should result in the state being updated in just one cycle. However, I couldn't get this to work.
——
What do you think is best between option 1, 2 and (3). Any advice/guidance on how I should address this problem?
I have a code like this
const ChildComponent = ({ products, setProducts }) => (
<form>
<input type="text" value={products.name} onChange={(e) => setProducts(e.target.value)} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState(
{
id: 1,
name: "Test",
}
);
useEffect(() => {
// Where i call API to get list product and set it to child component
}, [])
return <ChildComponent products={products} setProducts={setProducts} />
}
for some reason , i can ONLY update state of ParentComponent in ChildComponent. It's work but i think it's so weird, that look like i change props of child component everytime when i make a edit of input. Can any one tell me that is an anti pattern or not.Sorry about my bad English. Thank you so much!
It's not an anti-pattern to pass the state object and state updater function as props, but this offloads the responsibility to update state correctly and maintain the state invariant to consuming components.
As you can see, your child component already messes up and changes the state shape/invariant from Object to String.
const [products, setProducts] = useState({ // <-- object
id: 1,
name: "Test",
});
... child
onChange={(e) => setProducts(e.target.value)} // <-- string value
On the subsequent render attempting to access value={products.name} in the child will fail as now products is a string.
I typically suggest declaring a handler function to do the state update and pass that instead.
In your snippet it seems the child component is more a "controlled input" meaning it's an input tag with a value and onChange handler. This is an example refactor I would do.
const ChildComponent = ({ value, onChange, onSubmit }) => (
<form onSubmit={onSubmit}>
<input type="text" value={value} onChange={onChange} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState({
id: 1,
name: "Test",
});
const changeHandler = e => {
setProducts(products => ({
...products,
name: e.target.value,
}));
};
const onSubmit = e => {
e.preventDefault();
// handle the form submission
};
useEffect(() => {
// Where i call API to get list product and set it to child component
}, []);
return (
<ChildComponent
value={products}
onChange={changeHandler}
onSubmit={submitHandler}
/>
);
}
This way the parent maintains control over both the state updates and how the form data is submitted. The child hasn't any idea what the value represents and it isn't trying to update anything in any way, but simply passing back out the events.
I've come accross a performance optimization issue that I feel could be fixed somehow but I'm not sure how.
Suppose I have a collection of objects that I want to be editable. The parent component contains all objects and renders a list with an editor component that shows the value and also allows to modify the objects.
A simplified example would be this :
import React, { useState } from 'react'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = props => {
const { object, onChange } = props
return (
<li>
<Input value={object.name} onChange={onChange('name')} />
</li>
)
}
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = id => key => value => {
const newObjects = objects.map(obj => {
if (obj.id === id) {
return {
...obj,
[key]: value
}
}
return obj
})
setObjects(newObjects)
}
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} object={obj} onChange={handleObjectChange(obj.id)} />
))
}
</ul>
)
}
export default Objects
So I could use React.memo so that when I edit the name of one object the others don't rerender. However, because of the onChange handler being recreated everytime in the parent component of ObjectEditor, all objects always render anyways.
I can't solve it by using useCallback on my handler since I would have to pass it my objects as a dependency, which is itself recreated everytime an object's name changes.
It seems to me like it is not necessary for all the objects that haven't changed to rerender anyway because the handler changed. And there should be a way to improve this.
Any ideas ?
I've seen in the React Sortly repo that they use debounce in combination with each object editor changing it's own state.
This allows only the edited component to change and rerender while someone is typing and updates the parent only once if no other change event comes up in a given delay.
handleChangeName = (e) => {
this.setState({ name: e.target.value }, () => this.change());
}
change = debounce(() => {
const { index, onChange } = this.props;
const { name } = this.state;
onChange(index, { name });
}, 300);
This is the best solution I can see right now but since they use the setState callback function I haven't been able to figure out a way to make this work with hooks.
You have to use the functional form of setState:
setState((prevState) => {
// ACCESS prevState
return someNewState;
});
You'll be able to access the current state value (prevState) while updating it.
Then way you can use the useCallback hook without the need of adding your state object to the dependency array. The setState function doesn't need to be in the dependency array, because it won't change accross renders.
Thus, you'll be able to use React.memo on the children, and only the ones that receive different props (shallow compare) will re-render.
EXAMPLE IN SNIPPET BELOW
const InputField = React.memo((props) => {
console.log('Rendering InputField '+ props.index + '...');
return(
<div>
<input
type='text'
value={props.value}
onChange={()=>
props.handleChange(event.target.value,props.index)
}
/>
</div>
);
});
function App() {
console.log('Rendering App...');
const [inputValues,setInputValues] = React.useState(
['0','1','2']
);
const handleChange = React.useCallback((newValue,index)=>{
setInputValues((prevState)=>{
const aux = Array.from(prevState);
aux[index] = newValue;
return aux;
});
},[]);
const inputItems = inputValues.map((item,index) =>
<InputField
value={item}
index={index}
handleChange={handleChange}
/>
);
return(
<div>
{inputItems}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Okay, so it seems that debounce works if it's wrapped in useCallback
Not sure why it doesn't seem to be necessary to pass newObject as a dependency in the updateParent function though.
So to make this work I had to make the following changes :
First, useCallback in the parent and change it to take the whole object instead of being responsible for updating the keys.
Then update the ObjectEditor to have its own state and handle the change to the keys.
And wrap the onChange handler that will update the parent in the debounce
import React, { useState, useEffect } from 'react'
import debounce from 'lodash.debounce'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = React.memo(props => {
const { initialObject, onChange } = props
const [object, setObject] = useState(initialObject)
const updateParent = useCallback(debounce((newObject) => {
onChange(newObject)
}, 500), [onChange])
// synchronize the object if it's changed in the parent
useEffect(() => {
setObject(initialObject)
}, [initialObject])
const handleChange = key => value => {
const newObject = {
...object,
[key]: value
}
setObject(newObject)
updateParent(newObject)
}
return (
<li>
<Input value={object.name} onChange={handleChange('name')} />
</li>
)
})
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = useCallback(newObj => {
const newObjects = objects.map(obj => {
if (newObj.id === id) {
return newObj
}
return obj
})
setObjects(newObjects)
}, [objects])
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} initialObject={obj} onChange={handleObjectChange} />
))
}
</ul>
)
}
export default Objects