ReactJS: Dynamic previous & next buttons - javascript

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.

Related

React - How to re-render a component using another component?

I have a NavBar component that has a list of dynamically generated links (these links are generated after querying my backend for some categories). These links are stored inside a child component of the NavBar, called DrawerMenu.
The NavBar is a child of the main App.js component.
In my Category component, I have a "delete" function that deletes a category. Once I delete a category I want to remove the link to it in the NavBar. How would I go about doing this?
For further context, my components are given below:
DrawerMenu component
class DrawerMenu extends Component {
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
getData = (query) => {
// Query backend for category data and set it to this.state.menuItems
}
componentDidMount() {
this.getData(menuItemsQuery)
}
render() {
const { classes, handleDrawerClose, open } = this.props
const { menuItems } = this.state
const drawer = (classes, handleDrawerClose) => (
<div>
...
{
menuItems.map((menuItem, index) => (
<Link color="inherit" key={index} to={menuItem.link} className={classes.drawerLink} component={RouterLink}>
<ListItem button className={classes.drawerListItem} onClick={handleDrawerClose}>
<ListItemText primary={menuItem.name} />
</ListItem>
</Link>
))
}
...
</div>
)
...
return (
<div>
<Drawer
variant="temporary"
anchor='left'
open={open}
onClose={handleDrawerClose}
classes={{
paper: `${open ? classes.drawerOpen : null} ${!open ? classes.drawerClose : null}`,
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
{drawer(classes, handleDrawerClose)}
</Drawer>
</div>
)
}
}
NavBar component
function PrimarySearchAppBar(props) {
return (
<div className={classes.grow}>
...
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
/>
...
</div>
)
}
Category component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.history.push(`/`)
}
...
}
There are two common ways of doing this: You can either use a state management tool, like Redux or pass your state down the component tree as props.
Redux is often used when several components depend on the same state or when the component that depends on a state is several layers deep, so it would get cumbersome to pass it down as props.
I'll assume your component tree is not very large, so I will create a simple example passing props down the tree.
class DrawerMenu extends Component {
// We're gonna manage the state here, so the deletion
// will actually be handled by this component
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
handleDelete = (id) => {
let updatedMenuItem = [...this.state.menuItems]; //Create a copy
updatedMenuItem = updatedMenuItem(item => item.id !== id) // Remove the
deleted item
this.setState({
menuItems: updatedMenuItem
})
}
...
// Then wherever you render the category component
<Category handleDelete = {handleDelete}/> //Pass a reference to the delete method
}
Category Component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.handleDelete(categoryId) //Pass the id of the category
this.props.history.push(`/`)
}
...
}
I would suggest reading about state management, it is a core concept in React and you will use it everywhere. Redux and Context API for example.
Not sure why Dennis Vash deleted their answer, they are correct, but perhaps not descriptive enough in the solution.
The way you delete the category is not to call the backend itself from inside the category component, because then the navbar doesn't know that you made a call, but to call a callback that is in an ancestor shared by both the category component and the navbar to delete a category, and then rerequest the categories list from the server. In the example below, this ancestor that is shared is MyCategoriesProvider
Because the category component is likely to be in a much different place (or multiple places) in the tree than the NavBar, it's best to use context.
Honestly, this is a great place for redux, but I'm not going to push redux on you and instead will just demo a Context solution.
// We're going to create a context that will manage your categories
// The only job of this context is to hold the current categories,
// and supply the updating functions. For brevity, I'll just give
// it a handleDelete function.
// Ideally, you'd also store the status of the request in this context
// as well so you could show loaders in the app, etc
import { createContext } from 'react';
// export this, we'll be using it later
export const CategoriesContext = createContext();
// export this, we'll render it high up in the app
// it will only accept children
export const MyCategoriesProvider = ({children}) => {
// here we can add a status flag in case we wanted to show a spinner
// somewhere down in your app
const [isRequestingCategories,setIsRequestingCategories] = useState(false);
// this is your list of categories that you got from the server
// we'll start with an empty array
const [categories,setCategories] = useState([]);
const fetch = async () => {
setIsRequestingCategories(true);
setCategories(await apiCallToFetchCategories());
setIsRequestingCategories(false);
}
const handleDelete = async category => {
await apiCallToDeleteCategory(category);
// we deleted a category, so we should re-request the list from the server
fetch();
}
useEffect(() => {
// when this component mounts, fetch the categories immediately
fetch();
// feel free to ignore any warnings if you're using a linter about rules of hooks here - this is 100% a "componentDidMount" hook and doesn't have any dependencies
},[]);
return <CategoriesContext.Provider value={{categories,isRequestingCategories,handleDelete}}>{children}</CategoriesContext.Provider>
}
// And you use it like this:
const App = () => {
return (
<MyCategoriesProvider>
<SomeOtherComponent>
<SomeOtherComponent> <- let's say your PrimarySearchBar is in here somewhere
<SomeOtherComponent>
</MyCategoriesProvider>
)
}
// in PrimarySearchBar you'd do this:
function PrimarySearchBar(props) => {
const {categories} = useContext(CategoriesContext); // you exported this above, remember?
// pass it as a prop to navbar, you could easily put the useContext hook inside of any component
return <NavBar categories={categories}/>
}
// in your category component you could do this:
class Category extends Component {
render() {
// Don't forget, categoriesContext is the thing you exported way up at the top
<CategoriesContext.Consumer>
{({handleDelete}) => {
return <button onClick={() => handleDelete(this.props.category)}>
}}
</CategoriesContext.Consumer>
}
}
EDIT:
I see you're mixing class and functional components, which is fine. You should check out this article on how to use the context api in either of them - in functional components you typically use a useContext hook, while in class components you'll use a consumer.
I would just refresh the list of categories that come from the server, after the delete request is done.
I'd do it as follows:
I would make the drawer component not so smart, making it receive the list of menuItems.
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
items={/* ... */}
/>
This is an important step, because now, to refresh the list of items rendered, you just pass another list. The server-side logic remains disconnected from this component in this way.
I'm not sure where you render the Category components, but supposing it is rendered outside the PrimarySearchAppBar it seems that this menuItems might need to be passed to the components from an upper level. I see 2 solutions:
I'd do the request for the menuItems from the same place where I do the request for the categories:
const App = props => {
const [categories, setCategories] = React.useState([])
const [menuItems, setMenuItems] = React.useState([])
const fetchCategories = useCallback(()=> {
yourApi.getCategories().then(categories => setCategories(categories))
})
const fetchMenuItems = useCallback(() => {
yourApi.getMenuItems().then(menuItems => setMenuItems(menuItems))
})
useEffect(() => {
fetchCategories()
}, [])
useEffect(() => {
fetchMenuItems()
}, [categories])
const handleDeleteCategory = useCallback(idToDelete => {
yourApi.deleteCategory(idToDelete).then(fetchCategories)
})
return (
<div>
<PrimarySearchAppBar menuItems={menuItems}/>
<Categories categories={categories} onDeleteClick={handleDeleteCategory} />
</div>
)
}
you can do the same thing but do it with a provider and using the content API if you do not want to have all the logic here. It is good to have smart/fetches/server-side logic in a top level component and then pass down props to dumb components.
PS.
There is also a nice hook to make fetches easier:
https://github.com/doasync/use-promise
I currently use a custom version of a usePromise hook I found because I added some interesting features. I can share it if you want but I don't want to add noise to the answer.

