React child component doesn't re-render when parent state changes - reactjs

I'm writing a React stateful component to process text inputted in a textbox, and to save and load text between a database and the textbox. I can type in the textbox, and I can save what's in the textbox to my database. When I load from the database to the textbox, my component's state updates, and the (child) textbox's prop which handles passing text from the parent component to the textbox updates according to React DevTools in Chrome. The problem is that the value of the textbox is not re-rendered to reflect the new prop.
I did some research, and I found that React components are supposed to re-render whenever the state changes. I tried adding state to the child component using useState(), and I update the state every time the props change using
useEffect(() => setText(propText), [propText])
It didn't work. The props and state in React DevTools show the value I loaded in, but the component doesn't show it in the textbox. Maybe it isn't designed to get its input value from props and state as opposed to a user typing, but its onChange() event handler updates the parent state like I'm trying to do with the database access component, so it's completely abstracted.
Here's the child component as it stands right now:
function InputTextComponent(props) {
const { parentInput, textUpdate } = props;
const [text, setText] = useState(parentInput);
useEffect(() => setText(parentInput), [parentInput]);
const onTextChange = event => {
const {
target: { value }
} = event;
textUpdate(value);
};
return (
<TextAreaGroup
placeholder={'Input text'}
name={'Input text here'}
value={text}
info={'Input text'}
onChange={event => onTextChange(event)}
/>
);
}
Here's the parent component:
class ParentComponent extends Component {
constructor() {
super();
this.state = {
textInput: '',
};
this.textUpdate.bind(this);
}
textUpdate = textInput => this.setState({ textInput });
render() {
const {
textInput,
} = this.state;
return (
<div className="container mt-3">
<InputTextComponent
textUpdate={this.textUpdate}
parentInput={textInput}
/>
<LoadTextsComponent
alias={alias} // alias is irrelevant part of the component
aliasUpdate={this.aliasUpdate}
skuUpdate={this.skuUpdate}
/>
</div>
);
}
}
The database load component uses the textUpdate() function. Here's the full component:
function LoadTextsComponent(props) {
const { alias, aliasUpdate, textUpdate } = props;
const [setList, setListUpdate] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await client.get('/texts/listTexts');
aliasUpdate(result[0].alias);
setListUpdate(result);
};
fetchData();
}, []);
const loadTextsToInputBox = () => {
let aliasSet;
for (let set of setList) {
if (set.alias === alias) {
aliasSet = set.texts;
}
}
let textString = '';
for (let text of aliasSet) {
textString += text + '\n';
}
textString = textString.slice(0, -1);
textUpdate(textString);
};
return (
<div>
<ShowAliasesComponent
setList={setList}
alias={alias}
aliasUpdate={aliasUpdate}
/>
<button
disabled={!alias}
className="btn btn-primary"
onClick={() => loadTextsToInputBox()}
>
Send texts of alias {alias} to input box
</button>
</div>
);
}
What's really confusing is when I type in the child textbox, the parent state updates correctly (passing down a function to update parent state) and the new prop is reflected in the render. But, when I call that same function from the database load component, nothing happens.

Related

Re-render Child component and change child state states when parent state changes

I have a parent component which has a state containing an array of object:
function ParentComponent(props) {
const [objects, setObjects] = React.useState([...props.objects]);
const handleObjectRefresh = () => {
/**
* Refresh objects
*/
setObjects([
...newObjects
])
}
return (
<div>
<Button onClick={handleObjectRefresh} >Refresh</Button>
{objects.map((obj, index) => {
return(
<ChildComponent obj={obj} />
)
})
}
</div>
)
}
function ChildComponent(props){
const [objParam1, setObjectParam1] = React.useState(props.obj.param1);
const [objParam2, setObjectParam2] = React.useState(props.obj.param2);
useEffect(()=> {
setObjectParam1(props.obj.param1);
setObjectParam2(props.obj.param2);
}, [props.obj])
return (
<div>
<Texfield defaultValue={objectParam1}/>
<Texfield defaultValue={objectParam2}/>
</div>
)
}
My issue is right now the current default values of Textfield does not get updated if an object is removed, the object does change in the child component however, the textfield default value are the previous state, I was wondering how can I completely change the child state when the object list in the parent component changes?
EDIT
To fix the issue, I simply changed defaultValue to value

React: How to display mapped array after button press?

I'm new to react and struggling to get this to work. I can display a mapped array, but I am trying to update the array, map it, then display it. Can't figure out how to get the mapped array to display after it's updated?
Tried to add the mappedItems to my function also, but that didn't seem to solve it. TIA.
import React, { Component } from 'react';
const itemsList = ['item1','item2'];
class RecieveInput extends React.Component {
constructor(){
super();
this.state = {
list: itemsList
}
}
render(){
const pressedEnter = event => {
if (event.key === 'Enter') {
this.state.list.push(event.target.value);
console.log(this.state.list);
}
}
const mappedItemsList = this.state.list.map( (listItem, i) => <li>{this.state.list[i]}</li> )
return(
<div>
<input
type ="text"
id="textInput"
placeholder="Enter Item Here"
onKeyPress={pressedEnter}
/>
<ol className='ol'>{mappedItemsList}</ol>
</div>
)
}
}
export default RecieveInput
enter image description here
Issue
The pressedEnter callback should be declared in the component, not the render function. It should also not mutate the state object by pushing into the state array.
Solution
Move the pressedEnter handler into the component and use this.setState to enqueue an update. You need to shallow copy the existing state and append the new element into it so your list state is a new array reference.
const pressedEnter = event => {
const { value } = event.target;
if (event.key === 'Enter') {
this.setState(prevState => ({
list: prevState.list.concat(value), // or [...prevState.list, value]
}));
}
}

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

