how to create new state on the fly - reactjs

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.

Related

Passing data from child to parent component in React

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

A method triggers a console.log from another component

When I click on bet now the function triggers a console.log from another component. betNow should group all the inputs from stake in one common array but when I click on it it renders the console log from stake and includes all the values that I typed into one array. Everything works but not as I wish. The parent component should display the common array with all the values. I do not understand why it is happening.Could anyone explain me why is reacting like that? Thanks in advance
Parent Component
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
import FilterMenu from "./selectButton";
import FetchRandomBet from "./fetchRandomBets";
function Betslip() {
const data = [
{
value: 0,
label: "No Filter"
},
{
value: 1,
label: "Less than two"
},
{
value: 2,
label: "More than two"
},
]
const [selectedValue, setSelectedValue] = useState(0);
const [allStakes, setAllStakes] = useState([]);
const handleChange = obj => {
setSelectedValue(obj.value);
}
const betNow = () => {
const stakes = localStorage.getItem("stakes");
const jsnStake = JSON.parse(stakes) || [];
setAllStakes([...allStakes, jsnStake]);
}
return (
<div className="betslip">
<div className="betslip-top">
<h1 className="text">BETSLIP</h1>
<p className="text-two">BET WITH US!</p>
<div>
<FilterMenu
optionsProp={data}
valueProp={selectedValue}
onChangeProp={handleChange}
/>
</div>
</div>
<div>
<FetchRandomBet
valueProp={selectedValue}
/>
</div>
<Button
onClick={betNow}
className="betnow"
variant="contained"
>
Bet Now!
</Button>
</div>
);
}
export default Betslip;
Child Component
import React, { useState, useEffect } from 'react';
function Stake() {
const [stakes, setStakes] = useState([]);
const addStake = (e) => {
e.preventDefault();
const newStake = e.target.stake.value;
setStakes([newStake]);
};
useEffect(() => {
const json = JSON.stringify(stakes);
localStorage.setItem("stakes", json);
}, [stakes]);
console.log(stakes)
return (
<div>
<form onSubmit={addStake}>
<input
style={{
marginLeft: "40px",
width: "50px"
}}
type="text"
name="stake"
required
/>
</form>
</div>
);
}
export default Stake;
You have this console.log in you function that will run every time the component is rendered, since it´s outside of any function:

How do I return an input tag from a function?

I have a button on my webpage, and I want an input tag to appear, whenever the user clicks that button. I earlier tried something like this:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [showInput, setShowInput] = useState(false);
const handleClick = () => setShowInput(true);
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{showInput ? <input type="text" /> : ""}
</div>
);
}
But this only worked once. I want it to add an input tag whenever the user clicks that button. How do I do so?
Instead of maintaining the number of input elements in the state, i suggest that you maintain an object in the state that is initially empty. Once the button is clicked to add an input, you could update the object with a key-value pair that represents the new input element.
State after adding one input could like as shown below:
{
input1: { value: '' }
}
Similarly, as more inputs are added, more objects will be added in the state.
This will allow your input elements to be controlled components and will allow you to handle the onChange event with only one event handler function.
Demo
let counter = 1;
function App() {
const [inputs, setInputs] = React.useState({});
const handleClick = () => {
const inputName = "input" + counter++;
const inputObj = { value: "" };
setInputs({ ...inputs, [inputName]: inputObj });
};
const handleChange = (event) => {
const { name, value } = event.target;
setInputs({ ...inputs, [name]: { ...inputs[name], value } });
};
return (
<div className="App">
<button onClick={handleClick}>Add Input</button>
<div className="inputContainer">
{Object.keys(inputs).map((inputName) => {
const { value } = inputs[inputName];
return (
<input
key={inputName}
name={inputName}
value={value}
onChange={handleChange}
placeholder={inputName}
/>
);
})}
</div>
</div>
);
}
ReactDOM.render(<App/>, document.querySelector('#root'));
.App {
font-family: sans-serif;
text-align: center;
}
.inputContainer {
display: flex;
flex-direction: column;
max-width: 300px;
margin: 10px auto;
}
input {
margin: 5px;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Make showInput a number that defaults to 0.
Have handleClick increment that number instead of just setting true.
Outside the return expression, create an array. With a for loop, push inputs (until you reach the number specified) into the array.
Replace the line where you add the input to the JSX with that array.
Something like ...
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [inputs, setInputs] = useState([]);
const handleClick = () => setInputs([...inputs, ""]);
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{inputs.map(i => <input type="text"/>)}
</div>
);
}
Now you can also store your input values into your inputs state for further processing.
I leave formatting up to you ... !
import React, { useState } from "react";
export default function App() {
const initialValue = [{ value: "first input" }];
const [userInputs, setUserInputs] = useState(initialValue);
const handleClick = () => {
const updatedInputs = [...userInputs, { value: "new input"}]
setUserInputs(updatedInputs);
}
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{userInputs.map((el, i) => (
<input type="text" value={el.value} />
))}
</div>
);
}
All of the implementation above is correct, But I also have my own implementation.
import React, { useState, Fragment } from "react";
export default function App() {
const [showInputs, setInputs] = useState([]);
const handleClick = () => {
setInputs((prev) => {
const i = prev.length + 1;
return [
...prev,
<Fragment key={i}>
<input type="text" />
<br />
</Fragment>
];
});
};
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
<br />
{showInputs}
</div>
);
}

Why doesn't the function work after I have changed the state in another component?