Local storage in react todo list

I created to do list using react, but I want it to be local storage - so when the user refresh the page it still saved the items and will present them.
I read I need to use localStorage but I'm not sure where and how, attach the app.js and TodoItem component
class App extends Component {
state = {
items: [],
id: uuidv4(),
item: "",
editItem: false
};
handleChange = e => {
...
};
handleSubmit = e => {
e.preventDefault();
const newItem = {
id: this.state.id,
title: this.state.item
};
const updatedItems = [...this.state.items, newItem];
this.setState({
items: updatedItems,
item: "",
id: uuidv4(),
editItem: false
});
};
...
render() {
return (
<TodoInput
item={this.state.item}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
editItem={this.state.editItem}
/>
<TodoList
items={this.state.items}
clearList={this.clearList}
handleDelete={this.handleDelete}
handleEdit={this.handleEdit}
/>
);
}
}
export default class TodoItem extends Component {
state = {
avatarURL: '',
}
componentDidMount() {
imgGen().then(avatarURL => this.setState({ avatarURL }));
}
render() {
const { title, handleDelete, handleEdit } = this.props;
const { avatarURL } = this.state;
return (
<h6>{title}</h6>
<span className="mx-2 text-success" onClick={handleEdit}>
</span>
<span className="mx-2 text-danger" onClick={handleDelete}>
</span>
);
}
}
You can do it like this, mind the comments
class App extends Component {
state = {
// load items while initializing
items: window.localStorage.getItem('items') ? JSON.parse(window.localStorage.getItem('items')) : [],
id: uuidv4(),
item: "",
editItem: false
};
handleChange = e => {
// ...
};
handleSubmit = e => {
e.preventDefault();
const newItem = {
id: this.state.id,
title: this.state.item
};
const updatedItems = [...this.state.items, newItem];
// Save items while changing
window.localStorage.setItem('items', JSON.stringify(updatedItems));
this.setState({
items: updatedItems,
item: "",
id: uuidv4(),
editItem: false
});
};
// ...
render() {
return (
<>
<TodoInput
item={this.state.item}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
editItem={this.state.editItem}
/>
<TodoList
items={this.state.items}
clearList={this.clearList}
handleDelete={this.handleDelete}
handleEdit={this.handleEdit}
/>
</>
);
}
}
Here's some simple logic you can use in your componentDidMount() method of your App.
const localStorageList = localStorage.getItem('todo-list')
if (!localStorageList) {return null} else {this.setState({items: localStorageList})
To add to the localStorage please look at this question
and this resource
Let me help you with this, using the least no. of codes. I have written a clear explanation of the steps, for you all to better understand, please bear with me , it is definitely with the time to read.
Also, note this solution is perfectly crafted for functional components. However I have mentioned how to do it in class components, you have to tweak some things if you are using class components. Like you can not use hooks in class-based components, but access this instance, so it will be fine, either ways
Please give it a full read, if you are having a tough time understanding the functionality, I have tried to break down the process in layman. The explanation is long, the lines of code is just under 10. happy to help
Persisting states of the todo app, upon page refresh, is pretty simple.
We can use State management libraries for it, or local storage as well.
Here, we will just go with the most simple one - using local storage.
Before we jump to the code, let us build the functionality visually.
So, after the user enters things in the todo space, we want few things to happen:
We want to store the list of items (which will essentially be an array) in the local storage. (We can skip the JSON.parse, here, since the array that will be saved, will be string, bcz user enters string in the todo-app, generally, however, it's not a bad idea to parse the userinputs).
useEffect(()=>{
window.localStorage.setItems("key" , value)
}, [dependency])
After you do this, make sure you check the dev-tools => application => localStorage => to see if the key and values are being stored. You shall be able to see them.
However, you will notice, that upon refresh, the localStorage values stay, but the data in the UI are lost. Not surprising.
This is the last and important step.
What we want upon page reload? Let us break it down :
We want to check if there is any data that is there in the localStorage. If there is: we will change the state of the app, based on the previous user inputs.
If there is no data in the LocalStorage, we will just pass an empty array.
Using hooks, in the functional component is actually What I prefer, class components require many boiler plates, so, the code...
import {useState} from 'react';/for functional components
//for class components you can just init the state , in the constructor(props) and
change it using the this.setState method
//to getItems from localStorage to render in the UI
useEffect(()=>{
const storedData = localStorage,getItems("keys" , value)
storedData ? setValue(value) : [];
},[])
[] : because we want it to render on every reload, once.
smake sure to initiliaze the state using useState;
const [value , setValue] = useState("")
//to setItems in localStorage
useEffect(()=>{
window.localStorage.setItems("key" , value)
}, [dependency])
useEffect is essentially a hook for functional components which is similar to componentDidMount in-class components.
If using class components, instead of using the useState, hook, use this.setState.
You could format your todolist into a JSON string and store it using :
localStorage.setItem("todolist", "your_JSON_string_here");
However, web Local Storage have storage limitations which will cause issues if the data stored are getting larger in time.
More info at here
Perhaps you could consider IndexedDB (if you are storing huge data) INFO

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.

Manipulating state of multiple child components

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
}

Categories

Resources