Trouble updating radio button state value in parent component from child element

I'm currently working a a multipage checklist app to make a common checklist procedure more efficient.
my parent component called MainForm has all of the states for my app. In my first child element, I had to fill some text inputs. The states are updating and saving as planned. My second page (or other child element) was the portion where my checklist would begin. The issue is my app is rending, but the radiobutton value isn't being sent to my state. I'm also having an issue where I can select the 'yes' radio button and then the 'no' radio button, but I can't go from 'no' to 'yes'. radioGroup21 is the radio group that's giving me problem. All other states are working.
I'm getting an error in my console that says:
"Checkbox contains an input of type radio with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props.
I've tried removing the value tag and the defaultValue line in my Radio elements, but no luck. I've tried creating constructor(props) in my parent element but I still kept having issues."
So far I've tried removing the defaultValue in my radio button and after I tried removing the value line. Unfortunately I this did not help.
I also read about controlled and uncontrolled inputs. I've tried changing my parent components state to put them in a constructor(props) bracket. But no luck.
I also tried to not use the handleChange function and use the setState function with values of {radioButton21 === 'yes'} but that didn't work.
//Parent Component
Class MainForm extends Component {
state = {
step: 1,
projectNumber: '',
projectName: '',
numberOfSystems: '',
buildSheet: '',
controlPhilosophy: '',
projectLayoutDrawing: '',
projSoftwareValidation: '',
CppDrawing: '',
radioGroup21: '',
}
nextStep = () => {
const { step } = this.state
this.setState({
step : step + 1
})
}
prevStep = () => {
const { step } = this.state
this.setState({
step : step - 1
})
}
handleChange = input => event => {
this.setState({ [input] : event.target.value })
}
render(){
const {step} = this.state;
const { projectNumber, projectName, numberOfSystems, buildSheet , controlPhilosophy, projectLayoutDrawing, projSoftwareValidation, CppDrawing, radioGroup21 } = this.state;
const values = { projectNumber, projectName, numberOfSystems, buildSheet, controlPhilosophy, projectLayoutDrawing, projSoftwareValidation, CppDrawing, radioGroup21 };
switch(step) {
case 1:
return <ProjectInfo
nextStep={this.nextStep}
handleChange = {this.handleChange}
values={values}
/>
case 2:
return <PrelimInspection
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange = {this.handleChange}
values={values}
/>
export default MainForm;
-----------------------------------
//Child Component
import React, { Component } from 'react';
import { Form, Button, Radio } from 'semantic-ui-react';
import { throws } from 'assert';
class PrelimInspection extends Component{
saveAndContinue = (e) => {
e.preventDefault();
this.props.nextStep();
}
back = (e) => {
e.preventDefault();
this.props.prevStep();
}
render(){
const { values } = this.props
return(
<Form color='blue' >
<h1 className="ui centered">System Installation</h1>
<Form.Field inline>
<Form.Field>System Properly Supported</Form.Field>
<Radio
label = {'Yes'}
name = {'radio21'}
value = {'Yes'}
onChange={this.props.handleChange('radioGroup21')}
defaultValue={values.radioGroup21}
/>
<Radio
label = {'No'}
name = {'radio21'}
value = {'No'}
onChange={this.props.handleChange('radioGroup21')}
defaultValue={values.radioGroup21}
/>
</Form.Field>
<Button onClick={this.back}>Back</Button>
<Button onClick={this.saveAndContinue}>Save And Continue </Button>
</Form>
)
}
}
export default PrelimInspection
The app is rendering and the layout is correct. Unfortunately the state values aren't being sent to the parent state.
I checked the documentation https://react.semantic-ui.com/addons/radio/#types-radio-group and I have found few things you missed:
1.) Radio component asked the checked props (but you did not supply it).
2.) Which then requires you to pass the value, in your case it should come from the parent component:
<PrelimInspection
valueFromParent={this.state["radioGroup21"]}
nextStep={this.nextStep}
handleChange={this.handleChange}
values={values}
/>
so in your Child Component' render, take the value:
render() {
const { values, valueFromParent } = this.props;
...
3.) Radio's onChange value is passed as the second param (obj.value).
<Radio
label={'Yes'}
name={'radio21'}
value={"Yes"}
checked={valueFromParent === 'Yes'}
onChange={this.props.handleChange("radioGroup21")}
...
/>
So you can take the selected value like this:
// MainForm
handleChange = input => (event, obj) => { // obj is the second param
console.log("sendin here", input, obj.value);
this.setState({ [input]: obj.value });
};

React: Get state of children component component in parent

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.

Resources