I'm having a couple of questions with my React dropdown component. Basically, I want to get the selected object from the dropdown back to the Parent component. Right now the dropdown list is working and I seem to be able to get back a {this.state.selectedUser} to the parent from the Dropdown component. The only thing that isn't working is the default value.
From the parent, I want to pass some details from the object selected from Dropdown to a sister component, UserIcon.
First I have my dropdown which has an onChange. I was attempting to add a defaultValue to the Select to fix the issue of no default but no dice so far.
import React from 'react';
class PolicyDropdown extends React.Component {
render() {
let initialUsers = this.props.state.users;
let alphabetizeUsers = initialUsers
.sort((a, b) => {
return a.name > b.name;
})
.map(obj => {
return (
<option key={obj.id} value={obj.name}>
{obj.name}
</option>
);
});
return (
<select
onChange={e => this.props.onChange(e.target.value)}
>
{alphabetizeUsers}
</select>
);
}
}
export default UserDropdown;
Then I have a middle component that makes the api call .
import UserDropdown from './UserDropdown';
class UserHeader extends React.Component {
state = {
users: []
};
componentDidMount() {
let initialUsers = [];
fetch('http://localhost:3000/users')
.then(response => {
return response.json();
})
.then(data => {
this.setState({ users: data });
});
}
render() {
return (
<UserDropdown state={this.state} onChange={this.props.onChangePolicy} />
);
}
}
export default UserHeader;
And finally the main component where I should be handling the data, and passing the information from the dropdown into a new component, UserIcon.
class Main extends Component {
state = {
selectedUser: this.props.user
};
onChangeUser = user => {
this.setState({ selectedUser: user });
};
render() {
return (
<div className="App">
<header className="App-header">
<span className="App-title">
SELECT A POLICY:{' '}
<UserHeader onChangeUser={this.onChangeUser} />
</span>
<br /> <br />
<UserIcon onChangeUser={this.onChangeUser} />
Dropdown value here: {this.state.selectedUser}
</header>
</div>
);
}
}
export default Main;
Where the value is displayed as this.state.selectedUser. When I changed the value to just obj, I tried doing this.state.selectedUser.name but the console complained Uncaught TypeError: Cannot read property 'name' of undefined
. I want to pass the id as a prop to UserIcon component for it to use as a variable.
You have to pass the user object back to Main via a callback. This is a simplified version of what that would look like:
const UserIcon = props => <div>{props.user.name}</div>;
class UserDropdown extends React.Component {
render() {
let initialUsers = this.props.state.users;
let alphabetizeUsers = initialUsers
.sort((a, b) => {
return a.name > b.name;
})
.map(obj => {
return (
<option key={obj.id} value={obj.id}>
{obj.name}
</option>
);
});
return (
<select
onChange={e => this.props.onChange(e.target.value)}
>
{alphabetizeUsers}
</select>
);
}
}
class UserHeader extends React.Component {
state = {
users: [
{name: "thompson", id: 1},
{name: "anderson", id: 2}
]
};
handleChange = selectedId => {
const selectedUser = this.state.users.find( el => el.id === selectedId);
this.props.onChange(selectedUser);
}
render() {
return (
<UserDropdown state={this.state} onChange={this.handleChange} />
);
}
}
class App extends React.Component {
state = {
selectedUser: {
name: "",
id: ""
}
};
onChangeUser = user => {
this.setState({ selectedUser: user });
};
render() {
return (
<div className="App">
<header className="App-header">
<span className="App-title">
SELECT A POLICY:{' '}
<UserHeader onChange={this.onChangeUser} />
</span>
<br /> <br />
<UserIcon user={this.state.selectedUser} />
Dropdown value here: {JSON.stringify(this.state)}
</header>
</div>
);
}
}
Related
I'm just starting out with React, adapting the tic tac toe tutorial for my case.
I'm trying to click on the grandchild component to change the state of the grandparent component . Code is as follows:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
fields: [
{
id: 1,
show: false
},
{
id: 2,
show: false
}
]
}
}
handleClick(i) {
const fields = this.state.fields.slice();
fields[i].show = true;
this.setState({fields: fields});
}
render() {return <Preview />}
}
const Preview = (props) => {
return (
<div className="preview">
{props.fields.map((field) => (
<Field data={field} key={field.id} onClick={ props.onClick(field.id) }/>
))}
</div>
);
};
const Field = props => {
return (
<div className="field" onClick={ props.onClick } />
);
};
I get a TypeError: Cannot read property 'state' of undefined from this line:
handleClick(i) {
const fields = this.state.fields.slice();
Issues
this of the App class isn't bound to the handleClick function. This is cause of TypeError: Cannot read property 'state' of undefined error.
You are mutating your state object. Slicing the array creates a new array reference, but fields[i].show = true; mutates the object reference in state.
You don't pass fields or onClick props to Preview.
The onClick callback isn't called correctly in Preview.
Solution
Bind this to the handler or convert to arrow function so it is automatically bound.
constructor(props){
...
this.handleClick = this.handleClick.bind(this);
}
or
handleClick = (i) => { ..... };
DON'T MUTATE STATE. Shallow copy state then update properties.
handleClick = (id) => {
this.setState(prevState => ({
fields: prevState.fields.map((field) => {
return field.id === id ? {
...field,
show: true,
} : field;
}),
}));
};
Pass fields and handleClick as onClick to Preview.
render() {
return (
<Preview
fields={this.state.fields}
onClick={this.handleClick}
/>
);
}
Call props.onClick correctly with the id.
{props.fields.map((field) => (
<Field
data={field}
key={field.id}
onClick={() => props.onClick(field.id)}
/>
))}
I've added some explanations, check the comments
// [...]
render() {
// Here you need to pass "fields" and "handleClick" as props:
return <Preview fields={this.state.fields} onClickField={this.handleClick} />
}
}
const Preview = (props) => {
// Here you get the props:
const { fields, onClickField } = props;
// Your onclick was a function call instead of just a function
return (
<div className="preview">
{fields.map((field) => (
<Field
data={field}
key={field.id}
onClick={() => onClickField(field.id) }
/>
))}
</div>
);
};
const Field = props => {
return (
<div className="field" onClick={ props.onClick } />
);
};
I've created a React app for a school project that can add multiple types of input fields to a view by clicking a button (sort of like Wordpress Gutenberg).
Currently, I can add one of each type of item onto the view. However, if I click the button again, it erases the current text that was added. I'd like the ability to click the button to add as many fields as I'd like on click.
Also, the items are only added into the view in the order they were created meaning, even if I choose photo first and I click headline after, it (headline) will appear at the top of the list above the initial item.
I've had a look at these solutions (which were pretty good) but they didn't provide what I need.
Dynamically adding Input form field issue reactjs
and "update delete list elements using unique key": https://www.youtube.com/watch?v=tJYBMSuOX3s
which was closer to what I needed to do.
Apologies in advance for the length of the code,(there are two other related components for text input and an editform). I'm sure there is a much more simple way to do this. I haven't been able to find an npm package or solution to this specific problem online and am open to a simpler solution.
Edit.jsx
export default class Edit extends React.Component {
state = {
texts: {
hl: '',
shl: '',
txt: '',
photo: []
},
coms: {
hl: false,
shl: false,
txt: false,
photo: null
},
labels: {
// Replace with icons
hl: 'Headline',
shl: 'Sub',
txt: 'Text Area',
photo: 'Photo'
},
selectedItem: '',
}
componentDidMount() {
const saveData = localStorage.getItem('saveData') === 'true';
const user = saveData ? localStorage.getItem('user') : '';
this.setState({ user, saveData });
}
createPage = async () => {
await this.props.postPage(this.state.texts)
}
// add options
addOptions = (item) => {
const { coms } = this.state
coms[item] = !coms[item]
this.setState({ coms: coms })
}
// ADD TEXT
addTxt = () => {
this.setState({ texts: [...this.state.texts, ""] })
}
enableAllButtons = () => {
this.setState({ selectedItem: '' })
}
handleChange = (e, index) => {
this.state.texts[index] = e.target.value
//set the changed state.
this.setState({ texts: this.state.texts })
}
setDisable = (selectedItem) => {
this.setState({ selectedItem })
}
handleRemove = () => {
// this.state.texts.splice(index, 1)
this.setState({ texts: this.state.texts })
}
handleSubmit = (e) => {
console.log(this.state, 'all text')
}
handleChange = (e, item) => {
let { texts } = this.state
texts[item] = e.target.value
//set the changed state.
this.setState({ texts })
console.log(texts)
}
render() {
const { coms, labels, selectedItem, texts } = this.state
let buttons = Object.keys(coms)
let showItems = Object.keys(coms).filter(key => coms[key] === true)
return (
<div>
<InnerHeader />
{/* Make a route for edit here */}
<Route path='/edit/form' render={() => (
<EditForm
texts={texts}
coms={coms}
labels={labels}
addOptions={this.addOptions}
setDisable={this.setDisable}
selectedItem={selectedItem}
showItems={showItems}
handleChange={this.handleChange}
enableAllButtons={this.enableAllButtons}
/>
)} />
{/* Make route for preview */}
<Route path='/edit/preview' render={(props) => (
<Preview
{...props}
createPage={this.createPage}
/>
)}
/>
</div>
)
}
}
AddText.jsx:
export default class AddText extends Component {
state = {
}
// ADD TEXT
addTxt(item) {
const {
addOptions } = this.props
addOptions(item)
}
render() {
const { coms, labels } = this.props
const { selectedItem } = this.props
let buttons = Object.keys(coms)
console.log('here', selectedItem)
return (
<div>
<Card>
<Card.Body>
{
buttons.map((item, index) => <button
value={(selectedItem === "") ? false : (selectedItem === item) ? false : true} key={index} onClick={() => this.addTxt(item)}>
{labels[item]}
</button>
)
}
</Card.Body>
</Card>
</div>
)
}
}
EditForm.jsx
export default function EditForm(props) {
return (
<div>
<div className='some-page-wrapper-sm'>
<div className="dash-card-sm">
<button><Link to={{
pathname: '/edit/preview',
item: props.texts
}}>preview</Link></button>
<br />
<br />
<AddText
coms={props.coms}
labels={props.labels}
addOptions={props.addOptions}
setDisable={props.setDisable}
selectedItem={props.selectedItem}
/>
<div>
{
props.showItems.map((item, index) => {
return (
<InputFieldComponent
// setDisable={props.setDisable}
onChangeText={(e) => props.handleChange(e, item)}
enableAllButtons={props.enableAllButtons}
key={index}
item={item}
labels={props.labels}
texts={props.texts}
/>
)
})
}
</div>
</div>
</div>
</div>
)
}
InputFieldComponent.jsx
export default class InputFieldComponent extends React.Component {
setWrapperRef = (node) => {
this.wrapperRef = node;
}
render() {
const { labels, item, onChangeText, texts } = this.props
return (
<div>
<textarea
className="txt-box"
ref={this.setWrapperRef}
onChange={onChangeText}
placeholder={labels[item]}
value={texts[item]} />
</div>
)
}
}
I'm making a React app using openweathermap API. Right now I receive the list of weather data. I'm trying to highlight the weather if I click it.
To make this happen, I wrote on App.js to pass a prop to WeatherDetail.js, but so far seems like WeatherDetail.js doesn't recognize props from its parent.
class App extends React.Component {
constructor(props) {
super(props);
}
state = { forecasts: [], selectedWeather: null }
getWeather = async city => {
const response = await weather.get('/forecast', {
params: {
q: city
}
});
this.setState ({
forecasts: response.data.list,
city: response.data.city.name,
selectedWeather: response.data.list[0]
})
}
}
onWeatherSelectFunction = (item) => {
this.setState({ selectedWeather: item });
};
render() {
return (
<div>
<Form loadWeather={this.getWeather} />
<WeatherDetail itemToChild={this.state.selectedWeather} />
<WeatherList
onWeatherSelect={this.onWeatherSelectFunction}
weathers={this.state.forecasts}
city={this.state.city}
/>
</div>
);
}
}
export default App;
const WeatherDetail = ({forecasts, itemToChild}, props) => {
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
const WeatherItem = ({item, onWeatherSelectFromList, humidity, city, temp }) => {
return (
<div>
<div onClick={() => onWeatherSelectFromList(item)} >
{city}<br /> <-- Appears on screen
{humidity}<br /> <-- Appears on screen
</div>
</div>
);
};
const WeatherList = ({weathers, onWeatherSelect, city}) => {
const renderedList = weathers.map((item) => {
return (
<div>
<WeatherItem
city={city}
temp={item.main.temp}
humidity={item.main.humidity}
temperature={item.weather.icon}
onWeatherSelectFromList={onWeatherSelect}
/>
</div>
);
});
return (
<div className="flex">
{renderedList}
</div>
);
}
class Form extends React.Component {
state = { term: '' };
onFormSubmit = (event) => {
event.preventDefault();
this.props.loadWeather(this.state.term);
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<input
ref="textInput"
type="text"
value={this.state.term}
onChange={event => this.setState({term: event.target.value})}
/>
<button>Get Weather</button>
</form>
</div>
);
}
}
How do I connect App.js and WeatherDetail.js using props?
In your App.js file you are passing only one props called itemToChild
<WeatherDetail itemToChild={this.state.selectedWeather} />
In your WeatherDetail file from where you're getting forecasts? do you get forecasts from redux store?
const WeatherDetail = ({forecasts, itemToChild}, props) => {
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
change your code with this.
const WeatherDetail = (props) => {
console.log("props.itemToChild", props.itemToChild) // check this console that do you get data as you want.
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
You have already destructured the props so there is no need to mention props in WeatherDetail component
and also there is an extra parenthesis after the return statement you should remove that also...
Old:
const WeatherDetail = ({forecasts, itemToChild}, props) => {
const weather = props.itemToChild;
if(!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div> <-- This doesn't appear on screen
);
}
New:
const WeatherDetail = ({ forecasts, itemToChild }) => {
const weather = itemToChild;
if (!weather) {
return <div>Loading...</div>;
}
return <div>{weather.humidity}</div>;
};
componentDidMount() {
const restaurants = Restaurant.all()
restaurants.then( rests => {
this.setState({
restaurants: rests
})
})
}
render() {
const { restaurants } = this.state;
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
restaurants.map( restaurant => {
return restaurant ?
( <option key={restaurant.id}>{ restaurant.name }</option> )
:
''
})
}
</Chosen>
</main>
);
}
I have my react code above and trying to return a mapped array that is supposed to be something like
[<option key={1}>first</option>, <option key={2}>two</option>, <option key={3}>three</option>]
It works if I put a randomly created array like this,
render() {
const { restaurants } = this.state;
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
[<option key={1}>first</option>, <option key={2}>two</option>, <option key={3}>three</option>]
}
</Chosen>
</main>
);
}
but no matter what I do with the map method, it just doesn't show anything.
I have already checked there is an array containing elements assigned to this.state.restaurant.
componentDidMount is called after the first render. Consequently your restaurants is undefined when the first render processed.
You can check if restaurants exists in render method:
componentDidMount() {
const restaurants = Restaurant.all()
restaurants.then( rests => {
this.setState({
restaurants: rests
})
})
}
render() {
const { restaurants } = this.state;
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
restaurants && restaurants.map( restaurant => {
return restaurant ?
( <option key={restaurant.id}>{ restaurant.name }</option> )
:
null
})
}
</Chosen>
</main>
);
}
Also, check if your state is defined in the constructor or as the class property.
So the whole component could be the follow:
class Rests extends React.Component {
state = {restaurants: null};
componentDidMount() {
const restaurants = Restaurant.all()
restaurants.then( rests => {
this.setState({
restaurants: rests
})
})
}
render() {
const { restaurants } = this.state;
if (!restaurants) {
return null; // or you can return <LoadingSpinner /> here
}
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
restaurants.map( restaurant => {
return restaurant ?
( <option key={restaurant.id}>{ restaurant.name }</option> )
:
null
})
}
</Chosen>
</main>
);
}
}
In the last example, we render nothing if there is no any data in restaurants. After we fetch data we rerender component and show options to users
The issue might be where you declared the restaurent constant from the state. I've written a sample code that works below.
import React from 'react';
const restaurentData = [
{
id: 1,
name: 'name 1'
},
{
id: 2,
name: 'name 2'
},
{
id: 3,
name: 'name 3'
}
]
class Hello extends React.Component {
constructor() {
super();
this.state = {
restaurents: restaurentData
}
}
render () {
const restaurents = this.state.restaurents;
return (
<ul>
{restaurents.map(restaurent => {
return restaurent
? (<li key={restaurent.id}>{`${restaurent.id} -- ${restaurent.name}`} </li>)
: null;
})}
</ul>
)
}
}
export default Hello;
I am struggling with successfully removing component on clicking in button. I found similar topics on the internet however, most of them describe how to do it if everything is rendered in the same component. In my case I fire the function to delete in the child component and pass this information to parent so the state can be changed. However I have no idea how to lift up the index of particular component and this is causing a problem - I believe.
There is a code
PARENT COMPONENT
export class BroadcastForm extends React.Component {
constructor (props) {
super(props)
this.state = {
numberOfComponents: [],
textMessage: ''
}
this.UnmountComponent = this.UnmountComponent.bind(this)
this.MountComponent = this.MountComponent.bind(this)
this.handleTextChange = this.handleTextChange.bind(this)
}
MountComponent () {
const numberOfComponents = this.state.numberOfComponents
this.setState({
numberOfComponents: numberOfComponents.concat(
<BroadcastTextMessageForm key={numberOfComponents.length} selectedFanpage={this.props.selectedFanpage}
components={this.state.numberOfComponents}
onTextChange={this.handleTextChange} dismissComponent={this.UnmountComponent} />)
})
}
UnmountComponent (index) {
this.setState({
numberOfComponents: this.state.numberOfComponents.filter(function (e, i) {
return i !== index
})
})
}
handleTextChange (textMessage) {
this.setState({textMessage})
}
render () {
console.log(this.state)
let components = this.state.numberOfComponents
for (let i = 0; i < components; i++) {
components.push(<BroadcastTextMessageForm key={i} />)
}
return (
<div>
<BroadcastPreferencesForm selectedFanpage={this.props.selectedFanpage}
addComponent={this.MountComponent}
textMessage={this.state.textMessage} />
{this.state.numberOfComponents.map(function (component) {
return component
})}
</div>
)
}
}
export default withRouter(createContainer(props => ({
...props
}), BroadcastForm))
CHILD COMPONENT
import React from 'react'
import { createContainer } from 'react-meteor-data'
import { withRouter } from 'react-router'
import { BroadcastFormSceleton } from './BroadcastForm'
import './BroadcastTextMessageForm.scss'
export class BroadcastTextMessageForm extends React.Component {
constructor (props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.unmountComponent = this.unmountComponent.bind(this)
}
handleChange (e) {
this.props.onTextChange(e.target.value)
}
unmountComponent (id) {
this.props.dismissComponent(id)
}
render () {
console.log(this.props, this.state)
const textMessage = this.props.textMessage
return (
<BroadcastFormSceleton>
<div className='textarea-container p-3'>
<textarea id='broadcast-message' className='form-control' value={textMessage}
onChange={this.handleChange} />
</div>
<div className='float-right'>
<button type='button'
onClick={this.unmountComponent}
className='btn btn-danger btn-outline-danger button-danger btn-small mr-3 mt-3'>
DELETE
</button>
</div>
</BroadcastFormSceleton>
)
}
}
export default withRouter(createContainer(props => ({
...props
}), BroadcastTextMessageForm))
I am having problem with access correct component and delete it by changing state. Any thoughts how to achieve it?
Please fix the following issues in your code.
Do not mutate the state of the component. Use setState to immutably change the state.
Do not use array index as the key for your component. Try to use an id field which is unique for the component. This will also help with identifying the component that you would need to unmount.
Try something like this. As mentioned before, you don't want to use array index as the key.
class ParentComponent extends React.Component {
constructor() {
this.state = {
// keep your data in state, as a plain object
textMessages: [
{
message: 'hello',
id: '2342334',
},
{
message: 'goodbye!',
id: '1254534',
},
]
};
this.handleDeleteMessage = this.handleDeleteMessage.bind(this);
}
handleDeleteMessage(messageId) {
// filter by Id, not index
this.setState({
textMessages: this.state.textMessages.filter(message => message.id !== messageId)
})
}
render() {
return (
<div>
{this.state.textMessages.map(message => (
// Use id for key. If your data doesn't come with unique ids, generate them.
<ChildComponent
key={message.id}
message={message}
handleDeleteMessage={this.handleDeleteMessage}
/>
))}
</div>
)
}
}
function ChildComponent({message, handleDeleteMessage}) {
function handleClick() {
handleDeleteMessage(message.id)
}
return (
<div>
{message.message}
<button
onClick={handleClick}
>
Delete
</button>
</div>
);
}