Passing data from child to parent component in React - reactjs

Edited to add solution at bottom
I have a project created with React and Typescript.
There is a parent component (Home) that displays a child component depending on the value of the state variable 'currentDemo'. The goal is to have a navigation component that will display whatever item was clicked. Each nav item has an id associated that relates to the component to be displayed. Ie, nav item 'a' should display component 'a', nav item 'b' should show component 'b', etc. Here is a snippet of the code.
Home.tsx (Parent):
import React, { useState } from 'react';
import { Intro } from 'app/components/intro/Intro';
import { SidebarNav } from 'app/components/sidebarNav/SidebarNav';
import { ComponentA } from 'app/components/ComponentA/ComponentA';
import { ComponentB } from 'app/components/ComponentB/ComponentB';
export function Home() {
//use state to track which demo is currently displayed ('intro' is default)
const [currentDemo, setCurrentDemo] = useState('intro');
return (
<>
<Header />
<div className="home">
<SidebarNav setCurrentDemo={setCurrentDemo} />
{currentDemo === 'intro' && <Intro />}
{currentDemo === 'ComponentA' && <ComponentA/>}
{currentDemo === 'ComponentB' && <ComponentB/>}
</div>
</>
);
}
SidebarNav.tsx(child):
import React, { useState } from 'react';
const navData = [
{
title: 'Introduction',
id: 'intro'
},
{
title: 'Component A',
id: 'ComponentA'
},
{
title: 'Component B',
id: 'ComponentB'
}
];
export function SidebarNav(setCurrentDemo: any) {
//GOAL: PASS ID OF SELECTED NAV ITEM TO PARENT COMPONENT AND SET VALUE OF 'CURRENTDEMO' TO THAT ID
const handleCurrentClick = (id: any) => {
if (id === 'intro') {
setCurrentDemo('ComponentA');
} else if (id === 'ComponentA') {
setCurrentDemo('ComponentB');
} else if (id === 'ComponentB') {
setCurrentDemo('intro');
}
};
return (
<div className="sidebarNav">
<div className="sidebarNav__container">
{navData?.map((item, index) => (
<div key={index}>
<button
onClick={() => {
handleCurrentClick(item.id);
}}
id={item.id}
>
{item.title}
</button>
</div>
))}
</div>
</div>
);
}
The specific implementation of Component A and B don't matter for this scenario. I've tested by manually setting the value of 'currentDemo' and the correct demo will display. I also confirmed that the id's for each nav item are correctly displaying via console.log(item.id).
How can I pass the pass the value of the id from SidebarNav to Home, setting the value of currentDemo to the ID of the nav item that was clicked? I feel like I'm close, but it's not quite right.
When clicking any of the nav elements there is a console error stating that setCurrentDemo is not a function. Which makes sense because it's the setter for the state, but how can I specify that we want to actually set currentDemo to the value of the item's ID?
Here is the solution that worked for this application. Changes made are in the navigation component. Added an interface in the nav and adjusted as such:
interface SidebarNavProps {
setCurrentDemo: React.Dispatch<SetStateAction<string>>;
}
export function SidebarNav(props: SidebarNavProps) {
const { setCurrentDemo } = props;
...rest of function remains the same
};

Each component receives props as an object. In SidebarNav component, props will look like this { setCurrentDemo } :any not setCurrentDemo:any.
Here's the interface for SidebarNav Component
import { SetStateAction } from "react";
interface SidebarNavProps {
setCurrentDemo: SetStateAction<string>
}
And your SidebarNav component will look like this:
export function SidebarNav(props: SidebarNavProps) {
const { setCurrentDemo } = props;
//GOAL: PASS ID OF SELECTED NAV ITEM TO PARENT COMPONENT AND SET VALUE OF 'CURRENTDEMO' TO THAT ID
const handleCurrentClick = (id: any) => {
if (id === 'intro') {
setCurrentDemo('ComponentA');
} else if (id === 'ComponentA') {
setCurrentDemo('ComponentB');
} else if (id === 'ComponentB') {
setCurrentDemo('intro');
}
};
return (
<div className="sidebarNav">
<div className="sidebarNav__container">
{navData?.map((item, index) => (
<div key={index}>
<button
onClick={() => {
handleCurrentClick(item.id);
}}
id={item.id}
>
{item.title}
</button>
</div>
))}
</div>
</div>
);
}
This will fix that error and you can store the id in state using setCurrentDemo.

