Get child component state in parent component - reactjs

I'm making a contact list where you can add contacts to your favorites. Then filter my favorite contacts.
First all contacts have the state isFavorite: false, then I click on one contact, click on the star that sets isFavorite: true. I close that contact and click on the filter button, to see all my favorite contacts
so in here I add a contact to my favorites:
ContactName.js
state = {
isFavorite: false
}
handleFavorite = () => {
this.setState({
isFavorite: !this.state.isFavorite
})
}
render() {
return (
<React.Fragment>
<li onClick={this.handleClick}>
{this.props.contact.name}
</li>
{
this.state.isOpen ?
<Contact
contact={this.props.contact}
close={this.handleClick}
favorite={this.handleFavorite}
isFavorite={this.state.isFavorite}
/>
: null
}
</React.Fragment>
)
}
Contact.js
<Favorites
id={contact.id}
name={contact.name}
onClick={this.props.favorite}
state={this.props.isFavorite}
/>
Favorites.js
this is just where the favorite component is
<span onClick={this.props.onClick}>
{
!this.props.state
? <StarBorder className="star"/>
: <Star className="star"/>
}
</span>
and here is where I want to be able to get the isFavorite state. This is the parent component where the button for filtering the contacts is.
ContactList.js
<React.Fragment>
<span
className="filter-button"
>Filtera favoriter</span>
<ul className="contacts">
{
this.props.contacts
.filter(this.handleSearchFilter(this.props.search))
.map(contact => (
<ContactName
key={contact.id}
contact={contact}
name={contact.name}
/>
))
}
</ul>
</React.Fragment>

You are doing this in the wrong direction.
In the React, you can pass the data down with props (or by using Context which is no the case here). So if you need a data on the ancestor component, the data should be state/props of that ancestor.
In your case, the favorite data should be inside of the contacts (that is defined as props of the ContactName), and you should pass it to the ContactName just like other props.
<React.Fragment>
<span
className="filter-button"
>Filtera favoriter</span>
<ul className="contacts">
{
this.props.contacts
.filter(this.handleSearchFilter(this.props.search))
.map((contact, index) => (
<ContactName
key={contact.id}
contact={contact}
name={contact.name}
isFavorite={contact.isFavorite}
handleFavorite={() => this.props.handleFavorite(index))}
/>
))
}
</ul>
</React.Fragment>
and inside your ContactName.js
render() {
return (
<React.Fragment>
<li onClick={this.handleClick}>
{this.props.contact.name}
</li>
{
this.state.isOpen ?
<Contact
contact={this.props.contact}
close={this.handleClick}
favorite={this.props.handleFavorite}
isFavorite={this.props.isFavorite}
/>
: null
}
</React.Fragment>
)
}
and toggleFavorite function also should be the same place as the contacts state is.

In React, parent components should not have access to their children's state. Instead, you need to move your isFavorite state up a level to your ContactList component and turn it into a list or map instead of a boolean.
ContactList.js
class ContactList extends React.Component {
state = {
// In this example, `favorites` is a map of contact ids.
// You could also use an array to keep track of the favorites.
favorites: {},
};
render() {
return (
<React.Fragment>
<span className="filter-button">Filter a favorite</span>
<ul className="contacts">
{this.props.contacts
.filter(this.handleSearchFilter(this.props.search))
.map(contact => (
<ContactName
key={contact.id}
contact={contact}
isFavorite={!!this.state.favorites[contact.id]}
name={contact.name}
handleFavorite={() => this.handleFavorite(contact.id)}
/>
))}
</ul>
</React.Fragment>
);
}
handleFavorite = contactId => {
// Use a callback for state here, since you're depending on the previous state.
this.setState(state => {
return {
...state.favorites,
[contactId]: !state.favorites[contactId], // Toggle the value for given contact.
};
});
};
}
Now the handleFavorite and isFavorite props can simply be passed down as needed to your child components.

okay, I've managed to get the childs state in the parent. But now everytime I add a new contact to my favorites, it creates new objects - see codebox https://codesandbox.io/embed/charming-bohr-rwd0r
Is there a way to mash all of those new created objects into one object and set that one big objects equal to a new state called favoriteContacts = []?

Related

change parent from children created with map()

