Manipulating state of multiple child components - javascript

I'm not sure whether or not it is best to keep state within the individual child component or the parent.
I have a parent component which will hold a child component which needs to be able to be duplicated on demand.
I have a few questions:
Where do i store the state for the individual component is it in the component itself or is it in the parent?
If it is in the child component how do I tell the parent to update the other children.
If it's in the parent how do I pass a function to the child which will update ITS state and not the parents state?
How do I access each of the components state and tell it to change based on another child state changing?
Currently I'm pushing a new "card" Component into an array which keeps track of all the Components I need to render on the "board".
I can't conceptualise the best way to manage the state of everything and how to access each child. Do they have an individual ID? how can I change all their states.
--------------------- BOARD ----------------------- *
import React from "react";
import Card from "./Card";
export default class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: [<Card />]
};
}
componentDidMount() {}
createCard() {
this.state.cards.push(<Card />);
this.forceUpdate();
}
render() {
return (
<div id="Board">
<button onClick={() => this.createCard()}>Create Card</button>
{this.state.cards.map(card => (
<Card />
))}
</div>
);
}
}
-------------------------- CARD ------------------------------ *
export default class Card extends React.Component {
constructor(props) {
super(props);
this.state = {
active: false
};
}
cardClick = () => {
this.setState({
active: !this.state.active
});
};
render(cardClick) {
return (
<div>
{this.state.active ? (
<div
className="activeCard"
id="card"
onClick={() => this.cardClick()}
>
Active
</div>
) : (
<div
className="inactiveCard"
id="card"
onClick={() => this.cardClick()}
>
Inactive
</div>
)}
</div>
);
}
} ```

Alrighty, let's take it from the top. I imagine you have some data you want to render as <Card />, one for each data item. You don't hold the components in the state, but rather, data. For instance:
this.state = {
data: [
{value: 'Cat 1'},
{value: 'Cat 2'},
{value: 'Cat 3'}
]
}
In this case, it's correct for the parent to hold the data. However, the whole concept of lifting the state up (React docs link) is not all that hard. Mostly, it's the question of: does more than one component care/depend on the same state? If the answer is yes, usually the parent should hold it. You're correct for having each card holding its own active state. However, the the parent could do it as well. Let's do that then shall we:
Board.js
import React from 'react';
import {Card} from './Card';
class Board extends React.Component{
state = {
data: [
{value: 'Cat 1'},
{value: 'Cat 2'},
{value: 'Cat 3'}
],
activeCard: null,
}
cardClick = id => {
this.setState({ activeCard: id })
}
addCard = () => {
const newCard = { value: 'Some val here' };
this.setState({ data: [...this.state.data, newCard] });
}
render(){
return(
<div id="Board">
<button onClick={this.addCard}>Add a card</button>
{this.state.data.map((item, index) =>
<Card key={index}
value={item.value}
active={this.state.activeCard === index}
cardClick={this.cardClick}
index={index}
/>
)}
</div>
)
}
}
export default Board;
By doing this, our parent manages everything. Not to say it's the only right way, but this lets us have the Card component as a 'dumb', functional component. Be careful - this specific approach relies on data never changing order, since it relies on the .map index, which is from 0 to length of data - 1.
Card.js
import React from 'react';
const Card = props => (
<div
className={props.active ? 'activeCard' : 'inactiveCard'}
onClick={() => props.cardClick(props.index)}
>
Active
</div>
);
export { Card };
There are a few key differences between what you posted and what I've done.
1) You were mapping cards with the same id="card" which is bad, id should be unique. Also, unless you really need it, you can omit it.
2) I'm toggling the className based off of the index of the active card. If the current card, let's say 2, is also the active card, it'll have activeCard className.
3) Finally, I'm passing in a function to the child that updates the parents state. By doing this, I have the state contained to the parent, and every time I update it, it'll reflect on the children as well. That's not to say your approach of having class based components for Cards is wrong, but this is simpler I think.
4) Just to throw it out there, WAI-ARIA doesn't really agree with a div having onClick events. To make the Internet a cool, accessible place, you can put a role="button" on the div, to signal it's a button. That also requires it be focusable, as well as a keyboard event listener, in which case, you're probably better of just using a <button> if the element should be clickable.
To answer your other questions:
The parent automatically propagates all state changes to all children that care for it
All child components are independent of the parent, eg if they have their own states, the parent doesn't care. It's only when they share a state AND a state update function that this becomes relevant, so only if you specifically pass such function to the children
If the children share a state, then it should be lift up, which is explained in the doc linked!
EDIT: for the given example, I assumed you only want one active card at a time. If that's not the case, either have each card hold their active states, like Keno Clayton suggested, or change activeCard into an array, checking index of each card against array of active cards

To answer your questions:
Where do i store the state for the individual component is it in the component itself or is it in the parent?
Best practice is to store the state in the component that needs it. If two sibling components need access to the same state then raise it to the parent.
You can then pass down individual props or pass the entire state down with the Context Api. You can use this to pass down functions to update the parent state if needed, and the children will automatically receive the props as they are updated.
For your specific scenario, each Card should have its own state to determine whether it is active or not. It would be possible to keep an array of active states in the parent as well, but that's not as intuitive and a bit more complex.
How do I access each of the components state and tell it to change based on another child state changing?
You shouldn't do that. You should maintain any information that you want to share with other components in the parent.

Where do i store the state for the individual component is it in the component itself or is it in the parent?
State of the individual component should be inside the component if that state is only controlled and needed within that component. For eg: active(boolean) in Card. Since a click on a card should make it selected, as well as make current active Card inactive we can conclude that it is better stored outside Card component in the Parent.
If it's in the parent how do I pass a function to the child which will update ITS state and not the parents state?
You can pass that function as a prop(after binding to the parent)
How do I access each of the components state and tell it to change based on another child state changing?
Since you keep list of cards and active boolean in parent Board component, you dont need to do this.
Keep a state like this in Board.
this.state = {
cards: [
{id: 1, name: 'Card1'},
{id: 1, name: 'Card2'},
{id: 1, name: 'Card3'},
],
activeId: 2
}

Related

How to change state of a single component in ReactJS without rendering the whole component again?

I'm new to ReactJS and have a question about how I can change the state of a specific component that was instantiated inside a Map function.
Let's say I have a simple component called panels, and inside I have N panel-item, panel-item is only a single component instantiated N times with Map function inside panels. Something like this:
class Panels extends React.Component {
constructor(props) {
super();
this.state = {
panelItems: [
{ id: '1', text: 'A' },
{ id: '2', text: 'B' },
{ id: '3', text: 'C' },
]
};
}
render() {
return (
<div>
{this.state.panelItems.map(item => (
<PanelItems key={item.id}>{item.text}</PanelItems>
))}
</div>
)
}
}
export default Panels;
Now some questions that I have:
Let's say that I wanted a button that change the state (precisely the text) of the panel item 1 to D, how can I do this?
If I did the question 1, will it re-render the whole Panels component (including the panel items 2 and 3)?
If yes, how can I only re-render the component panel item 1 without
creating a separate component to each panel item? Because they use the
same structure, only the data inside will change.
Thank you.
…a button that changes the state (precisely the text) of the panel item 1…
Create a function that splices in the change:
const updateText = (index, text) => {
// get the existing panel items from state.
// we don't want to mutate the original so we make a copy using spread syntax
const panelItems = [...this.state.panelItems];
panelItems.splice(
index, // starting at item "index"
1, // delete one item from the array
{ ...panelItems[index], text } // and insert its replacement
);
this.setState({ panelItems }); // set state with the new array
}
Then invoke that function with an onClick on the button:
<button onClick={() => updateText(0, 'the new text')}>Update Item 0</Button>
If I did the question 1, will it re-render the whole Panels component…
As a general rule, React won't re-render a pure component if props don't change. So I'd expect your example code to to work without re-rendering every PanelItem.
how can I only re-render the component panel item 1 without creating a separate component to each panel item
If you are seeing unnecessary re-renders, you might try having the loop render a component that just takes an item prop:
{this.state.panelItems.map(item => (
<Items key={item.id} item={item} />
))}
Where the Item component is what you're already doing:
const Item => ({item}) => <PanelItem>{item.text}</PanelItem>
As long as the item prop is the same object it shouldn't re-render, even when the parent state changes.

React - setting one item from parent's state as child's state

I am a React newbie and trying to learn it by building a simple quote generator where a quote is generated depending on the mood a user selects. The App component holds the state: quotes and moods (where each element is a nested object) and its children are Mood components.
Now, the state of the App component consists of four moods and what I would like to happen is: when a user clicks a button inside the Mood component, s/he is redirected to that mood's page and the Mood component's state is set to that particular mood.
The solution I worked out by myself is very crude and I'm looking for a way to make it more elegant/functional.
Here is the moods object that is the App's state:
const moods = {
mood1: {
type: 'upset',
image: 'abc.png',
},
mood2: {
type: 'unmotivated',
image: 'abc.png',
},
mood3: {
type: 'anxious',
image: 'abc.png',
},
}
the App component:
state ={
moods: moods,
}
render(){
return (
<div className="Container">
<ul className='moods'>
{
Object.keys(this.state.moods).map(key => <Mood
moodsData = {this.state.moods}
key={key}
indexKey = {key}
index={this.state.moods[key].type}
details={this.state.moods[key]}
/>)
}
</ul>
</div>
);}}
And this is how far I got inside the Mood component, where the onClick function on the button is:
handleClick = (e) => {
this.setState({moods: e.target.value});
}
I will be grateful for any pointers/suggestions! Spent so many hours on this I feel like my brain doesn't accept any more Youtube tutorials/Medium articles.
Well, the first thing i notice is that you are trying to use map on a javascript object instead of an array, this could bring some problems when using some functions, so i advice to make it an array.
If you just have one Mood component and based on the mood type it receives change its style, it doesn't actually need to manage state from inside, you can just pass the props to the Mood component and work around what props receives.
For example:
Moods as an array:
const moods = [
{
type: 'upset',
image: 'abc.png',
},
{
type: 'unmotivated',
image: 'abc.png',
},
{
type: 'anxious',
image: 'abc.png',
},
]
i'm assuming you get the Mood list from a server or an external source so that's why i'm keeping moods in state instead of just mapping through the const moods.
state ={
moods: moods,
mood:null,
}
onClick= (key) =>{
console.log(this.state.moods[key]);
this.setState({
mood:this.state.moods[key],
})
}
render(){
return (
<div className="Container">
<ul className='moods'>
{
Object.keys(this.state.moods).map((key) => <div key={key}>
<a onClick={() =>{this.onClick(key)}}>Click here to change the mood to {this.state.moods[key].type}</a>
</div>)
}
</ul>
{this.state.mood ? <Mood actualMood={this.state.mood}/> : null}
</div>
);
}
and the Mood component just gets some props and display something based on what it gets:
class Mood extends Component
{
render()
{
console.log(this.props.actualMood.type);
return (
<div>
<p>You're looking at {this.props.actualMood.type} mood</p>
</div>
);
}
}
this can be easily achieved using react hooks but class based components need a tricky solution and may not look easy at first glance.
If what you want to achieve is to move to another component, you can have a parent Component which only manages the state, then based on a condition (if mood has been selected) render a component either ChooseAMoodComponent or MoodComponent.
Another way to achieve this is through React Router which you can pass some params via URL get params.
but the best practice should be using Redux.

Parent component unnecessarily re-rendering child on parent state change

I am creating a simple Magic The Gathering search engine. The vision is to have a list of search results, and when a search result is clicked the main display renders extended information about the card selected.
You can see it here
The top level App component contains the state of what card is to be displayed and the ScrollView component maintains the state of the card selected for only the highlighting of the selected card in the list. I propagate down the setDisplayCard handler so that when a card is clicked in the list, I can set the display card as a callback.
function App(props) {
const [displayCard, setDisplayCard] = useState(null)
return (
<div className="App">
<SearchDisplay handleCardSelect={setDisplayCard}/>
<CardDisplay card={displayCard} />
</div>
);
}
function SearchDisplay({handleCardSelect}) {
const [cards, setCards] = useState([]);
useEffect(() => {
(async () => {
const cards = await testCardSearch();
setCards(cards);
})();
}, []);
async function handleSearch(searchTerm) {
const searchCards = await cardSearch({name: searchTerm});
setCards(searchCards)
};
return (
<StyledDiv>
<SearchBar
handleSubmit={handleSearch}
/>
<ScrollView
handleCardSelect={handleCardSelect}
cards={cards}
/>
</StyledDiv>
);
}
function ScrollView({cards, handleCardSelect}) {
const [selected, setSelected] = useState(null);
return (
<ViewContainer>
{cards.map((card, idx) =>
<li
key={idx}
style={selected === idx ? {backgroundColor: "red"} : {backgroundColor: "blue"}}
onClick={() => {
setSelected(idx);
handleCardSelect(card);
}}
>
<Card card={card} />
</li>
)}
</ViewContainer>
);
}
The issue I am having is that calling setDisplayCard re-renders my ScrollView and eliminates its local state of the card that was selected so I am unable to highlight the active card in the list. Based on my understanding of react, I don't see why ScrollView re-renders as it does not depend on the state of displayCard. And I am not sure what approach to take to fix it. When I click on a card in the list, I expect it to highlight red.
A child component's render method will always be called, once its parent's render method is invoked. The same goes for if its props or state change.
Since you're using functional components, you could use the React.memo HOC to prevent unnecessary component re-renders.
React.memo acts similar to a PureComponent and will shallowly compare ScrollView's old props to the new props and only trigger a re-render if they're unequal:
export default React.memo(ScrollView);
React.memo also has a second argument, which gives you control over the comparison:
function areEqual(prevProps, nextProps) {
// only update if a card was added or removed
return prevProps.cards.length === nextProps.cards.length;
}
export default React.memo(ScrollView, areEqual);
If you were to use class-based components, you could use the shouldComponentUpdate life cycle method as well.
By default (stateless) components re-render under 3 conditions
It's props have changed
It's state has changed
It's parent re-renders
This behavior can be changed using either shouldComponentUpdate for components or memo for stateless-components.
// If this function returns true, the component won't rerender
areEqual((prevProps, nextProps) => prevProps.cards === nextProps.card)
export default React.memo(ScrollView, areEqual);
However I don't think this is your problem. You are using an array Index idx as your element key which can often lead to unexpected behavior.
Try to remove key={idx} and check if this fixes your issue.
So your App component is supposed to hold the state of the card the user clicked? Right now your App component is stateless. It's a functional component. Try converting it to a class component with an initial, and maintained, state.
What is the logic of your setDisplayCard()?
I've heard that in React 16? there is something like 'useState()' and 'hooks', but I'm not familiar with it.
This person seemed to be having a similar problem,
React functional component using state

ReactJS: Dynamic previous & next buttons

I am new to ReactJS and trying to understand how to accomplish a basic concept using best practices. The code below will render a list of items, when an item is clicked a stateful component is toggled and data is passed from to the state object before being consumed by on re-render.
What I need help with is a method to retrieve data from siblings from the array example below. Once the data has been fetched for previous/next projects that data should be accessible to the component. Can these siblings be accessed with a key value? If so, how would I use key?
DATA:
window.Seed = (function() {
const projects = [
{id:1, title:'Project 1', description:'Project 1 Description'},
{id:2, title:'Project 2', description:'Project 2 Description'},
{id:3, title:'Project 3', description:'Project 3 Description'}
]
};
REACT CODE:
class ProjectList extends React.Component {
state = {
projects: [],
isOpen: false,
modalTitle: '',
modalDescription: '',
modalId: ''
}
componentDidMount() {
this.setState({ projects: Seed.projects });
}
handleModalOpen = (id, title, description) => {
this.setState({
isOpen: true,
modalId: id,
modalTitle: title,
modalDescription: description
});
};
handleModalClose = () => {
this.setState({ isOpen: false });
};
render() {
const projects = this.state.projects;
// GOAL #1: Pass previous/next project data as props
// =================================================
// Pass projects.map data from previous & next siblings
// to the current component or explore a new method to
// retrieve data from state? I can already tell that this
// probably is not the most dynamic approach to handle
// previous/next data.
const projectComponents = projects.map((project) => (
<Project
key={'project-' + project.id}
id={project.id}
title={project.title}
url={project.url}
description={project.description}
background={project.background}
onModalOpen={this.handleModalOpen}
prevTitle={???}
nextTitle={???}
prevDescription={???}
nextDescription={???}
/>
));
if (this.state.isOpen) {
return (
<Modal
id={this.state.modalId}
title={this.state.modalTitle}
description={this.state.modalDescription}
onModalClose={this.handleModalClose}
/>
);
} else {
return (
<div className="projects-container">
<div id="projects" className="wrapper">
{projectComponents}
</div>
</div>
);
}
}
}
class Project extends React.Component {
render() {
return (
<aside className='item'>
<a onClick={this.props.onModalOpen.bind(
this, this.props.id, this.props.title, this.props.description
// I think that I need to bind previous/next data here on click.
// this.props.prevTitle, this.props.nextTitle
// this.props.prevDescription, this.props.nextDescription
)}>
<img src={this.props.background} />
</a>
</aside>
);
}
}
class Modal extends React.Component {
render() {
return (
<div className="modal">
// GOAL #2: Get sibling data from mapped array
// =================================================
// I would like to be able to pass previous and next
// project data as props to populate these headings.
<h2>Current Project: {this.props.title}</h2>
<h2>Previous Project: {this.props.prevTitle} </h2>
<h2>Next Project: {this.props.nextTitle} </h2>
// GOAL #3: Replace content with data stored in state
// =================================================
// I would like to be able to 'onClick' these buttons
// to replace the content of the <Modal />. Essentially,
// this should behave like a lightbox or carousel.
// Initial Project Description
<p>{this.props.description}</p>
// this.props.description = this.props.prevDescription
<button onClick={???}>Previous Project</button>
// this.props.description = this.props.nextDescription
<button onClick={???}>Next Project</button>
<button onClick={this.props.onModalClose}>Close Modal</button>
</div>
);
}
}
DEMO
Thanks for reading!
UPDATED SOLUTION
As a general principle, data should flow through React components from parent to child, not from sibling to sibling. In theory, it might be possible to pass data between siblings but this makes things orders of magnitude more complex. The beauty of the top-down data flow is that any time the data in the parent changes, the children will (probably, excluding optimizations like pure rendering) re-render. So you just have to change the data in the parent and the children will automatically update as they need to. Here are suggestions for approaching your goals this way. This isn't perfect but I hope it illustrates the point:
Goal #1: Array.prototype.map takes a function with these parameters: function(currentValue, index, array) {...}. For each project, the previous project is array[index - 1] and the next project is array[index + 1]. All you have to do is reference the previous and next projects this way in the map function to get their title, description, etc.
Goal #2: Instead of using this.state.isOpen, use this.state.activeProject. When the project is clicked, set this.state.activeProject to the index of that project. Then you can infer that the modal is open if !!this.state.activeProject (if this.state.activeProject is truthy). Use the activeProject id/index to pass desired data from that project to the Modal component in the ProjectList component render method.
Goal #3: Update this.state.activeProject to the index/id of the previous or next project. This will cause a re-render of ProjectList with the new props. To do this, you'll want to pass onNextClick and onPrevClick props. You can partially apply this.state.activeProject + 1 and this.state.activeProject - 1 to onNextClick and onPrevClick, respectively.

React with lists in state, how to set new state?

I ran into an issue with updating part of the state that is a list that's passed on to children of a component.
I pass in a list to a child, but then have trouble to update that list and have the child reflect the new state;
<ItemsClass items={this.state.items1} />
When I change the value of this.state.items1, the component doesn't render with the new value.
this.setState({items1: []}); // this has no effect
However, if I change the already existing array (not replacing it new a new empty one), the component renders as I wish;
this.setState(state => { clearArray(state.items1); return state; });
That means the state updating function isn't pure, which React states it should be.
The HTML;
<div id='app'></div>
The js;
class ItemsClass extends React.Component {
constructor(props){
super(props);
this.state = {items: props.items};
}
render() {
var items = this.state.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
function ItemsFunction(props) {
var items = props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
class App extends React.Component {
constructor(props){
super(props);
var items = [{id:1, text: 'item 1'}, {id: 2, text: 'item 2'}];
this.state = {
items1: items.slice(),
items2: items.slice(),
items3: items.slice()
};
this.clearLists = this.clearLists.bind(this);
}
clearLists() {
// for items1 and items2, clear the lists by assigning new empty arrays (pure).
this.setState({items1: [], items2: []});
// for items3, change the already existing array (non-pure).
this.setState(state => {
while (state.items3.length) {
state.items3.pop();
}
})
}
render() {
return (
<div>
<button onClick={this.clearLists}>Clear all lists</button>
<h2>Items rendered by class, set list to new empty array</h2>
<ItemsClass items={this.state.items1} />
<h2>Items rendered by class, empty the already existing array</h2>
<ItemsClass items={this.state.items3} />
<h2>Items rendered by function</h2>
<ItemsFunction items={this.state.items2} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Try it out on codepen.
It seems that the ItemsClass doesn't update even though it's created with <ItemsClass items={this.state.items1}/> and this.state.items1 in the parent changes.
Is this the expected behavior? How can I update the state in the ItemsClass child from the parent?
I'm I missing something? This behavior seems quite error prone, since it's easy to assume that the child should follow the new state, the way it was passed in when the child was created.
You're copying the props of ItemsClass into the state when the component gets initialized - you don't reset the state when the props change, so your component's updates don't get displayed. To quote the docs:
Beware of this pattern, as state won't be up-to-date with any props update. Instead of syncing props to state, you often want to lift the state up.
If your component has to do something when the props change, you can use the componentWillReceieveProps lifecycle hook to do so (note that it doesn't get run when the component initially mounts, only on subsequent prop updates).
That said, there's zero reason for you to be duplicating the props here (and honestly there's rarely a good reason to do so in general) - just use the props directly, as you're doing with ItemsFunction, and everything will stay in sync:
class ItemsClass extends React.Component {
render() {
var items = this.props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
Here's a working version of your Codepen: http://codepen.io/anon/pen/JNzBPV

Categories

Resources