you can identify state in parent and set this state on child component to pass data from child to parent

Related

Handling state of multiple instances of same component

I have this Item component that I am using in another component:
import React, { useState } from "react";
import Button from "./Button";
const Item = ({ name }) => {
const [isSelected, setIsSelected] = useState(false);
const toggle = () => {
setIsSelected(!isSelected);
};
var buttonColor;
var buttonText;
if (isSelected === true) {
buttonColor = "bg-button-blue";
buttonText = "Selected";
} else {
buttonColor = "bg-button-gray";
buttonText = "Select";
}
return (
<div onClick={toggle} className="flex ml-2 items-center">
<div className="text-misc-gray text-xs w-40">{name}</div>
<div>
<Button
text={buttonText}
color={buttonColor}
height={"h-8"}
width={"w-18"}
></Button>
</div>
</div>
);
};
export default Item;
In the other component, I have multiple instances of this Item component, representing different items. The Item component can with a click change property, like text and color for the button.
The problem is that in the other component, multiple of these Items can be toggled at the same time.
I would like that out of every instance of the Item component in the other component, only a single one can be toggled on at the same time. So if I select one item, the previous (if any selected) will be "unselected", changing the text and color back to the original state.
Can this be solved by only making changes in the Item component, or do I also need to make changes where it's being imported?
Can this be solved by only making changes in the Item component
Isolating state is good, but in this situation, Item state has dependencies on other components, so we cannot isolate that state completely.
I'd suggest that you should lift your state isSelected up to the parent component, and pass that state down to each Item for UI update.
import React, { useState } from "react";
const ParentComponent = () => {
const [selectedIndex, setSelectedIndex] = useState();
//[...Array(5)] is representing your actual items
return (
<div>
[...Array(5)].map((value, index) => <Item key={index} isSelected={selectedIndex === index} index={index} toggle={(i) => setSelectedIndex(i)} />)
</div>
);
};
And then change Item props with a little logic modification
import React, { useState } from "react";
import Button from "./Button";
const Item = ({ name, isSelected, index, toggle }) => {
var buttonColor;
var buttonText;
if (isSelected === true) {
buttonColor = "bg-button-blue";
buttonText = "Selected";
} else {
buttonColor = "bg-button-gray";
buttonText = "Select";
}
return (
<div onClick={() => toggle(index)} className="flex ml-2 items-center">
<div className="text-misc-gray text-xs w-40">{name}</div>
<div>
<Button
text={buttonText}
color={buttonColor}
height={"h-8"}
width={"w-18"}
></Button>
</div>
</div>
);
};
export default Item;
Can this be solved by only making changes in the Item component
No good way using the React paradigm, because you want one instance to affect another instance, where the other instance is not a child.
In the ancestor component of the <Item>s, create a state variable that holds the index (or ID, or name, or some other uniquely identifying aspect) of the Item component currently toggled on, and then pass down an isSelected prop to each Item, as well as the state setter. (The individual Items should no longer have an isSelected state.) Perhaps something along the lines of:
const numItems = 5;
const Parent = () => {
const [selectedIndex, setSelectedIndex] = useState(-1);
const makeToggle = (i) => () => {
setSelectedIndex(i === selectedIndex ? -1 : i);
};
return (
<div>
{ Array.from(
{ length: numItems },
(_, i) => <Item
isSelected={selectedIndex == i}
toggle={makeToggle(i)}
/>
)}
</div>
);
};

how to create new state on the fly