Those are the three components I’m using, excluding the component that displays them in the DOM, but that’s not needed.
Here I have a Parent and two Child components.
For some reason when the popup is active, and I click the Refresh Child 1 Component button, it changes the state back to Child1, but I lose the functionality within that component. So the popUpToggle function stops working.
It was working fine before. When I click the Refresh Child 1 Component again however, it starts working. Why is that?
import React, { useState, useEffect } from 'react';
import Child1 from './child1'
import Child2 from './child2'
const Parent = () => {
const [display, setDisplay] = useState('');
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(!popUp)
console.log('PopUp Toggle ran')
};
const reRenderComponent= () => {
setRenderCount(renderCount + 1);
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
popUpToggle();
console.log('reRenderComponent ran, and the key is ' + renderCount)
};
useEffect(() => {
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
}, [])
return (
<div>
<button
style={{position: 'fixed', zIndex: '999', right: '0'}}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp ? <Child2 popUpToggle={popUpToggle}/> : null}
{display}
</div>
);
};
export default Parent;
Child 1:
import React from 'react';
const Child1 = ({ popUpToggle, renderCount }) => {
return (
<>
<button onClick={popUpToggle}>
Pop Up Toggle function
</button>
<h1>Child 1 is up, count is {renderCount}</h1>
</>
);
};
export default Child1;
Child 2:
import React, { useState } from 'react';
const Child2 = ({ popUpToggle }) => {
return (
<div
style={{
backgroundColor: 'rgba(0,0,0, .7)',
width: '100vw',
height: '100vh',
margin: '0',
}}
>
<h1>Child 2 is up</h1>
<h2>PopUp active</h2>
<button onClick={popUpToggle}>Toggle Pop Up</button>
</div>
);
};
export default Child2;
setDisplay(<Child1 /*etc*/ />);
Putting elements into state is usually not a good idea. It makes it very easy to cause bugs exactly like the one you're seeing. An element in state never gets updated, unless you explicitly do so, so it can easily refer to stale data. In your case, i think the issue is that the child component has a stale reference to popUpToggle, which in turn has an old instance of popUp in its closure.
The better approach, and the standard one, is for your state to contain just the minimal data. The elements get created when rendering, based on the data. That way, the elements are always in sync with the latest data.
In your case it looks like all the data already exists, so we don't need to add any new state variables:
const Parent = () => {
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(prev => !prev);
};
const reRenderComponent = () => {
setRenderCount(prev => prev + 1);
popUpToggle();
};
return (
<div>
<button
style={{ position: "fixed", zIndex: "999", right: "0" }}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp && <Child2 popUpToggle={popUpToggle} />}
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
</div>
);
};

Pass a variable in a functional component to a class component

I've a ToysPage.js with a class component and a SearchFeature.js with a functional component (child of ToysPage).
I made a searchBar in SearchFeature.js with hooks and with console.log I can see that it works. But how can I pass the const filteredToys to the state toysFiltered in ToysPage.js?
ToysPage.js
import React, { Component } from "react";
export default class ToysPage extends Component {
state = {
toys: undefined,
toysFiltered: undefined,
};
//Here I call all the toys through Axios and insert them in toys and toysFiltered
return (
<>
<div className="container">
<div className="row">
<SearchSide
toysFiltered={this.state.toysFiltered}/>
<ToysSide toys={this.state.toysFiltered} />
</div>
</div>
</>
);
}
}
function SearchSide({ toysFiltered }) {
return (
<>
<div className="col-3">
<SearchFeature toysFiltered={toysFiltered} />
</div>
</>
);
}
SearchFeature.js
import React, { useState, useMemo } from "react";
export default function SearchFeature({ toysFiltered }) {
const [query, setQuery] = useState("");
const [filteredToys, setFilteredToys] = useState(toysFiltered);
useMemo(() => {
const result = toysFiltered.filter((toy) => {
return toy.titulo.toLowerCase().includes(query.toLowerCase());
});
setFilteredToys(result);
}, [toysFiltered, query]);
return (
<div className="form-group">
<label>Search a Toy</label>
<input
type="text"
className="form-control"
placeholder="Search"
value={query}
onChange={(e) => {
setQuery(e.target.value);
}}
/>
</div>
);
}
You have some state in a child component, SearchFeature, that you would like in the parent component ToysPage. The usual pattern for this is to lift the state out of SearchFeature, which you have partially done.
You're almost doing this correctly. You pass a list of filtered toys into SearchFeature, but that list is only being used as a default value for a duplicate state variable in SearchFeature, and changes don't flow up to the parent:
// filteredToys has the real state. toysFiltered is just a default value.
const [filteredToys, setFilteredToys] = useState(toysFiltered);
Instead, leave the state variable in the parent, and send changes up from the search component.
Also, useMemo shouldn't have side effects. I've changed it to useEffect to be safe. There is a way to do this with useMemo, but that's out of the scope of the question.
in ToysPage.js
Add a setter function:
<SearchSide
toysFiltered={this.state.toysFiltered}
setToysFiltered={
newFilterList => this.setState({...this.state, toysFiltered: newFilterList})}
/>
Pass it through from SearchSide into SearchFeature:
function SearchSide({ toysFiltered, setToysFiltered }) {
return (
<>
<div className="col-3">
<SearchFeature toysFiltered={toysFiltered} setToysFiltered={setToysFiltered} />
</div>
</>
);
}
SearchFeature.js
export default function SearchFeature({ toysFiltered, setToysFiltered }) {
const [query, setQuery] = useState("");
useEffect(() => {
const result = toysFiltered.filter((toy) => {
return toy.titulo.toLowerCase().includes(query.toLowerCase());
});
setToysFiltered(result);
}, [setToysFiltered, query]);
// ...
}

Resources