React: Get state of children component component in parent - reactjs

I have this container where and is not placed in the same level. How can I get the state of the Form when I click on the button (which is placed on the parent) ?
I've created a demo to address my issue.
https://codesandbox.io/s/kmqw47p8x7
class App extends React.Component {
constructor(props) {
super(props);
}
save = () => {
alert("how to get state of Form?");
//fire api call
};
render() {
return (
<div>
<Form />
<button onClick={this.save}>save</button>
</div>
);
}
}
One thing I don't want to do is sync the state for onChange event, because within Form there might be another Form.

To access a child instance from parent, your need to know about ref:
First, add formRef at top your App class:
formRef = React.createRef();
Then in App render, pass ref prop to your Form tag:
<Form ref={this.formRef} />
Finaly, get state from child form:
save = () => {
alert("how to get state of Form?");
const form = this.formRef.current;
console.log(form.state)
};
Checkout demo here

ideally, your form submit action belongs to the Form component
You can put button inside your From component and pass a submit callback to the form.
class App extends React.Component {
constructor(props) {
super(props);
}
save = (data) => {
// data is passed by Form component
alert("how to get state of Form?");
//fire api call
};
render() {
return (
<div>
<Form onFormSubmit={this.save} />
</div>
);
}
}

you can write the code like this
https://codesandbox.io/s/23o469kyx0

As it was mentioned, a ref can be used to get stateful component instance and access the state, but this breaks encapsulation:
<Form ref={this.formRef}/>
A more preferable way is to refactor Form to handle this case, i.e. accept onChange callback prop that would be triggered on form state changes:
<Form onChange={this.onFormChange}/>
One thing I don't want to do is sync the state for onChange event, because within Form there might be another Form.
Forms will need to handle this any way; it would be impossible to reach nested form with a ref from a grandparent. This could be the case for lifting the state up.
E.g. in parent component:
state = {
formState: {}
};
onFormChange = (formState) => {
this.setState(state => ({
formState: { ...state.formState, ...formState }
}));
}
render() {
return (
<Form state={this.state.formState} onChange={this.onFormChange} />
);
}
In form component:
handleChange = e =>
this.props.onChange({
[e.target.name]: e.target.value
});
render() {
return (
<input
onChange={this.handleChange}
name="firstName"
value={this.props.state.firstName}
/>
);
}
Here is a demo.

Related

How to get children values on antd's form submit?

I made a custom component called TagsInput, and added it to a form. My problem is, when the form is submitted, the value for field tags is always undefined, because the Form.Item cannot retrieve the value from the TagsInput child. This is usually not a problem when children of Form.Item are antd components.
export default function NewProjectForm(props) {
return (
<Form layout="vertical" onFinish={props.onSubmit} id="newProjectForm">
<Form.Item label="Tags" name="tags">
<TagsInput />
</Form.Item>
</Form>
);
}
TagsInput is a class component that has its value within its state:
class TagsInput extends React.Component{
constructor(props){
super(props);
this.state = {
value: []
}
}
render(){
return(<div></div>)
}
}
I dont want to elevate the value field from TagsInput state to NewProjectForm because it is not a class component... I would be doing it just for the sake of solving this issue. I am asking this question because I think there is a cleaner way of solving this.
How can I make NewProjectForm retrieve the value of TagsInput from within its state?
Is there any function I can pass to TagsInput as a property, so that the form asks the component for a value when it is submitted?
In order to achive what you want, you need to handle the state of the form (and its components) at the parent component (NewProjectForm).
If you are willing to keep that component a function component, you can use React Hooks and define a state with useState like this:
const [tagValue, setTagValue] = useState([]);
While tagValue is the value you will get from the child TagsInput, and the setTagValue is the function that can set that value. You need to pass setTagValue as a function prop to TagsInput and use it on change. It will look something like this:
export default function NewProjectForm(props) {
const [tagValue, setTagValue] = useState([]);
return (
<Form layout="vertical" onFinish={props.onSubmit} id="newProjectForm">
<Form.Item label="Tags" name="tags">
<TagsInput setTagValue={(val)=>setTagValue(val)} />
</Form.Item>
</Form>
);
}
class TagsInput extends React.Component{
constructor(props){
super(props);
this.state = {
value: []
}
}
.........
.........
onChangeInput(val){
this.props.setTagValue(val);
}
.........
.........
render(){
return(<div></div>)
}
}
EDIT
After the clarification in the comments, you actually need to follow the steps of using Customized Form Controls, As described here.
You need to pass an object with value and onChange that changes the value. Then, the Form component should manage to get that value automatically.
So I guess it should look something like that:
const TagsInput= ({ value = {}, onChange }) => {
const triggerChange = newValue => {
if (onChange) {
onChange(newValue);
}
};
return(<div></div>)
};

