change parent from children created with map() - arrays

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.

Related

Accessing a component state from a sibling button

I'm building a page that will render a dynamic number of expandable rows based on data from a query.
Each expandable row contains a grid as well as a button which should add a new row to said grid.
The button needs to access and update the state of the grid.
My problem is that I don't see any way to do this from the onClick handler of a button.
Additionally, you'll see the ExpandableRow component is cloning the children (button and grid) defined in SomePage, which further complicates my issue.
Can anyone suggest a workaround that might help me accomplish my goal?
const SomePage = (props) => {
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { /* Need to access MyGrid state */ }} />
Add Row
</button>
<MyGrid>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
const ExpandableRowsComponent = (props) => {
const data = [{ id: 1 }, { id: 2 }, { id: 3 }];
return (
<>
{data.map((dataItem) => (
<ExpandableRow id={dataItem.id} />
))}
</>
);
};
const ExpandableRow = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<div className="row-item">
<div className="row-item-header">
<img
className="collapse-icon"
onClick={() => setExpanded(!expanded)}
/>
</div>
{expanded && (
<div className="row-item-content">
{React.Children.map(props.children, (child => cloneElement(child, { id: props.id })))}
</div>
)}
</div>
);
};
There are two main ways to achieve this
Hoist the state to common ancestors
Using ref (sibling communication based on this tweet)
const SomePage = (props) => {
const ref = useRef({})
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { console.log(ref.current.state) }} />
Add Row
</button>
<MyGrid ref={ref}>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
Steps required for seconds step if you want to not only access state but also update state
You must define a forwardRef component
Update ref in useEffect or pass your API object via useImerativeHandle
You can also use or get inspired by react-aptor.
⭐ If you are only concerned about the UI part (the placement of button element)
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
(Mentioned point by #Sanira Nimantha)

Get child component state in parent component

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 = []?

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}
/>)

React content rendering based on map output not working

I have a component which is going to rely on the results of an array.map to determine it's output. In this case the array contains an element called "name", and one of the names is 'Welcome.' What needs to happen is the component spits out a particular div for the 'Welcome' instance and different content (an Accordion component) for every other instance. I've used a ternary operator in the render which I'm then calling in the return, but it's outputting the same text for every instance of the component (the text specified for only the 'Welcome' instance.) I can't for the life of me figure out what I'm doing wrong. Here's the code:
export default class Back extends React.Component {
constructor(props) {
super(props)
}
render() {
const CheckDomains = this.props.totaldomains.map((domain, index) => {
<div>
{
domain.name === 'Welcome' ?
<div>This is welcome text.</div>
:
<div>This is accordion text.</div>
}
</div>
)
});
return(
<div className='back'>
{CheckDomains}
</div>
);
}
}
You need to return something in your map callback function.
Like so:
const CheckDomains = this.props.totaldomains.map((domain, index) => {
return (
<div>
{
domain.name === 'Welcome' ?
<div>This is welcome text.</div>
:
<div>This is accordion text.</div>
}
</div>
)
});

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

Resources