I have a view that has multiple cards with values inside each one.
I can duplicate a card. However, this doesn't preserve the values in the duplicated card.
If Card A has values inside of its state, how can I tell Parent View to create a Card B with Card A values?
Here is my view. I'm rendering multiple cards
{items.map((item, i) => (
{
'VideoCard': <div><Droppable onChange={droppableResponse} /><CreateVideoCard onChange={cardActionResponse} i={i} /></div>,
'AudioCard': <div><Droppable onChange={droppableResponse} /><CreateAudioCard onChange={cardActionResponse} i={i}></CreateAudioCard></div>,
'DescriptionCard': <div><Droppable onChange={droppableResponse} /><CreateDescriptionCard onChange={cardActionResponse} i={i}></CreateDescriptionCard></div>,
'BreakdownCard': <div><Droppable onChange={droppableResponse} /><CreateDescriptionCard onChange={cardActionResponse} i={i}></CreateDescriptionCard></div>,
'Terms': <div><Droppable onChange={droppableResponse} /><CreateTermsCard onChange={cardActionResponse} i={i}></CreateTermsCard></div>
}[item]
))}
When a card sends a response such as delete or duplicate, I go into my cardActionResponse function. Here is the duplicate part:
if (event[0] == 'duplicate') {
//Retrieve item from items array at location event[1]
console.log(items[event[1]])
var data = JSON.stringify(event[2])
console.log(data)
//Retrieve information about which card was duplicated. Not just AudioCard but AudioCard w/ info
items.splice(event[1], 0, items[event[1]])
forceUpdate()
}
Var data returns the duplicated data that I need to insert into my new duplicated card. However this will require me to change up my current structure of my cardList
const [items, setItems] = useState([
'VideoCard',
'AudioCard' ,
'DescriptionCard',
'BreakdownCard',
'Terms',
]);
In addition, I need the ability to send to each card what location it is (i) and I was able to do that with the items.map function.
I'm thinking about creating something like
<CreateVideoCard onChange={cardActionResponse} i={I} data={data} />
Then in my createvideocard component, I'll check if the data is empty, if not I'll swap the states.
Is there an easier way I can duplicate components with state inside? This seems a little extra?
if you post some code it will be helpful to answer.
Anyway, as you have not done that, here is a proposed soln.
pass a callback function say onDuplicate() from parent to cardList--> card. card component should call that function onClick of duplicate.
this onDuplicate function will update the parent state.. here is a sample example
const data = [{
id: 1111,
name:"some name",
//THERE ARE OTHER FIELDS I HAVE NOT ADDED
}]
const CardList = (props) => (
<div>
{props.profiles.map(profile => <Card key={profile.id} onDuplicate={props.onDuplicate} profile={...profile}/>)}
</div>
);
class Card extends React.Component {
duplicate = ()=>{
console.log(this.props)
this.props.onDuplicate({...this.props.profile,id: 222})
}
render() {
const profile = this.props.profile;
return (
<div className="github-profile">
<img src={profile.avatar_url} />
<div className="info">
<div className="name">{profile.name}</div>
<div className="company">{profile.company}
</div>
<div><button onClick={this.duplicate}>duplicate</button></div>
</div>
</div>
);
}
}
class App extends React.Component {
state = {
profiles: data,
};
onDuplicate = (profileData)=>{
console.log("+++++++"+profileData.name+""+profileData.company)
this.setState(prevState => ({
profiles: [...prevState.profiles, profileData],
}));
}
render() {
return (
<div>
<CardList profiles={this.state.profiles} onDuplicate={this.onDuplicate}/>
</div>
);
}
}
The duplicate function should be inside the card and then call a function in the parent passing the current state.
const Parent = () => {
const handleDuplicate = (state) => {
// your logic to create the new card with the initial state
}
<CardA onDuplicate={handleDuplicate}/>
}
const CardA = (props) => {
const { initialState, onDuplicate } = props;
const [state, setState] = useState(initialState);
const duplicate = () => {
onDuplicate(state);
}
}
The key function I had to use was React.cloneElement
I changed my items array to
const [items, setItems] = useState([
<CreateVideoCard onChange={cardActionResponse}/>,
<CreateAudioCard onChange={cardActionResponse}></CreateAudioCard>,
<CreateDescriptionCard onChange={cardActionResponse}></CreateDescriptionCard>,
<CreateDescriptionCard onChange={cardActionResponse}></CreateDescriptionCard>,
<CreateTermsCard onChange={cardActionResponse}></CreateTermsCard>,
]);
and rendered it with
{items.map((item, i) => (
React.cloneElement(item, {i: i})
))}
When I received a request to duplicate I would use cloneElement to add a data prop to my items array:
items.splice(event[1] + 1, 0, React.cloneElement(items[event[1]], { data: data }))
Then in my card constructor. I checked if props.data was undefined. If it was, I would swap out the state and if it wasn't, it would create base structure.
Related
I've been exploring React in practice and now I've stuck.
There is the app that contains: "FlashCardsSection" (parent component)
export const FlashCardsSection = () => {
const [cardsList, setCardsList] = useState([]);
const cardAddition = (item) => {
setCardsList(() => [item, ...cardsList]);
};
const handlerDeleteCard = (cardsList) => {
console.log(cardsList);
};
return (
<section className = "cards-section">
<div className = "addition-card-block">
<PopUpOpenBtn cardAddition={cardAddition} cardsList={cardsList}
handlerDeleteCard = {handlerDeleteCard}
/>
</div>
<CardsBlock cardsList={cardsList}/>
</section>
)
};
At the "PopUpOpenBtn" there is the "AddFlashCardBtn" component which adds the "CreatedFlashCard" component to the "cardsList" property by the "cardAddition" function. Each "CreatedFlashCard" component contains the "FlashCardRemoveBtn" component. I would like to implement the function which deletes the card where occurred click on the "FlashCardRemoveBtn" component which calls the "handlerDeleteCard" function. I need an actual array version at any click but I get: when a click at the first card - empty array, the second card - array with one element, third - array with two elements, etc.
The "PopUpOpenBtn" contains intermediate components which related with creation and addition flashcard, so I passed the properties "cardsList", "handlerDeleteCard" through all to the "FlashCardRemoveBtn".
export const FlashCardRemoveBtn = (props) => {
let {handlerDeleteCard, cardsList} = props;
return(
<button className = "remove-btn" onClick={() => {
handlerDeleteCard(cardsList);
}}>Remove
</button>
)};
In a react component I have an array of things. I iterate through that array display the name of the thing in a plain div, then pass each element to another component to display details.
What's happening: if I delete an element from anywhere except the bottom (last array element) the header that is displayed in the main element containing the array is correct (the one I clicked "delete" on disappeared), but the "body" (which is another component) remains. Instead, the inner component is acting as if I deleted the last element of the array and kind of "moves" up the array.
It's hard to describe in words. See example below. Delete the top element or one of the middle ones and see how the header for the section starts not matching the contents.
I'm trying to understand why this is happening.
(EDIT/NOTE: State IS needed in the child component because in real life it's a form and updates the object being passed in. I Just removed the updating here to make the example shorter and simpler)
Example code (delete the middle element of the array and see what happens):
https://codesandbox.io/s/confident-buck-dodvgu?file=/src/App.tsx
Main component:
import { useState, useEffect } from "react";
import InnerComponent from "./InnerComponent";
import Thing from "./Thing";
import "./styles.css";
export default function App() {
const [things, setThings] = useState<Thing[]>([]);
useEffect(() => resetThings(), []);
const resetThings = () => {
setThings([
{ name: "dog", num: 5 },
{ name: "cat", num: 7 },
{ name: "apple", num: 11 },
{ name: "book", num: 1}
]);
};
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
return (
<div className="App">
{things.map((thing, index) => (
<div key={`${index}`} className="thing-container">
<h2>{thing.name}</h2>
<InnerComponent
thing={thing}
index={index}
onDelete={onDeleteThing}
/>
</div>
))}
<div>
<button onClick={resetThings}>Reset Things</button>
</div>
</div>
);
}
Inner component:
import { useEffect, useState } from "react";
import Thing from "./Thing";
interface InnerComponentParams {
thing: Thing;
index: number;
onDelete: (indexToDelete: number) => void;
}
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
const [name, setName] = useState(thing.name);
const [num, setNum] = useState(thing.num);
return (
<div>
<div>Name: {name}</div>
<div>Num: {num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
export default InnerComponent;
You are creating unnecessary states in the child component, which is causing problems when React reconciles the rearranged Things. Because you aren't setting the state in the child component, leave it off entirely - instead, just reference the prop.
export const InnerComponent: React.FC<InnerComponentParams> = ({
thing,
index,
onDelete
}) => {
return (
<div>
<div>Name: {thing.name}</div>
<div>Num: {thing.num}</div>
<div>
<button onClick={(e) => onDelete(index)}>Delete Me</button>
</div>
</div>
);
};
The other reason this is happening is because your key is wrong here:
{things.map((thing, index) => (
<div key={`${index}`}
Here, you're telling React that when an element of index i is rendered, on future renders, when another element with the same i key is returned, that corresponds to the JSX element from the prior render - which is incorrect, because the indicies do not stay the same. Use a proper key instead, something unique to each object being iterated over - such as the name.
<div key={thing.name}
Using either of these approaches will fix the issue (but it'd be good to use both anyway).
This is also wrong. You're removing everything except the index.
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things];
newThings.splice(indexToDelete, 1);
setThings(newThings);
};
Use filter:
const onDeleteThing = (indexToDelete: number) => {
const newThings = [...things].filter(
(thing, index) => index !== indexToDelete
);
setThings(newThings);
};
I have two components in my project.
One is App.jsx
One is Child.jsx
In App, there is a state holding array of child state objects. All create, manage, and update of the child state is through a set function from parent.
So, Child componment doesnt have its own state. For some reason, it is my intention not to have child own state, because it matters.
However, at some points, I found it that passing data into child would be hard to manage.
Question:
So, is there a way that let the child to access the data from parent by themselves not by passing down, while having them be able to update the state like my code.
People say useContext may work, but I dont quite see how.
A example to illustrate would be prefect for the improvement.
<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">
const {StrictMode, useState} = React;
function getInitialChildState () {
return {
hidden: false,
id: window.crypto.randomUUID(),
text: '',
};
}
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>
);
}
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>
Usually context in react is used for a global things like themes and authentication. But you can use it for actions too.
const AppContext = createContext();
In App:
const getChildState = (id) => ...
const updatedChildState = (id, updatedChildState) =>
<AppContext.Provider value={{ getChildState, updatedChildState }}>...
In ChildListItem:
const { getChildState, updatedChildState } = useContext(AppContext);
const state = getChildState(id);
const setText = (text) => updatedChildState(id, { text });
You need to pass down the id anyway so ChildListItem know what to get and what to update:
<ChildListItem key={state.id} id={state.id} />
Working example
Update
Regarding your question about the theme and authentication examples let's first cite the documentation:
In a typical React application, data is passed top-down (parent to
child) via props, but such usage can be cumbersome for certain types
of props (e.g. locale preference, UI theme) that are required by many
components within an application. Context provides a way to share
values like these between components without having to explicitly pass
a prop through every level of the tree.
Examples:
Material UI uses ThemeProvider to pass down theme object. Thus all components can access the palette, typography etc.
Many apps uses context to pass down information about a currently logged in user. So all components can render accordingly.
You could try jotai atoms
App.jsx
import { atom, useAtom } from 'jotai'
export const itemAtom = atom('')
export const App = () => {
const [item] = useAtom(itemAtom)
<p>{item}</p>
...
}
Child.jsx
export const Child = () => {
const [, setItem] = useAtom(itemAtom)
<input onChange={(e) => setItem(e.value)} />
...
}
I am trying to update a state of a list. And need to rerender the list as per the ids state. But array takes reference value and everytime I update the ids with setIds, I am unable to get the updated list as the component is not rerendered. Anyway to achieve this. I have tried adding const [idsLength, setIdsLength] = useState(0) and passing and updating it in the list. But I feel there is a better approach for this.
const Component1 = () => {
const [ids, setIds] = useState([]);
const handleIdClick = () => {
const temp = ids
temp.push(ids.length)
setIds(temp)
console.log(ids)
}
return(
<div>
<div onClick={handleIdClick}>
Id Adder
</div>
{ids.map(id=>{
return(
<div key={id}>
{id}
</div>
)
})}
</div>
)
}
you just need to use useEffect hook
useEffect(() => {
console.log("ids :" + ids);
}, [ids]);
You can create and pass in a new array with updated contents to setIds
const handleIdClick = useCallback(() => {
setIds([...ids, ids.length]);
}, [ids])
I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.