Does pass all [state, setState] to child component is an anti pattern? - reactjs

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.

Related

How to update props in child component after getting values from chrome.storage?

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"} />
)
}

How to Sort Form Input Automatically With React Hooks

I have a form where user can enter a name that will then be displayed on a list. Upon entering a new name the list should automatically be sorted in alphabetical order. Current attempt with useEffect does work but is buggy(list will only be sorted after user start deleting previous input text).
A few notable things to highlight with current setup:
Submission component is used for rendering list of names
Form component is used to store state of app and input fields
handleSortName() will execute sorting
useEffect() executes handleSortName() when there is a change to submissions value
import React, { useEffect, useState } from "react";
const Submission = ({ submission }) => {
return <div>name: {submission.name}</div>;
};
const Form = () => {
const [values, setValues] = useState({
name: ""
});
const [submissions, setSubmission] = useState([
{ name: "John" }
]);
const addSubmission = (values) => {
const newSubmissions = [...submissions, values];
setSubmission(newSubmissions);
};
const handleChange = (event) => {
const value = event.target.value;
setValues({ ...values, [event.target.name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
addSubmission(values);
handleSortName(submissions);
};
const handleSortName = (submissions) => {
return submissions.sort((a, b) => a.name.localeCompare(b.name));
};
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
return (
<>
<form onSubmit={handleSubmit}>
<h1>Student Enrollment</h1>
<div>
<label>name: </label>
<input
required
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<input type="submit" value="Submit" />
</div>
</form>
<h1>Submitted Student</h1>
{submissions.map((submission, index) => (
<Submission key={index} submission={submission} />
))}
</>
);
};
export default Form;
Working Sample: https://codesandbox.io/s/usestate-form-oj61v9?file=/src/Form.js
I am aware that useState is asynchronous and will not update value right away.
Any suggestion on other implementations such as functional updates, a custom hook or current UseEffect approach? Thanks in Advance!
UPDATE:
because React re-renders the component when the props or state changes. that means inside your handleSortName() function you have to call setSubmissions with the new sorted array, then React will know that the state was changed.
const handleSortName = (submissions) => {
// create a new copy of the array with the three dots operator:
let copyOfSubmissions = [...submissions];
// set the state to the new sorted array:
setSubmissions(
copyOfSubmissions.sort((a, b) => a.name.localeCompare(b.name))
);
};
or you can do both steps in 1 line:
const handleSortName = (submissions) => {
// set the state to the newly created sorted array with the three dots operator:
setSubmissions(
[...submissions].sort((a, b) => a.name.localeCompare(b.name))
);
};
sandbox link here

Passing State to parent immediately after updating React

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);
}}

Unable to update state for a component

I have just started learning react.
I have a component which is calling the weather API and fetching data for user provided location, user input is getting updated under userLoc but somehow the state is not getting updated for finalLoc and whenever I console log it, it is showing undefined.
const Inputs = props => {
const [userLocation, setUserLocation] = React.useState('')
const [finalLocation, setFinalLocation] = React.useState('')
function fetchLocation(e) {
setUserLocation (e.target.value)
}
function fetchDetails(e) {
e.preventDefault()
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
const API_KEY = '&API_KEY'
let total = baseURL + userLocation + API_KEY
console.log(userLocation) // Outputs the input value
setFinalLocation(userLocation)
console.log(finalLocation) // Comes as blank
console.log(total);
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={loc.userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={loc.finalLoc} results={loc.result} />
</div>
)
}
const Inputs = props => {
const [finalLoc, setFinalLoc] = React.useState("");
const [userLoc, setUserLoc] = React.useState("");
const [data, setData] = React.useState([]);
function fetchLocation(e) {
userLoc( e.target.value )
}
function fetchDetails(e) {
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
let API_KEY = '&appid=API_KEY'
let total = baseURL + loc.userLoc + API_KEY
setFinalLoc( userLoc )
fetch(total)
.then(response => response.json())
.then(data => {
setData( data )
})
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={ userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={ finalLoc} results={data} />
</div>
)
}
If you update a state when using useState you don't write it as
setState({state: //data})
But
setState(//data)
The updating method in useState is similar to class component setState method but you have to update it differently.
See more about useState here.
If your state is an object you should update it as an object:
const [state, setState] = useState({
name: "John",
lastName: "Due",
});
setState({
name: "Liam",
lastName: "call",
});
BTW,
If you are updating multiple values in your state, You should update it in one function.#1
Note:
Each time you're updating your state cuase React to re-render and slow your app.
For better performance you should always try to re-render your app as less as possible.
Edit:
#1 if your state is an object you should update it in one function.
You can use the functional form. The function will receive the previous value, and return an updated value. so to update the state we have to make copy of previous value with updated one and then set the state with updated values
setLoc((loc) => ({ ...loc, finalLoc: loc.userLoc }));
same when you get result data keep copy of previous state
setLoc((loc) => ({ ...loc, result: data }));

React collect child component data on some event from the parent component

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!

Resources