Render child component in parent after re-rendering sibling component

I have a parent component housing two children components(AddPersonForm and PeopleList). When I submit a name via the AddPersonForm, I expect it to be rendered in the PeopleList component, but it doesn't.
Here is my AddPersonForm:
class AddPersonForm extends React.Component {
state = {
person: ""
}
handleChange = (e) => this.setState({person: e.target.value});
handleSubmit = (e) => {
if(this.state.person != '') {
this.props.parentMethod(this.state.person);
this.setState({person: ""});
}
e.preventDefault();
}
render() {
return (
<form onSubmit={this. handleSubmit}>
<input type="text" placeholder="Add new contact" onChange={this.handleChange} value={this.state.person} />
<button type="submit">Add</button>
</form>
);
}
My PeopleList component:
class PeopleList extends React.Component {
constructor(props) {
super(props);
const arr = this.props.data;
this.state = {
listItems: arr.map((val, index) => <li key={index}>{val}</li> );
}
}
render() {
return <ul>{this.state.listItems}</ul>;
}
}
Now the parent component, ContactManager:
class ContactManager extends React.Component {
state = {
contacts: this.props.data
}
addPerson = (name) => {
this.setState({contacts: [... this.state.contacts, name]});
render() {
return (
<div>
<AddPersonForm parentMethod={this. addPerson}×/>
<PeopleList data={this.state.contacts} />
</div>
);
Please what I'm I doing wrong, or not doing?
The issue is in your PeopleList component. The state object which renders your list is created in the constructor when the component mounts, but you have no way of updating it when it recieves new values. It will always give you the initial value.
You could introduce a lifecycle method, componentDidUpdate, which would allow you to compare the previous props to the new props when they arrive, and update the state accordingly. I would recommend you not do this for two reasons:
Storing props directly in a components state is not good practice. You are just creating a copy of the state in the component above and that creates opportunities for confusion and stale values when one of them updates. Ideally, each piece of data should live in only one place.
If all PeopleList is doing is rendering your data, then it doesn't need any state at all. It can act as a display component that maps your props in place and doesn't have to worry about updating itself or managing its own data. This would actually make it a good candidate for conversion into a functional component.
class PeopleList extends React.Component {
render() {
return (
<ul>
{this.props.data.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
);
}
}
You are initializing PeopleList with props when its created and mounted but then you are not using new values of props for updating it.
To fix your issue use current value of prop when rendering:
class PeopleList extends React.Component {
render() {
return <ul>{ this.props.data.map((val, index) => <li key={index}>{val}</li>) }</ul>;
}
}

Pass state as props from parent to child and then update parent from child onSubmit

I have a parent component that renders a child component and passes it's initial state to the child component. I need a few clarifications- My gut feeling is to handle the event change in the Child component, is this correct? Upon submission, how do I pass the updated props back to parent? My gut is also telling that once the props is passed back to the parent, I can use componentDidUpdate() to set the state to be used elsewhere. If so, how?
class Parent extends React.Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "destination"
};
}
componentDidUpdate(){
// how to update state?
}
render() {
const { arrival, departure } = this.state;
return <Child arrival={arrival} departure={departure} />;
}
}
class Child extends React.Component{
constructor(){
this.handleSubmission = this.handleSubmission.bind(this);
}
handleSubmission(e){
const target = e.target;
const name = target.name;
const value = target.value;
// not sure how to handle props from here
}
render(){
let { arrival, departure } = this.props;
return(
<form onSubmit = {this.handleSubmission} >
<div class="form-group">
<label for="departure">Departure</label>
<input type="" class="form-control" name="departure" aria-describedby="emailHelp" placeholder="Enter Departing Station"/>
</div>
<div class="form-group">
<label for="arrival">Arrival</label>
<input type="password" class="form-control" name="arrival" id="inputArrival" placeholder="Enter Arriving Station"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
)
}
}
export default Child
#hello_there
Please disregard the previous answer, as it requires a lot more than just changing a few props.
I've forked the Sandbox and rewrote it here (so you can follow along).
I've outlined the steps to make the change to propagate to the parent below.
Capture the states of <input />
First step is to capture the state of form fields.
There are two ways to handle form fields
Controlled Components
Uncontrolled Components - discouraged
I am going to use the former (controlled) to capture the form field states by adding a state to Form component.
And you need to set the value={...} of each state and update each state from onChange event (using handleInputChange added below) for each form field.
I've added 👇 where changes were made
import React, { Component } from "react";
class Form extends Component {
// .. 👇
state = {
departure: "",
arrival: ""
};
//... rest removed for brevity
// .. 👇 is used to update each form field state.
handleInputChange = e => {
e.preventDefault();
const { name, value } = e.target;
this.setState({ [name]: value });
};
render() {
const { departure, arrival } = this.state;
return (
<form onSubmit={this.handleSubmission}>
<div className="form-group">
<label> Departure</label>
<input
className="form-control"
name="departure"
placeholder="Enter Departing Station"
// ... 👇 ...👇
value={departure} onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label> Arrival</label>
<input
className="form-control"
name="arrival"
id="inputArrival"
placeholder="Enter Arriving Station"
// ... 👇 ...👇
value={arrival} onChange={this.handleInputChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
}
Update App's state change event handler
Now we have states handy, we need to update the App's updateState to accept a whole new state, so we don't make multiple method calls (this.props.updateParentState) and it'd let us pass a new reference so that React would know that the state has been changed in the App component.
class App extends Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "departure"
};
}
// From this 👇
// updateState = (name, value) => {
// this.setState({
// [name]: value
// });
// };
// to this 👇
updateState = newState => this.setState(newState);
componentDidUpdate(prevProps, prevState) {
const { arrival, departure } = this.state;
console.log(`Arrival: ${arrival}, departure: ${departure}`);
}
render() {
const { arrival, departure } = this.state;
return (
<Fragment>
<Form
arrival={arrival}
departure={departure}
// 👇 stays the same
updateParentState={this.updateState}
/>
</Fragment>
);
}
}
Update Child's submission event handler
Now the App.updateState accepts a state object, which can be used to update App.state, let's change Child.handSubmission.
handleSubmission = e => {
e.preventDefault();
// this.props.updateParentState(name, value);
this.props.updateParentState(this.state);
};
You can see that this.props.updateParentState(name, value) has been replaced with this.props.updateParentState(this.state), which would let us update App.state at once.
Now you should be able to see the change in the App.componentDidUpdate.
OLD ANSWER - Disregard this
Changing the state in the child doesn't cause the re-render in the parent (so componentDidUpdate is probably not triggered by change in child component). So what you can do is to pass the event handler down to the child, which the child can notify the Parent component that something has changed.
I've explained the flow of how you can update the parent's states.
First you need to create a handler, with which you can update the Parent's state with.
updateState = (name, value) => this.setState({ [name]: value });
Here, I am using [name], which is a dynamic property. So it should match up with the Parent's state name. In this case either arrival or departure.
Then you need to pass that event handler to the Child (you can name the prop name to whatever (In the code below, I used updateParentState but it can be updateWhatever so long as you pass it the updateState correctly).
<Child arrival={arrival} departure={departure} updateParentState={updateState} />
Here is the complete code that will update the parent state.
Changes are indicated with "👇" emoji below.
class Parent extends React.Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "destination"
};
}
// This is what you pass to the child as a `prop` to call.
updateState = (name, value) => this.setState({ [name]: value });
render() {
const { arrival, departure } = this.state;
// pass the event handler to the child component ... 👇 ...
return <Child arrival={arrival} departure={departure} updateParentState={updateState} />;
}
}
class Child extends React.Component{
constructor(){
this.handleSubmission = this.handleSubmission.bind(this);
}
handleSubmission(e){
const target = e.target;
const name = target.name;
const value = target.value;
// 👇 Update the "Parent"'s state.
// `name` & `value` will be the supplied to `Parent.updateState`
this.props.updateParentState(name, value)
}
render(){
// ... removed for brevity
}
}
If the statement management gets too hard to manage with prop-drilling, then you can reach out for Context API (after familiarizing with it, you can check out How to use React Context effectively, which uses Hooks).
Or you can use Redux.
Just pass to your children a reference on how to update the state:
class Parent extends React.Component{
state = {departure : ''}
setDeparture = departure => this.setState({ departure })
render(){ <Child setDeparture={this.setDeparture} /> }
}
const Child = ({setDeparture}) => <button onClick={() => setDeparture('foo')}>Click</button>

