Obtaining actual array state - reactjs

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>
)};

Related

Why when you delete an element out of react array, the inner element you pass it to remains

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);
};

React hook error when changing size of rendered array

I get the following error: React Error: "Rendered more hooks than during the previous render", and it is because inside a mapped array that I render are buttons that have their own useState hooks.
So I have an array of projects that I render from a list. Initially, only 3 projects are shown, and clicking a button will load the whole list.
The problem is that inside project can be multiple ProjectButtons, and those ProjectButtons are components because I want to use special hover states using the useState hook.
But when I change the size of the project list being rendered, it throws an error because of the useState hook inside the ProjectButton component.
import { projects } from "../lib/projectList";
const Projects: FC = () => {
// Initially use a portion of the array
const [projectArray, setProjectArray] = useState(projects.slice(0, 3));
// Load the whole array on button click
const loadMoreProjects = () => {
setProjectArray([...projects]);
}
const ProjectButton = (button: { type: string, link: string }) => {
// Removing this useState hook fixes the problem, but I need it for my design
const [hoverColor, setHoverColor] = useState("#0327d8");
const handleMouseEnter = () => {
setHoverColor("white");
}
const handleMouseLeave = () => {
setHoverColor(original);
}
return (
<a href={button.link} rel="noreferrer" target="_blank" key={button.link}>
<button onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<WebsiteIcon className="projectButtonIcon" fill={hoverColor} />
<p>{button.type}</p>
</button>
</a>
);
}
return projectArray.map(project => (
...
<div className="projectLinks">
{project.buttons.map(button => ProjectButton(button))}
</div>
...
<Button onClick={loadMoreProjects}>Load More</Button>
));
}
You've defined ProjectButton within your Projects component, so you're breaking the rule of hooks - specifically "Only Call Hooks at the Top Level".
Move the ProjectButton component out of the scope of Projects and it will be happy.
This is happening because you are using hooks inside a function and it should be used directly inside a component.
This can solved if you create ProjectButton as a component instead of function.
Here is the updated code:
import { projects } from "../lib/projectList";
const ProjectButton = (button) => {
// Removing this useState hook fixes the problem, but I need it for my design
const [hoverColor, setHoverColor] = useState("#0327d8");
const handleMouseEnter = () => {
setHoverColor("white");
};
const handleMouseLeave = () => {
setHoverColor(original);
};
return (
<a href={button.link} rel="noreferrer" target="_blank" key={button.link}>
<button onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<WebsiteIcon className="projectButtonIcon" fill={hoverColor} />
<p>{button.type}</p>
</button>
</a>
);
};
const Projects: FC = () => {
// Initially use a portion of the array
const [projectArray, setProjectArray] = useState(projects.slice(0, 3));
// Load the whole array on button click
const loadMoreProjects = () => {
setProjectArray([...projects]);
}
return projectArray.map(project => (
...
<div className="projectLinks">
{project.buttons.map((button) => (
<ProjectButton {...button} />
))}
</div>
...
<Button onClick={loadMoreProjects}>Load More</Button>
));
}

Components not re render on state change

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])

Duplicate components with state values [Updated code for problem]

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.

How to target a specific item to toggleClick on using React Hooks?

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.

Resources