I'm a react and JS newbie trying to create a bookmark-like prototype. The idea was that when user clicks a unique pin, it becomes selected (bookmarked) and the app remembers which ones are bookmarked on multiple screens of the app.
tl:dr
I need my parent component to remember which of the child components are selected
I have a JSON with 600+ entries of the following info:
export const pinData = [
{
"id": "a0b6e",
"name": "share transfer",
"type": "basic",
"isSelected" : false
},
{
"id": "f7m6z",
"name": "commute",
"type": "group",
"isSelected" : false
}
I've used map() to pass JSON data to Pin Class using map()
render() {
return (
<>
<div className="pin-container">
{pinData.map((data, key) => {
return (
<div key={data.id}>
<Pin
// onClick={() => this.handleClick(i)}
// onClick={()=>console.log(this.data.id}
key={data.id}
id={data.id}
name={data.name}
type={data.type}
isSelected={data.isSelected}
index={data.index}
/>
</div>
);
})}
</div>
</>
);
}
and created a shared state in its parent Pins with
export class Pins extends React.Component {
constructor(props) {
super(props);
this.state = {
pins: Array(60).fill(false),
};
}
The issue is that I cannot find a way to pass a function that changes the shared state for a particular index.
Rather than filling an array with false (bad for large sets of data), I'd either create an object that houses which pins have been clicked using the id as the index. IE it is initially empty then {a0b6e: true} after one click, then {b2dk4: true, a0b6e: true} after two clicks. Or you could just have an array of IDs that have been clicked. Or what you are doing will work too.
Anyways, in your parent create a function that accepts an id (or an index in your case).
function pinClicked(index) {
const newPins = [...this.state.pins];
newPins[index] = true;
this.setState({pins: newPins});
}
Then pass this to your child.
render() {
return (
<>
<div className="pin-container">
{pinData.map((data, key) => {
return (
<div key={data.id}>
<Pin
onClick={() => this.handleClick(key)}
key={data.id}
id={data.id}
name={data.name}
type={data.type}
isSelected={data.isSelected}
index={data.index}
/>
</div>
);
})}
</div>
</>
);
}
Where handleClick is the function passed to the child via a prop.
I still recommend using the ids since you have them, but at least in your case the lookup will be fast.

Button in li element to display more details

I would like to display the modal window with more details when I click on record. I'm using OfficeUI.
My parent component:
public render() {
{
return (
<div>
{this.props.items
.map((item: IListItem, i: number): JSX.Element => <ul className="list-group">
<li className="list-group-item">
<div className={styles.text}>
<p>{item.Id}</p>
</div>
<DefaultButton onClick={this.showModalEvent} text="Open Modal" />
{this.state.showPopup
? <ModalPopUpItem item={item}
showModalState={this.state.showPopup}
showModal={this.showModalEvent}/> : null
}
</li>
</ul>)}
</div>
);
}
private showModalEvent = (): void => {
this.setState({ showPopup: !this.state.showPopup });
}
My child:
export class ModalPopUpItem extends React.Component<IModalProps> {
public render() {
return (
<Modal
isOpen={this.props.showModalState}
onDismiss={this.props.showModal}
isBlocking={false}
containerClassName="ms-modalExample-container">
<div className="ms-modalExample-header">
<span>{this.props.item.date}</span>
</div>
</Modal>
);
}
}
When I click my DeafultButton on parent component it invokes and displays Modal for every item, how can I limit this to only one current clicked item. I tried with i: number, but I couldn't figure it out.
Since you are using single state for every child, so once you change this.state.showPopup to true, every modal will showed up. So you might can change the showModalEvent method.
private showModalEvent = (id: number): void => {
this.setState({ showPopupId: id });
and the render will be look like
return (
<div>
{this.props.items
.map((item: IListItem, i: number): JSX.Element => <ul className="list-group">
<li className="list-group-item">
<div className={styles.text}>
<p>{item.Id}</p>
</div>
<DefaultButton onClick={() => this.showModalEvent(item.Id)} text="Open Modal" />
{this.state.showPopupId === item.Id
? <ModalPopUpItem item={item}
showModalState={this.state.showPopup}
showModal={this.showModalEvent} /> : null
}
</li>
</ul>)}
</div>
);
Since this method only store one id at a time, this mean only one modal will showed up. If you want to show more modal at the time, you could change it to array or something.
Your parent component only has one flag to store the showPopup. This means that when you click on one of your buttons you set this flag for the whole parent component and that means that the whole list of your child components is evaluated and this.state.showPopup will be true.
You need to find a way to limit the effect of clicking the button to the item the button was clicked on.
You could, for example, not set the showPopup flag on the parent component, but on the item.
This could work, but you would have to revisit the way you include the ModalPopUpItem.
private showModalEvent = (item): void => {
item.showPopup = !item.showPopup;
}

Is there any obvious reason this won't render?

I'm pulling in an array of objects and mapping them to another component to be rendered.
renderRatings(){
if(this.props.ratings.length > 0){
return this.props.ratings.map(rating => {
<Rating
id={rating.id}
title={rating.title}
value={rating.value}
/>
});
}
}
This is where I render the rendering function.
render() {
return (
<div>
{this.renderRatings()}
</div>
);
}
}
This is the component I'm trying to populate and have rendered.
class Rating extends Component{
componentDidMount(){
console.log("props equal:", this.props)
}
render() {
return (
<div className="card darken-1" key={this.props._id}>
<div className="card-content">
<span className="card-title">{this.props.title}</span>
<p>{this.props.value}</p>
<button>Edit</button>
<button onClick={() => this.deleteRating(this.props._id)}>Delete</button>
</div>
</div>
);
}
}
export default connect({ deleteRating })(Rating);
No errors are being thrown, but when the page loads, the surrounding menu comes up, and the fetch request returns an array and supposedly maps it to the 'Rating' component, but no mapped Rating cards appear.
in your map, you're not returning the Rating etc... because you used { to define a code block, you have to type return. And since it's multi-line, use parens to mark the start and end of the Rating component.
return this.props.ratings.map(rating => {
<Rating
id={rating.id}
title={rating.title}
value={rating.value}
/>
needs to be
return this.props.ratings.map(rating => {
return (<Rating
id={rating.id}
title={rating.title}
value={rating.value}
/>)

Render Component Only Once in a Div Below an LI When In-Line Button is Clicked

Currently, this will render a component below each of the list items when the img is clicked by keeping an array of shown components per index in local state. Eg. (state.showItems ==[true,false,false,true]).
I would like to restrict the values in this array to only one 'true' at a time so that the <SuggestStep /> component is rendered only once in the div under the button that was clicked. I'm not using CSS because the list can grow very large and don't want to render and hide a component for each one. Also considered using a radio button displayed as an image, but don't know if that would involve mixing forms with LI's and if that is bad. Feedback on the question of restricting the showItems array items to only one true at a time, and general patterns to approaching the component rendering problem I'm describing are welcome.
class CurrentSteps extends Component {
constructor(props) {
super(props)
this.state = {
toggleOnSuggestInput: false,
showItems: []
}
this.clickHandler = this.clickHandler.bind(this)
}
clickHandler(index){
let showItems = this.state.showItems.slice();
showItems[index] = !showItems[index]
this.setState({showItems})
this.setState(prevState => ({
toggleOnSuggestInput: !prevState.toggleOnSuggestInput
}))
}
render() {
let steps = this.props.currentGoalSteps.map((step, index) => {
return (
<div key={`divKey${index}`}>
<li key={index}>{step}</li>
<img key={`imageKey${index}`} onClick={this.clickHandler.bind(this,index)} alt="" src={plus}/>
{this.state.showItems[index] ? <SuggestStep /> : null}
</div>
)
});
return (
<div>
<ul> {steps} </ul>
</div>
)
}
Try making the following modifications to your code...
Change your this.state like so.
this.state = {
toggleOnSuggestInput: false,
activeIndex: null
};
Change your clickHandler to this.
clickHandler(event, index) {
this.setState({ activeIndex: index })
}
Change your map to like the one below. Notice the onClick prop change.
let steps = this.props.currentGoalSteps.map((step, index) => {
return (
<div key={`divKey${index}`}>
<li key={index}>
{step}
</li>
<img
key={`imageKey${index}`}
onClick={e => this.clickHandler(e, index)}
alt=""
src={plus}
/>
{this.state.activeIndex === index ? <SuggestStep /> : null}
</div>
);
});

Calling a function for ALL child components

I have 3 components. They parent layout, a select box, and a panel this is generated x times from some data.
<Layout>
<Dropdown>
<Panel>
<Panel>
<Panel>
I'm trying to make it so when the select value changes, the contents of each panel changes. The changes are made by doing some math between the new select value, and data that is stored in the panel component. Each panel has different data.
Layout.js
updateTrueCost(selected){
this.refs.panel.setTrueCost
}
render(){
return (
<div>
<div class="row">
Show me the true cost in
<CurrencyDrop currencyChange = {(e) => this.updateTrueCost(e)} data = {this.state.data} />
</div>
<div class="row">
{this.state.data.map((item, index) => (
<Panel ref="panel" key = {index} paneldata= {item} />
))}
</div>
</div>
);
}
Panel.js
setTrueCost(selected){
//Do some math
this.setState({truecost: mathresult})
}
render(){
return(
<div>
{this.state.truecost}
</div>
)
}
CurrencyDrop.js
onHandelChange(e){
this.props.currencyChange(e);
}
render(){
return(
<Select
onChange={this.onHandelChange.bind(this)}
options={options} />
)
}
The current result is only the last panel updates when the select changes. I'm guessing I'm doing something wrong with the ref handling, but I must not be searching the right terms because I can't find any related questions.
Instead of calling ref's method use React build-in lifecycle methods.
class Panel extends React.Component {
componentWillReceiveProps (newProps) {
// compare old and new data
// make some magic if data updates
if (this.props.panelData !== newProps.panelData) {
this.setState({trueCost: someMath()});
}
}
render () {
return <div>{this.state.trueCost}</div>
}
}
Then just change input props and all data will be updated automatically.

Resources