How to change a component's state correctly from another component as a login method executes?

I have two components - a sign in form component that holds the form and handles login logic, and a progress bar similar to the one on top here in SO. I want to be able to show my progress bar fill up as the login logic executes if that makes sense, so as something is happening show the user an indication of loading. I've got the styling sorted I just need to understand how to correctly trigger the functions.
I'm new to React so my first thought was to define handleFillerStateMax() and handleFillerStateMin() within my ProgressBarComponent to perform the state changes. As the state changes it basically changes the width of the progress bar, it all works fine. But how do I call the functions from ProgressBarComponent as my Login component onSubmit logic executes? I've commented my ideas but they obviously don't work..
ProgressBarComponent:
class ProgressBarComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
percentage: 0
}
}
// the functions to change state
handleFillerStateMax = () => {
this.setState ({percentage: 100})
}
handleFillerStateMin = () => {
this.setState ({percentage: 0})
}
render () {
return (
<div>
<ProgressBar percentage={this.state.percentage}/>
</div>
)
}
}
Login component:
class SignInFormBase extends Component {
constructor(props) {
super(props);
this.state = {...INITIAL_STATE};
}
onSubmit = event => {
const {email, password} = this.state;
// ProgressBarComponent.handleFillerMax()????
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
//ProgressBarComponent.handleFillerMin()????
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}
Rephrase what you're doing. Not "setting the progress bar's progress" but "modifying the applications state such that the progress bar will re-render with new data".
Keep the current progress in the state of the parent of SignInFormBase and ProgressBarComponent, and pass it to ProgressBarComponent as a prop so it just renders what it is told. Unless there is some internal logic omitted from ProgressBar that handles its own progress update; is there?
Pass in a callback to SignInFormBase that it can call when it has new information to report: that is, replace ProgressBarComponent.handleFillerMax() with this.props.reportProgress(100) or some such thing. The callback should setState({progress: value}).
Now, when the SignInFormBase calls the reportProgress callback, it sets the state in the parent components. This state is passed in to ProgressBarComponent as a prop, so the fact that it changed will cause he progress bar to re-render.
Requested example for #2, something like the following untested code:
class App extends Component {
handleProgressUpdate(progress) {
this.setState({progress: progress});
}
render() {
return (
<MyRootElement>
<ProgressBar progress={this.state.progress} />
<LoginForm onProgressUpudate={(progress) => this.handleProgressUpdate(progress)} />
</MyRootElemen>
)
}
}
The simply call this.props.onProgressUpdate(value) from LoginForm whenever it has new information that should change the value.
In basic terms, this is the sort of structure to go for (using useState for brevity but it could of course be a class-based stateful component if you prefer):
const App = ()=> {
const [isLoggingIn, setIsLoggingIn] = useState(false)
const handleOnLoginStart = () => {
setIsLoggingIn(true)
}
const handleOnLoginSuccess = () => {
setIsLoggingIn(false)
}
<div>
<ProgressBar percentage={isLoggingIn?0:100}/>
<LoginForm onLoginStart={handleOnLogin} onLoginSuccess={handleOnLoginSuccess}/>
</div>
}
In your LoginForm you would have:
onSubmit = event => {
const {email, password} = this.state;
this.props.onLoginStart() // <-- call the callback
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
this.props.onLoginSuccess() // <-- call the callback
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}

React/redux child update child (<select> update <select>)

I have container, binded with connect() to my store.
// GetActiveAccount.jsx
const VisibleActiveAccountsList = connect(
mapStateToProps,
mapDispatchToProps
)(ActiveAccountsSelector)
ActiveAccountsSelector is the presentational component. Inside this presentational component I load two child presentational components which are only
//activeAccountSelector render method
return(
<div>
<GatewaySelector gateways={gateways} onSelectGateway={ (e) => {
console.log(e.target.value);
}
}/>
<PairSelector gateway={gateways[0]} onSelectPair={ (e) => {
console.log(e.target.value);
}
}/>
</div>
)
What I want is that the selected value on gatewaySelector determine the value passed to PairSelector and update it when changed.
What is the right way to do that ? Going through actions seems way overkill for a simple select change update.
I guess I have to manager that in the parent (activeAccountSelector) but how ? It seems not advised to change state without going through the whole process (actions,reducers ...etc) shall I do that changing parents props ?
Yes. You have to manage that in state of the parent component. Simply, you can change set the value for gateway property of PairSelector from parent's state and change the state when gateway change in GatewaySelector.
Redux suggest to using react state for the state that doesn't matter to the app globally. Read this comment from the Redux author for more information.
class ActiveAccountSelector extends React.Component{
contructor(props){
super(props);
this.state = { selectedGateway: null};
this.onSelectGateway = this.onSelectGateway.bind(this);
}
onSelectGateway(e){
this.setState({ selectedGateway: e.target.value });
}
render(){}
....
return(
<div>
<form>
<GatewaySelector gateways={gateways} onSelectGateway={ this.onSelectGateway}
}/>
<PairSelector gateway={this.state.selectedGateway} onSelectPair={ (e) => {
console.log(e.target.value);
}}/>
</form>
</div>
);
}
}

Resources