I have two components in my project.
One is App.jsx
One is Child.jsx
Right now inside, there are 3 child components was rendered manually. The presenting data in child was passed from parent.
However, in future, I would like to add a button that can create new child on the fly.
let say, I can see a new child 4 appear after child 3, after clicking a new button.
So,
Q1: First question, since presenting data must be from parent (as I dont want to losing data after condition changing from false to true), how could I write things like creating extra state on the fly?
Q2: Second question: How to create a new component after child 3, after clicking a add child button?
For better illustrate, here is my code https://playcode.io/940784
In App.jsx:
import React,{useState,useEffect} from 'react';
import {Child} from './Child.jsx'
export function App(props) {
[message,setMessage]=useState('');
[showChild1,setShowChild1]=useState(true);
[showChild2,setShowChild2]=useState(true);
[showChild3,setShowChild3]=useState(true);
const [child1data,setChild1data] = useState('child1');
const [child2data,setChild2data] = useState('child2');
const [child3data,setChild3data] = useState('child3');
useEffect(() => {
console.log('parent was rendered')
})
return (
<div className='App'>
<button >add child</button>
<br/>
<br/>
<button onClick={()=>setShowChild1(!showChild1)}>Show child1</button>
{showChild1 && <Child key='1' data={child1data} setData={setChild1data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild2(!showChild2)}>Show child2</button>
{showChild2 && <Child key='2'data={child2data} setData={setChild2data}/>}
<br/>
<br/>
<button onClick={()=>setShowChild3(!showChild3) } setData={setChild3data}>Show child3</button>
<br/>
{showChild3 && <Child key='3' data={child3data}/>}
</div>
);
}
// Log to console
console.log('Hello console')
In Child.jsx
import React, {useState, useEffect} from 'react';
export const Child = (props) => {
const {data,setData} = props;
useEffect(()=>{
console.log(data)
})
return <>
<h1>This is {data}</h1>
<input value={data} onChange={((e)=>setData(e.target.value))}></input>
</>
}
In the code snippet below, I've created an example demonstrating how to create, manage, and update an array of child state objects from a parent component. I've included lots of inline comments to help explain as you read the code:
After you Run the code snippet, you can select "Full page" to expand the viewport area of the iframe.
body, button, input { font-family: sans-serif; font-size: 1rem; } button, input { padding: 0.5rem; } ul { list-style: none; } .vertical { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; }
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.12/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
// import ReactDOM from 'react-dom/client';
// import {StrictMode, useState} from 'react';
// This Stack Overflow snippet demo uses UMD modules
// instead of the commented import statments above
const {StrictMode, useState} = React;
// Returns a new child state object with a unique ID:
function getInitialChildState () {
return {
hidden: false,
id: window.crypto.randomUUID(),
text: '',
};
}
// A child component that displays a text state and allows for
// modifying the text state using a controlled input:
function Child ({text, setText}) {
return (
<div className="vertical">
<div>{text ? text : 'Empty 👀'}</div>
<input
type="text"
onChange={ev => setText(ev.target.value)}
value={text}
/>
</div>
);
}
// A wrapper component for each child that allows toggling its "hidden" property
// and conditionally renders the child according to that value:
function ChildListItem ({state, updateState}) {
const toggleHidden = () => updateState({hidden: !state.hidden});
const setText = (text) => updateState({text});
return (
<li className="vertical">
<button onClick={toggleHidden}>{
state.hidden
? 'Show'
: 'Hide'
} child</button>
{
state.hidden
? null
: <Child text={state.text} setText={setText} />
}
</li>
);
}
function App () {
// Array of child states:
const [childStates, setChildStates] = useState([]);
// Append a new child state to the end of the states array:
const addChild = () => setChildStates(arr => [...arr, getInitialChildState()]);
// Returns a function that allows updating a specific child's state
// based on its ID:
const createChildStateUpdateFn = (id) => (updatedChildState) => {
setChildStates(states => {
const childIndex = states.findIndex(state => state.id === id);
// If the ID was not found, just return the original state (don't update):
if (childIndex === -1) return states;
// Create a shallow copy of the states array:
const statesCopy = [...states];
// Get an object reference to the targeted child state:
const childState = statesCopy[childIndex];
// Replace the child state object in the array copy with a NEW object
// that includes all of the original properties and merges in all of the
// updated properties:
statesCopy[childIndex] = {...childState, ...updatedChildState};
// Return the array copy of the child states:
return statesCopy;
});
};
return (
<div>
<h1>Parent</h1>
<button onClick={addChild}>Add child</button>
<ul className="vertical">
{
childStates.map(state => (
<ChildListItem
// Every list item needs a unique key:
key={state.id}
state={state}
// Create a function for updating a child's state
// without needing its ID:
updateState={createChildStateUpdateFn(state.id)}
/>
))
}
</ul>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root'));
reactRoot.render(
<StrictMode>
<App />
</StrictMode>
);
</script>
Use data and setData inside Child.jsx, otherwise you can not have infinite childs.
import React, {useState} from 'react';
export const Child = (props) => {
const [data, setData] = useState(props.initialData);
return <>
<h1>This is {data}</h1>
<input value={data} onChange={((e)=>setData(e.target.value))}></input>
</>
}
Now, inside your App.jsx:
const [childs, setChilds] = useState([]);
return (
<button onClick={() => setChilds([...childs, {data: {`child${childs.length}`, showChild: true} }])}>add child</button>
{
childs.length &&
childs.map(child => {
if(child.showChild){
return (
<Child initialData={child.data} />
<button onClick={() => {let newChildsArray = childs.forEach(item =>{if(item.data === child.data){child.showChild = false}} ) setChilds(newChildsArray)}}>show {child.data}</button>
)
}
}
)
Some of the concepts I used here was Rest Operator, Literal function, and Controlled Component, if you want to search further.
The better approach for this type of problem is not to use separate useState for every child.
But, to use one useState which itself is an array of objects.
For this, you can add and manipulate as per your required wish.

React - how to map buttons in a list to the object they are in?

I have the following class Component which reads data from localstorage.
The Localstorage has an array of objects. Those objects are rendered in a list as you can see below. In each list item there is a button I added in the code. If a user clicks the <ExtendButton /> I want to extend the {el.infoDays} of 7 days.
Can anyone help me with that, or at least with binding the button to the object it is in, so that if a user clicks the button I will get the whole object (where the button is in) displayed in the console.log?
I tried the following, I tried with e.target, this, etc. The onExtendBtnClick method is not well written.
let uniqid = require('uniqid');
class History extends Component {
state = {
loans: []
};
componentDidMount() {
const rawInfos = localStorage.getItem('infos');
const infos = JSON.parse(rawInfos);
this.setState({
infos: infos
});
}
render() {
const {infos} = this.state;
return (
<Container>
<Header />
<div>
<ul>
{infos.map((el) => (
<li key={uniqid()}>
<small>Requested date: {el.infoRequestTime}</small><br />
<div>
<span ><em>Requested amount</em><em>{el.infoAmount} €</em></span>
<span><em>In days</em><em>{el.infoDays}</em></span>
</div>
<spa>Give back: {el.infoCost} €</span>
<ExtendButton />
</li>
))}
</ul>
</div>
</Container>
);
}
}
export default History;
And I have also the button component:
class ExtendButton extends Component {
onExtendBtnClick = () => {
console.log(this)
};
render() {
return (
<button
className="extend-button"
onClick={this.onExtendBtnClick}
>
Extend for 1 week
</button>
);
}
}
export default ExtendButton;
Have your button component take in an onClick prop and set that on its own internal button:
class ExtendButton extends Component {
onExtendBtnClick = () => {
this.props.onClick();
};
render() {
return (
<button
className="extend-button"
onClick={this.onExtendBtnClick}
>
Extend for 1 week
</button>
);
}
}
Then just pass an onClick function to your component:
<ExtendButton onClick={() => {console.log(el)}} />

Functional Component unable to render return value based on props values

Goal: To implement a Toast Message modal (using Functional Component) which will show or hide based on the props value (props.showToastModal) within the return of ToastModal component
Expected: Using props.showToastModal directly would determine if Toast appears
Actual: Modal does not appear based on props.showToastModal
Here's the code:
Parent Component
class Datasets extends Component {
constructor(props) {
super(props)
this.state = {
showToastModal: false,
toastModalText: ''
}
}
toggleOff = () => {
this.setState({ showToastModal: false, toastModalText: '' })
}
render() {
{this.state.showToastModal && (
<ToastModal
showToastModal={this.state.showToastModal}
toastModalText={this.state.toastModalText}
toggleOff={this.toggleOff}
/>
)}
}
}
Child Component
This works:
const ToastModal = (props) => {
const isOpen = props.showToastModal
return (
<div className={`${css.feedbackModal} ${isOpen ? css.show : css.hide}`}>
{props.toastModalText}
<i
className={`bx bx-x`}
onClick={() => props.toggleOff()}
/>
</div>
)
}
export default ToastModal
But this doesn't (using the props value directly):
const ToastModal = (props) => {
return (
<div className={`${css.feedbackModal} ${props.showToastModal ? css.show : css.hide}`}>
{props.toastModalText}
<i
className={`bx bx-x`}
onClick={() => props.toggleOff()}
/>
</div>
)
}
export default ToastModal
Using a const isOpen = props.showToastModal works as expected instead. I am confused why this happens. Is this is a React Lifecycle issue, or a case where it is bad practice to use props values which may be updated during the render?
Please try destructuring objects
const ToastModal = ({ showToastModal, toggleOff }) => {
return (
<div className={`${css.feedbackModal} ${showToastModal ? css.show : css.hide}`}>
{props.toastModalText}
<i
className={`bx bx-x`}
onClick={toggleOff}
/>
</div>
)
}
export default ToastModal

Update state by changing an entire object within state array

I'm trying to create rows of inputs that updates the state of my application. The app is currently only two components, TimeCalc and ItemsList. State is stored in TimeCalc, and ItemsList handles the conditional render (either show state, or show rows of input.
I have managed to fetch the edited and updated object, but I'm struggling to replace the updated object with the correct object in state. The objects contain the props _id, title, start and end, and preferably I'd like to search for the matching _id, and replace the entire object in the state. But seeing as the _id prop is a sibling prop of the other props, I'm not sure how to do this.
Here is TimeCalc:
import React from 'react';
import ItemsList from '../components/ItemsList';
const items = [
{
_id: '112233',
title: 'M1',
start: 900,
end: 1800
},
{
_id: '223344',
title: 'M2',
start: 1000,
end: 1900
}
];
export default class TimeCalc extends React.Component {
state = {
items
}
handleUpdate = (update) => {
}
render = () => {
return (
<div class="timeCalc flex center">
<ItemsList items={this.state.items} handleUpdate={this.handleUpdate}/>
</div>
)
}
}
And here is ItemsList:
import React from 'react';
export default class ItemsList extends React.Component {
state = {
editing: null
}
toggleEditing = (itemId) => {
this.setState({
editing: itemId
})
}
handleEditItem = () => {
let itemId = this.state.editing;
this.handleItemsUpdate({
_id: itemId,
title: this.refs[`title_${itemId}`].value,
start: this.refs[`start_${itemId}`].value,
end: this.refs[`end_${itemId}`].value,
})
}
handleItemsUpdate = (update) => {
console.log(update);
this.props.handleUpdate(update);
this.setState( { editing: null } );
}
renderItemOrEditField = (item) => {
if(this.state.editing === item._id) {
return <li key={`editing-${item._id} `} class="list-item flex row">
<input
onKeyDown={this.handleEditField}
type="text"
class="form-input"
ref={`title_${item._id}`}
name="title"
defaultValue={item.title}
/>
<input
onKeyDown={this.handleEditField}
type="text"
class="form-input"
ref={`start_${item._id}`}
name="start"
defaultValue={item.start}
/>
<input
onKeyDown={this.handleEditField}
type="text"
class="form-input"
ref={`end_${item._id}`}
name="end"
defaultValue={item.end}
/>
<button onClick={this.handleEditItem} label="Update Item"/>
</li>
} else {
return <li
onClick = {this.toggleEditing.bind(null, item._id)}
key = {item._id}
class = "list-position">
{` ${item.title} & ${item.start} && ${item.end} && ${item._id}`}
</li>
}
}
render = () => {
return (
<ul class="itemsList">
{this.props.items.map((item) => {
return this.renderItemOrEditField(item);
})}
</ul>
)
}
}
I'm trying to recreate MeteorChef's "Click to Edit fields in React", but he's storing the state in a Meteor way.
I'd suggest that you move all of the state to the parent component. Keeping ItemsList stateless separates your concerns more neatly and makes the code much simpler to reason about. In this scenario, all ItemsList needs to be concerned with is presenting data based on the state stored in the parent. In your TimeCalc component, add an "editing" key to the state object, and add a method to handle updates from ItemsList:
state = {
items,
editing: null,
}
handleUpdate = (editing) => {
this.setState({ editing });
}
Then when rendering the child component, pass this handler function, the items array, and the "editing" key as props:
<ItemsList items={this.state.items} editing={this.state.editing} handleUpdate={this.handleUpdate}/>
Now ItemsList can become a stateless functional component, like this:
import React from 'react';
const renderItemOrEditField = (item, editing, handleUpdate) => (
{editing === item._id ? (
<li key={item._id} className="list-item flex row">
<input onKeyDown={handleUpdate(item._id}
// The rest of your content to render if item is being edited
) : (
// content to be rendered if item isn't being edited goes here
)}
);
export default const ItemsList = (props) => (
<ul className="itemsList">
{props.items.map((item) => {
return renderItemOrEditField(item, props.editing, props.handleUpdate);
})}
</ul>
);
This strategy of using "smart" container components that manage state and business logic and "dumb" presentational components can go a long way towards keeping your application more maintainable and less fragile. Hope this helps!

Resources