I'm creating a sample react application for learning purpose, in that I've following hierarchy of separate components:
<RadioButton>
//Radio buttons are rendered here and selection of radio buttons are maintained in state of this component
</RadioButton>
<SelectCard>
<RadioButton dataToPopulate = ['choice A', 'choice B'] />
</SelectCard>
<ParentSelectCard>
<SelectCard /> //Rendering SelectCard with passing some data as prop from here
</ParentSelectCard>
<Button /> //Custom button component
<HomeScreen>
<ParentSelectCard />
<Button />
</HomeScreen>
Now when I press the button, I want to navigate to other screen by passing the selected options in radio buttons.
I've read this article about lifting state up. But the problem is, here there is no common parent ancestor to which I can lift the state to.
If I list the state up to <HomeScreen> component, How can I manage the selections made in <RadioButton> component?
Here is the complete code of <RadioButton> component:
class RadioButton extends React.Component {
constructor(props) {
super(props);
this.state = {
radioSelected: 0
}
}
handleRadioClick(id) {
this.setState({
radioSelected: id
});
}
render(){
return this.props.dataToPopulate.map((element) => {
return (
<View key = {element.id} style={styles.radioButton}>
<TouchableOpacity style={styles.radioButtonTint} onPress = {this.handleRadioClick.bind(this, element.id)}>
{ element.id === this.state.radioSelected ? (<View style={styles.radioButtonSelected}/>) : (null) }
</TouchableOpacity>
<Text style={styles.radioButtonText}> {element.value} </Text>
</View>
);
});
}
}
Here you can see that the final choice made will be stored in the state of this component (in radioSelected).
What I'm missing here? Is my design of <RadioButton> wrong?
What I'm missing here? Is my design of is wrong?
Well if you "lift state up" the radio button itself should not have any state at all. Instead pass down a handler to the RadioButton:
<RadioButton onChange={value => this.doSomethingWith(value)} />
Then you can call that inside of the radio button whenevver something was changed, and handle the results in <App/>.
If you have to pass down that handler through multiple levels it might be better to use a context.
Related
I'm new to React. I have written a simple React application using Semantic UI, with two menu items, each corresponding to a specific page. I'm not sure how to make each page have its own state, and remember it when navigating the menu.
In index.js:
ReactDOM.render(
<Main />,
document.getElementById('root')
);
In Main.jsx:
export default class Main extends Component {
state = {
activePageId: 1
};
handleSelectPage = (e, { id: selectedPageId }) => {
this.setState({ activePageId: selectedPageId })
}
getActivePage() {
return (
<Page id={this.state.activePageId} />
)
}
render () {
return (
<Grid divided padded >
<Grid.Column width={2}>
<Menu vertical inverted pointing fluid>
<Menu.Item
id={1}
name='Page 1'
active={this.state.activePageId === 1}
onClick={this.handleSelectPage}
key={1}
/>
<Menu.Item
id={2}
name='Page 2'
active={this.state.activePageId === 2}
onClick={this.handleSelectPage}
key={2}
/>
</Menu>
</Grid.Column>
<Grid.Column width={14}>
{this.getActivePage()}
</Grid.Column>
</Grid>
);
}
}
Finally, in Page.jsx:
export default class Page extends Component {
state = {
counter: 0
};
increaseCounter = (e) => {
this.setState({ counter: this.state.counter + 1 });
}
render () {
return (
<React.Fragment>
<Header>Page {this.props.id}</Header>
{this.state.counter}
<Button primary content='+' onClick={this.increaseCounter}/>
</React.Fragment>
);
}
}
What I can't figure out:
The page id is passed in the props when creating a Page component in getActivePage(). So, that changes every time I navigate the menu. However, the counter is in the state of the Page. That does not change as I navigate the menu. Am I correct to assume there is only one instance of a Page, which is re-rendered when the props (in this case id) change?
It seems like the only way to have a separate counter for each page is to have some sort global state (in a global object) and read from that when rendering a Page. This would mean, however, that I need to call this.forceUpdate inside Page.increaseCounter to re-render the page without re-clicking the menu item. Is that the React way to go?
I'm also thinking about using routing. But, from my preliminary experiments, it seems that, compared to the current scenario, a new Page would be created for each route (is this right?) with its own state. However, that state gets wiped out when navigating between routes, so I still need to keep a global state object and use forceUpdates or so. Any ideas here?
Thanks!
You have to set the counter state inside your Main.jsx component, and then pass the counter state as props to Page.jsx. I recommend you to use functional components and useState hook. Here you can see an example
https://codesandbox.io/s/recursing-bardeen-8w58z?file=/src/App.js
I have a signup page that toggles between two forms depending on bool value in this.state. If bool == true it shows a personal sign up form and if it == false it shows a business sign up form. Only one form can be visible at once (I have it working up to here). My confusion arises when trying to preserve the state of the signup forms regardless of the bool value. i.e. If you start filling out personal form and switch to business form, I don't want to lose those entries. I see the solution to this being store the form progress in the signup page's state.
I'm attempting to lift the state up from each *SignUpForm component and track them in the two child objects in this.state, personalForm & businessForm. However, whenever I enter text, for example, the first name field TextInput I receive an error stating:
TypeError: undefined is not an object (evaluating 'form.first')
So naturally I started placing console.log statements in the lifting up workflow and this is what is printed after I try to start entering data into the form:
Handling internal personal form change!
Lifting up:
undefined
Handling personal change in SignUp page
Existing personal form:
Object {
"email": "",
"first": "",
"last": "",
}
Updating personal form to:
undefined
Completed update, new personal form:
Object {
"email": "",
"first": "",
"last": "",
}
undefined
Rendering personal form!
undefined
Rendering personal form!
undefined
As you can see, the data I'm trying to lift up is undefined. I started messing around with which component has the onChange method in PersonalSignUpForm. Right now it's a prop of the root View component, which of course, is not working. I tried placing it in the actual TextInput itself and that still yielded the same undefined error.
So, in summary, I assume my state lifting is failing because my *SignUpForm components are more than a single component/complex and React can't deduce what e.target.value is. How do I change my lifting up code to properly capture all changes to TextInput and store them in SignUp.js's state?
SignUp.js
export default class SignUpScreen extends React.Component {
constructor(props){
super(props);
this._handlePersonalAccountPress = this._handlePersonalAccountPress.bind(this);
this._handleBusinessAccountPress = this._handleBusinessAccountPress.bind(this);
this._handlePersonalFormChange = this._handlePersonalFormChange.bind(this);
this._handleBusinessFormChange = this._handleBusinessFormChange.bind(this);
this.state = {
personalAccount: true,
personalForm: {
first: '',
last: '',
email: ''
},
businessForm: {
company: '',
email: ''
}
};
}
_handlePersonalAccountPress() {
this.setState({personalAccount: true})
}
_handleBusinessAccountPress() {
this.setState({personalAccount: false})
}
_handlePersonalFormChange(newForm) {
console.log('Handling personal change in SignUp page')
console.log('Existing personal form:')
console.log(this.state.personalForm)
console.log('Updating personal form to:')
console.log(newForm)
this.setState({personalForm: newForm})
console.log('Completed update, new personal form:')
console.log(this.state.personalForm)
}
render(){
if (this.state.personalAccount)
{
console.log(this.state.personalForm)
form = <PersonalSignUpForm
_onFormChange={this._handlePersonalFormChange}
form={this.state.personalForm}/>;
}
else
{
form = <BusinessSignUpForm
_onFormChange={this._handleBusinessFormChange}
form={this.state.businessForm}/>;
}
return (
<KeyboardAvoidingView behavior="padding">
<StatusBar barStyle="dark-content" />
<View>
<AccountTypeButton
text='Personal'
onAccountChange={this._handlePersonalAccountPress}
selected={this.state.personalAccount}/>
<AccountTypeButton
text='Business'
onAccountChange={this._handleBusinessAccountPress}
selected={!this.state.personalAccount}/>
</View>
{form}
<View>
<TouchableOpacity
onPress={() => this._onCreateAccountPress()}>
<Text>Create Account</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
}
PersonalSignUpForm.js
BusinessSignUpForm.js is the same just different TextInput
export default class PersonalSignUpForm extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange (e) {
console.log('Handling internal personal form change!')
console.log('Lifting up:')
console.log(e.target.value)
this.props._onFormChange(e.target.value);
}
render() {
console.log('Rendering personal form!')
console.log(this.props.form)
const form = this.props.form
return (
<View style={styles.forms}
onChange={this.handleChange}>
<View style={styles.textInputBorder}>
<TextInput style={styles.textInput}
value={form.first}
editable
placeholder='first name'/>
</View>
<View style={styles.textInputBorder}>
<TextInput style={styles.textInput}
value={form.last}
editable
placeholder='last name'/>
</View>
<View style={styles.textInputBorder}>
<TextInput style={styles.textInput}
value={form.email}
editable
placeholder='email'
keyboardType='email-address'/>
</View>
</View>
);
}
}
Yes lifting state up is best practice and honestly the only way to achieve clean data flow across component hierarchy. BUT, what i'm saying is your trying to modify the state of the parent component directly within the child component by referencing the value passed from props.
You should be trying to update the state of the parent component by values changed within the child component by passing down a function as a prop to the child and calling that fn as shown in the example below.
https://codesandbox.io/embed/clever-mirzakhani-8v4ly?fontsize=14
Let me know if you need any further help.
I have created a custom component which takes a color name from the parent component and updates that color in the state in the parent component. Currently, after I have done all the code, it does not save the new color, and therefore, does not update the the state.
This is for a react-native android app that I am building. I have looked at the ReactNative documentation for flatlist and textinput. I have looked at Stack overflow for solutions too
Set up a react native project. this is my parent component
class HomePage extends Component {
constructor(props) {
super(props);
this.state = {
backgroundColor: "blue",
availableColors: [
{name: 'red'}
]
}
this.changeColor = this.changeColor.bind(this)
this.newColor = this.newColor.bind(this)
}
changeColor(backgroundColor){
this.setState({
backgroundColor,
})
}
newColor(color){
const availableColors = [
...this.state.availableColors,
color
]
this.setState({
availableColors
})
}
renderHeader = ()=>{
return(
<ColorForm onNewColor={this.newColor} />
)
}
render() {
const { container, row, sample, text, button } = style
const { backgroundColor, availableColors } = this.state
return (
<View style={[container,{backgroundColor}, {flex: 1}]} >
<FlatList
data={availableColors}
renderItem={
({item}) =>
<ColorButton
backgroundColor={item.name}
onSelect={(color)=>{this.changeColor(color)}}>
{item.name}
</ColorButton>}
keyExtractor={(item, index) => index.toString()}
ListHeaderComponent={this.renderHeader}
>
</FlatList>
</View>
);
}
}
this is the code for ColorForm component
class ColorForm extends Component {
constructor(props) {
super(props);
this.state = {
txtColor:'',
}
this.submit = this.submit.bind(this)
}
submit() {
this.props.onNewColor(this.state.txtColor.toLowerCase())
this.setState({
txtColor: 'yellow',
})
}
render() {
const {container, txtInput, button} = style
return (
<View style={container}>
<TextInput style={txtInput}
placeholder="Enter a color"
onChangeText={(txtColor)=>this.setState({txtColor})}
value={this.state.txtColor}></TextInput>
<Text
style={button}
onPress={this.submit}>Add</Text>
</View> );
}
}
and below is the code for ColorButton component
export default ({backgroundColor, onSelect=f=>f}) => {
const {button, row, sample, text} = style
return (
<TouchableHighlight onPress={()=>{onSelect(backgroundColor)}} underlayColor="orange" style={button}>
<View style={row}>
<View style={[sample,{backgroundColor}]}></View>
<Text style={text}>{backgroundColor}</Text>
</View>
</TouchableHighlight>
)
}
The imports and stylesheets are setup as standard and do not effect the code so I have chosen to not show them.
EDIT: Adding the expo snack here.
Expected Behavior:
When I press "ADD" on the ColorForm component, it should take that color and add that to the this.state.availableColor array and therefore visible in the ColorButton component. And when I touch the button, it should make that change
Current behaviour:
When I enter a color and press on add, it makes an empty button in the ColorButton component - NOT the color i entered in the color I entered in the ColorForm component.
EDIT: Adding the expo snack here.
Your state is updating but the FlatList is not updating. Because your data={availableColors} in flatlist is not changing but your state is changing .
Try to add extraData
A marker property for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
Try this
<FlatList
extraData={this.state.backgroundColor}
Updated Answer
the problem is in this function newColor(color)
const availableColors = [
...this.state.availableColors,
color
]
you just receive a string of color but you have defined object like this {name: 'red'}
please use this code
newColor(color){
const availableColors = [
...this.state.availableColors,
{name: color}
]
this.setState({
availableColors
})
}
Snack link with example : https://snack.expo.io/#mehran.khan/healthy-cake-state-sample
Also add export default to main component to remove error of launch
export default class HomePage extends Component {
App Preview
I had many problems using setState() in the way you are using now. I recommend you to use in this way setState(), with a callback:
this.setState((previousState, currentProps) => {
return { ...previousState, foo: currentProps.bar };
});
This is one of the article that talks about it.
setState() does not always immediately update the component. It may
batch or defer the update until later.
From react website setState().
I'm new to react native.
My screen contains 5 buttons, each one opens the same <Modal>, but the <View> inside it will change depending on the button clicked.
If I click the first button, a text input will be shown into the modal.
If I click the second button, a switch will be shown into the modal.
I've made a modal component (Modal.tsx) :
export default class Modal extends Component {
constructor(props) {
super(props)
}
public render() {
return (
<View style={style.modal} >
{this.props.children}
<View>
)
};
}
// Specific modal implementation with TextInput
const ModalWithTextInput = props => (
<Modal>
<TextInput
value={props.someValue}
/>
<Modal>
)
// Specific modal implementation with Switch
const ModalWithSwitch = props => (
<Modal>
<Switch
value={props.someValue}
/>
<Modal>
)
And now in my 5-button-screen (ButtonsScreen.tsx), I open the right modal depending on the button clicked :
openTextModal = () => {
this.setState({ modalType: 'text' });
}
openSwitchModal = () => {
this.setState({ modalType: 'switch' });
}
These functions are called with, for example, onPress={this.openTextModal}
Finally, I render the modal, to be able to do something like :
<View>
{this.renderModal(modalType)}
</View>
As this :
renderModal = (type) => {
if (type === 'text') {
return <ModalWithTextInput someValue="default text" />
}
if (type === 'switch') {
return <ModalWithSwitch someValue={false}/>
}
}
When I try to open a modal with onPress={this.openTextModal}, nothing happens (no error, no warning).
Anyone can please help ? Thanks.
You need to extract modalType from state, in the render method of your component that displays the Modal.
Clicking the button, only set's state, you need to handle state in the component in order to trigger a refresh. A refresh of the render method will render your Modal changes; React 101.
render() {
const { modalType } = this.state;
return (
<View>
{this.renderModal(modalType)}
</View>
);
}
Based on the fact that this is pretty much my code from your other question. I strongly suggest you take a step back, learn the basic's of React rather than just asking people to piece together solutions which you do not understand. Otherwise the result is you learn very little and have code that you do not understand.
I created a conditional field which displays yes and no radio buttons. If Yes is selected then the child components should be shown.
The following code accomplishes that. The issue is the selection of yes or no is not registered in the redux state. If I remove the onChange function then the redux state is updated with the Yes or No value, but of course the child components won't show.
I believe the onChange function I pass is overwriting some other onChange function passed by redux-form. Tried many things but had the same result.
I was thinking of just linking the value property with ReactLink, but it's deprecated.
Using React 0.15, Redux-Form 6.0 alpha, and ES7.
const YesNoRadioButtonGroup = (props) =>
<RadioButtonGroup {...props}>
<RadioButton value='Yes' label='Yes' className={s.radio}/>
<RadioButton value='No' label='No' className={s.radio}/>
</RadioButtonGroup>
// TODO: Clear child fields when "No" is selected
// TODO: See if we can generalize into ConditionalField
export class ConditionalRadio extends React.Component {
state = {conditional: false}
updateConditional(event) {
console.log(event)
this.setState({conditional: event.target.value === 'Yes'})
}
render() {
return <div>
<Field name={this.props.name}
component={YesNoRadioButtonGroup}
onChange={::this.updateConditional} /> // The trouble line.
{this.state.conditional ? this.props.children : null}
</div>
}
}
It is used like this:
<ConditionalRadio name='willRelocate'>
<Field name='willRelocateTo.withinCurrentState' component={Checkbox} label='Within Current State'/>
<Field name='willRelocateTo.outOfState' component={Checkbox} label='Out of State'/>
<Field name='willRelocateTo.outOfCountry' component={Checkbox} label='Out of Country'/>
</ConditionalRadio>
If you have defined the field name when creating your redux-form, then you just have to call the default onChange event for that field inside your custom change event handler.
In your case that should be:
updateConditional(event) {
this.setState({conditional: event.target.value === 'Yes'});
this.props.fields.name.onChange(event);
}
Did you try to use the function componentWillReceiveProps in which you can check the new value then set the new conditional? see all helpful React lifecycle functions here
Your Component would be written like this:
export class ConditionalRadio extends React.Component {
state = {conditional: false}
componentWillReceiveProps(nextProps) {
const displayChildren = nextProps.**value of the radio from redux form STORE** === 'Yes'
this.setState({conditional: displayChildren});
}
render() {
return (
<div>
<Field name={this.props.name}
component={YesNoRadioButtonGroup}/>
{this.state.conditional ? this.props.children : null}
</div>
)
}
}
This works well:
class YesNoRadioButtonGroup extends React.Component {
handleChange(event) {
// Call the event supplied by redux-form.
this.props.onChange(event)
// If custom handler exists, call it.
if (this.props.hasOwnProperty('customHandler')) {
this.props.customHandler(event)
}
}
render() {
return <RadioButtonGroup {...this.props} onChange={::this.handleChange}>
<RadioButton value='Yes' label='Yes' className={s.radio}/>
<RadioButton value='No' label='No' className={s.radio}/>
</RadioButtonGroup>
}
}