React component state not updating with passed props - javascript

I have created an Accordion component which has data(object) and expanded(boolean) as props.
expanded props is used to set the expanded/collapsed state of this component passed as a prop.
const DeltaAccordion = ({ index, data, expanded = true }) => {
Accordion component also has an internal state
const [isExpanded, setIsExpanded] = useState(expanded);
which is used for expanding/collapsing the accordion.
Below is my complete component
Accordion.jsx
import React, { useState } from "react";
// styles
import styles from "./index.module.scss";
const Accordion = ({ index, data, expanded = true }) => {
// state
const [isExpanded, setIsExpanded] = useState(expanded);
console.log(data.name, `prop-val==${expanded}`, `inner-state==${isExpanded}`);
return (
<div
className={`${styles.container} caption ${isExpanded && styles.expanded}`}
>
<div className={styles.header} onClick={() => setIsExpanded(!isExpanded)}>
<div>{data.name}</div>
<div>Click</div>
</div>
<div className={styles.content}>
{data.newValue && (
<div className={styles.newValue}>
<span>{data.newValue}</span>
</div>
)}
{data.oldValue && (
<div className={styles.oldValue}>
<span>{data.oldValue}</span>
</div>
)}
</div>
</div>
);
};
export default Accordion;
The parent component has a list of data and it loops through the list to create an accordion for each data item.
App.js file
{dataList.map((data, index) => (
<Accordion data={data} expanded={!collpaseAll} index={1} />
))}
Here goes problem
There is also a button in my App.js file which is for either expanding/collapsing all the accordions together.
But when I pass its value as prop expanded to the accordion component then this value is not getting applied to the internal isExpanded state of the accordion component.
Here is live running code at codesandbox: https://codesandbox.io/s/goofy-nobel-qfxm1?file=/src/App.js:635-745

Inside the Accordion
const [isExpanded, setIsExpanded] = useState(expanded);
This line will take first time(on first render) value. To assign it next time(rerender) value you need to add a effect
useEffect(() => {
setIsExpanded(expanded);
}, [expanded]);
And in your case, you can use the props expanded directly inside Accordion, you dont need to take it in local state.

Related

Trying to filter an array using checkboxes and useState, checkbox doesn't stay checked

i have an array, called reportsData, then i need to filter it, generating some checkboxes with each of them having a label based on each name that comes from another array (emittersData), so basically i set it like this:
const [searchUser, setSearchUser] = useState<string[]>([])
const mappedAndFiltered = reportsData
.filter((value: any) =>
searchUser.length > 0 ? searchUser.includes(value.user.name) : true
)
Then i render my checkboxes like this:
function EmittersCheckboxes () {
const [checkedState, setCheckedState] = useState(
new Array(emittersData.length).fill(false)
)
const handleOnChange = (position: any, label: any) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
)
setSearchUser((prev) =>
prev.some((item) => item === label)
? prev.filter((item) => item !== label)
: [...prev, label]
)
setCheckedState(updatedCheckedState)
};
return (
<div className="App">
{emittersData.map((value: any, index: any) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
className="h-4 w-4 focus:bg-indigo border-2 border-gray-300 rounded"
type="checkbox"
id={`custom-checkbox-${index}`}
name={value.Attributes[2].Value}
value={value.Attributes[2].Value}
checked={checkedState[index]}
onChange={() => handleOnChange(index, value.Attributes[2].Value)}
/>
<label className="ml-3 font-medium text-sm text-gray-700 dark:text-primary" htmlFor={`custom-checkbox-${index}`}>{value.Attributes[2].Value}</label>
</div>
</div>
</li>
);
})}
</div>
)
}
And on the react component i am rendering each checkbox, that is a li, like:
<ul><EmittersCheckboxes /></ul>
And i render the mappedAndFiltered on the end.
Then it is fine, when i click each generated checkbox, it filters the array setting the state in setSearch user and the array is filtered.
You can check it here: streamable. com /v6bpk6
See that the filter is working, the total number of items in the array is changing based on the checkbox selected (one or more).
But the thing is that each checkbox does not become 'checked', it remains blank (untoggled).
What am i doing wrong, why doesnt it check itself?
You've defined your EmittersCheckboxes component inside another component. and every time that the parent component renders (by state change) your internal component is redefined, again and again causing it to lose it's internal state that React holds for you.
Here's a simplified example:
import React, { useState } from "react";
function CheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>CheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
export default function App() {
const [counter, setCounter] = useState(1);
function InternalCheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>InternalCheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
return (
<>
<InternalCheckboxeComponent />
<CheckboxeComponent />
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
</>
);
}
There's the App (parent component) with its own state (counter), with a button to change this state, clicking this button will increase the counter, causing a re-render of App. This re-render redefines a new Component named InternalCheckboxeComponent every render.
The InternalCheckboxeComponent also has an internal state (checkedState).
And there's an externally defined functional component named CheckboxeComponent, with this component React is able to hold its own state, because it's not redefined (It's the same function)
If you set the state of each to be "checked" and click the button, this will cause a re-render of App, this will redefine the InternalCheckboxeComponent function, causing React to lose its state. and the CheckboxeComponent state remains in React as it's the same function.

reactjs dynamically adding a form input

I am working with a form in react, and what I would like is that when I click a button, I add a new component which is just an input to the screen. It all mostly works, as planned. The issue is with the following: the layout is that I have one main component, which then displays a child component. That child component is called from a map of a useState. (More after code snippet)
This is the code of the main component:
import React, { useState } from "react";
import SingleProfile from "./individual_profile";
const ProfileInformation = (props) => {
console.log("proflie render");
const [ProfilesBoolean, setProfilesBoolean] = useState(false);
const [profiles, setProfiles] = useState(props.Data['profiles'])
const FieldAdd = (event)=>{
event.preventDefault();
const copy = profiles;
copy.push({Network:'',url:''})
return(copy)
}
function CreateInput(){
return profiles.map((data, index) =><SingleProfile index={index} data={data} />)
}
const accordion = (event) => {
const NextElement = event.target.nextElementSibling;
if (!event.target.className.includes("display")) {
NextElement.style.maxHeight = NextElement.scrollHeight + "px";
} else {
NextElement.style.maxHeight = 0;
}
};
return (
<div className="AccordionItem">
<div
className={
ProfilesBoolean ? "AccordionHeader-display" : "AccordionHeader"
}
onClick={(e) => setProfilesBoolean(!ProfilesBoolean)}
id="ProfileForm"
>
Profiles
</div>
<div className="AccordionContent">
<div className="AccordionBody">
{
profiles.map((data, index) => (
<SingleProfile index={index} data={data} />
))
}
<button id="ProfileAdd" onClick={(e) => {setProfiles(FieldAdd(e))}}>
Add a profile
</button>
</div>
</div>
</div>
);
};
export default ProfileInformation;
When I click the button and onClick fires FieldAdd() the useState updates, with a new empty object as expected. However, it does not appear inside my <div className="AccordionBody"> as I would expect it to.
The following code is used to display components, by opening and closing the child div. When it is open is when you see the child components and the add button. If I click the div, to close and then click again to re-open it, the new child component appears.
<div
className={ProfilesBoolean ? "AccordionHeader-display" : "AccordionHeader"}
onClick={(e) => setProfilesBoolean(!ProfilesBoolean)}
id="ProfileForm"
>
Profiles
</div>;
Is it possible to have the child component appear without having to close and re-open the div?
Your clickHandler FieldAdd is incorrect. You are mutating the state directly which will not cause re-render.
use setProfiles to update the state in the clickHandler. Like this
const FieldAdd = (event)=>{
setProfiles(prev => [...prev, {Network:'',url:''}])
}
Trigger the onClick like this
<button id="ProfileAdd" onClick={(e) => {FieldAdd(e)}}>
Add a profile
</button>
...

export Hooks in React for Nested Components?

I'm exporting hooks with nested components so that the parent can toggle state of a child. How can I make this toggle work with hooks instead of classic classes or old school functions?
Child Component
export let visible;
export let setVisible = () => {};
export const ToggleSwitch = () => {
const [visible, setVisibile] = useState(false);
return visible && (
<MyComponent />
)
}
Parent
import * as ToggleSwitch from "ToggleSwitch";
export const Parent: React.FC<props> = (props) => {
return (
<div>
<button onClick={() => ToggleSwitch.setVisible(true)} />
</div>
)
}
Error: Linter says [setVisible] is unused variable in the child... (but required in the parent)
You can move visible state to parent like this:
const Child = ({ visible }) => {
return visible && <h2>Child</h2>;
};
const Parent = () => {
const [visible, setVisible] = React.useState(false);
return (
<div>
<h1>Parent</h1>
<Child visible={visible} />
<button onClick={() => setVisible(visible => !visible)}>
Toggle
</button>
</div>
);
};
If you have many child-components you should make more complex logic in setVisible. Put object to useState where properties of that object will be all names(Ids) of child-components
as you know React is one-way data binding so if you wanna pass any props or state you have only one way to do that by passing it from parent to child component and if the logic becomes bigger you have to make it as a global state by using state management library or context API with react hooks use reducer and use effect.

Is my entire React component re-rendering unnecessarily when the state changes?

I am trying to create an accordion component using React, but the animation is not working.
The basic idea is, I believe, pretty standard, I am giving each item body a max-height of 0 which is affected by adding a show class to an element. I am able to select and show the item I want, but the animation to slide in/out is not working.
With the Chrome dev tools open, when I click on one of the items I can see that the whole "accordion" element is flashing, which leads me to believe that the whole element is being re-rendered. But I am unsure why this would be the case.
Here is the relevant Accordion component:
import React, { useState } from "react";
const Accordion = ({ items }) => {
const [selectedItem, setSelectedItem] = useState(0);
const AccordionItem = ({ item, index }) => {
const isOpen = index === selectedItem;
return (
<div className="accordion-item">
<div
onClick={() => {
setSelectedItem(index);
}}
className="accordion-header"
>
<div>{item.heading}</div>
</div>
<div className={`accordion-body ${isOpen ? "show" : ""}`}>
<div className="accordion-content">{item.body}</div>
</div>
</div>
);
};
return (
<div className="accordion">
{items.map((item, i) => {
return <AccordionItem key={i} item={item} index={i} />;
})}
</div>
);
};
export default Accordion;
And here is a codepen illustrating the problem:
https://codesandbox.io/s/heuristic-heyrovsky-xgcbe
of course, its going to re-render. When ever you call setSelectedIem, state changes and hence react re-renders on state change to exhibit that change.
Now if you place this
const [selectedItem, setSelectedItem] = useState(0);
inside Accordion Item, it would just re-render accordion item, but would mess up your functionality.

React with Hooks: how to correctly update state after a click event?

In my JSX, I'm, mapping through an array of objects (imported from a local JS file) to display a set of icons with a key, id and alt tag.
I use hooks to set a state to an empty string. I want to use an onClick event (passed to the HeroIcons component) to replace this state with the id of the clicked icon (that id is a string). Here's the code:
import React, { useState } from "react";
import HeroImages from "../images/HeroImages";
import HeroIcons from "../components/HeroIcons";
import HeroShowcase from "../components/HeroShowcase";
const Heroes = () => {
const [hero, currentHero] = useState("");
const setCurrentHero = e => {
currentHero(e.target.id);
console.log(hero);
};
return (
<div className="row">
<div className="col-heroes">
<ul className="hero-list">
{/* map function below */}
{HeroImages.map(({ id, src, altTag }) => (
<HeroIcons
key={id}
id={id}
src={src}
altTag={altTag}
setCurrentHero={setCurrentHero}
/>
))}
</ul>
</div>
<div className="col-showcase">
<HeroShowcase />
</div>
</div>
);
};
export default Heroes;
Inside the heroIcons component:
import React from "react";
const HeroIcons = props => {
return (
<li key={props.id} id={props.id} onClick={props.setCurrentHero}>
<img src={props.src} alt={props.altTag} />
</li>
);
};
export default HeroIcons;
When clicking on an icon (created by the map function), the id isn't logged to the console. However, when I furiously click it many times, sometimes an id DOES get logged. This gives me a hint that this click event could be causing the map function to re-run and prevent the normal console log
How could I fix this this issue?
First you have to use e.currentTarget.id instead of e.target.id so you get the id of current image.
const setCurrentHero = e => {
currentHero(e.currentTarget.id);
console.log(hero);
};
Second useState Hook needs you to handle the callback to use log the value of the current state, while it doesn't accept the callback like setState.
You can use useEffect but It would better if you use the value of e.currentTarget.id;
This is because you hero is not updated at the time of console so you need to use useEffect hook when that value is updated
const setCurrentHero = e => {
currentHero(e.target.id);
console.log(hero);
};
useEffect(() => {
console.log('Hero', hero);
}, [hero]);
why not just set the value in the render:
<HeroIcons
key={id}
id={id}
src={src}
altTag={altTag}
setCurrentHero={setCurrentHero(id)}
/>

Categories