I want to add underline to menu item when its active. All works fine, but when I click on an item, its previous classes received from the ReactTransitionGroup add-on component are reset. For example when I click second item the classes will be reset and only active will remain. I want the active class to be insert to existing without cleaning the previous ones.
The .active has ::after pseudo-class
const NavItems = (props) => {
const items = ["section1", "section2", "section3", "section4", "section5"];
const [activeItem, setActive] = useState(0);
return (
<>
<NavItemsOverlay open={props.open} />
<ScollLinks open={props.open}>
{items.map((item, index) => {
return (
<CSSTransition
in={props.open}
key={index}
timeout={{enter: 100 * index, exit: 0 }}
classNames="fade ">
<Link
className={activeItem === index ? " active" : ""}
onClick={() => setActive(index)} >
{item}
</Link>
</CSSTransition>
);
})}
</ScollLinks>
</>
);
};
You appear to be running into issue #318, which is still open. The person posting the issue thinks it's a bug, and the CSSTransition documentation does say:
A few details to note about how these classes are applied:
They are joined with the ones that are already defined on the child component, so if you want to add some base styles, you can use className without worrying that it will be overridden.
...so yeah, that sounds like a bug.
The best way to solve it would be to fork the project, fix the bug, and send a PR. :-)
A really hacky way to work around it would be to use a data-* attribute instead of a class:
<Link
data-cls={activeItem === index ? " active" : ""}
onClick={() => setActive(index)} >
{item}
</Link>
And then in the CSS, instead of:
.active::after {
/* ... */
}
You'd have
[data-cls~=active]::after {
/* ... */
}
That uses class-like attribute matching to match that element.
Related
I have a component that renders a list, and this list of items can be increased. I'm wrapping the list around a <TransitionGroup> to animate new items being added.
There's a message when we don't have items on the list. However, even when items are added, the message won't go away. I tested it without <TransitionGroup> and it works normally. I have a minimal example of the problem on CodeSandbox.
The code for the component is:
const ItemsList = ({ items }) => {
return (
<TransitionGroup component="ul" timeout={400}>
{items.length === 0
? "There is no items"
: items.map((item, i) => (
<CSSTransition key={i} classNames="item-animation">
<li key={i}>{item}</li>
</CSSTransition>
))}
</TransitionGroup>
);
};
After adding an element to the list, the message should disappear, but it's still visible.
My guess is there's something different react-transition-group uses on render, not rendering the full component as we write it. But how to fix this problem?
Try rendering Transition Group component only if items.length !=0
So your code should look something like this:
items.length !=0 ? `render your component` : "There is no item"
I am rendering a list of students, some of which have failed their exams. For those who have failed their exams, I display a red square behind their avatars.
Whenever I hover over a student's avatar I want to display the subject that student has failed. My issue at the moment is that I display the subjects for all students, not only the one I've hovered over.
How can I display only the mainSubject for the student who's avatar I hovered on?
Here is a link to my code sandbox: Example Link
I solved it like following.
Get the id of the hovered student. Match this id from the list of students you render. if its match then show the subjects
Also, I renamed the hook
add key prop
you can check this too https://codesandbox.io/s/zealous-bhaskara-mi83k
const [hoveredStudentId, setHoveredStudentId] = useState();
return (
<>
{students.map((student, i) => {
return (
<div className="student-card" key={i}>
<p>
{student.firstName} {student.lastName}
</p>
{student.passed === false ? (
<>
<img
id={student.id}
src={student.picture}
className="student-avatar fail"
onMouseEnter={e => {
setHoveredStudentId(e.currentTarget.id);
}}
onMouseLeave={e => {
console.log(e.currentTarget.id);
setHoveredStudentId(0);
}}
alt="avatar"
/>
{hoveredStudentId === student.id && (
<div className="subject-label">{student.mainSubject}</div>
)}
</>
) : (
<img
src={student.picture}
className="student-avatar"
alt="avatar"
/>
)}
</div>
);
})}
</>
);
Issue is that you have a list of students but only 1 flag to show/hide subjects.
Solution: 1
Maintain a list of flag/student. So you will have n flags for n students. Simple way for this is to have a state in a way:
IStudentDetails { ... }
IStudentStateMap {
id: string; // uniquely identify a syudent
isSubjectVisible: boolean;
}
And based on this flag isSubjectVisible toggle visibility.
Updated code
Solution 2:
Instead of handling it using React, use CSS tricks. Note this is a patch and can be avoided.
Idea:
Wrap Student in a container element and add a class onHover on elements on elements that needs to be shown on hover.
Then use CSS to show/hide those elements.
.student-container .onHover {
display:none;
}
.student-container:hover .onHover{
display: block;
}
This way there wont be rerenders and no need for flags.
Updated Code
However, solution 1 is better as you have more control and when you are using a UI library, its better to let it do all mutation and you should follow its ways.
I display a list of foos and when i click on some link more results i keep the existing foos and i append to them the new ones from my api like bellow
const [foos, setFoos] = useState([]);
...
// api call with axios
...
success: (data) => {
setFoos([ ...foos, ...data ])
},
Each <Foo /> component run the animation above
App.js
...
<div className="foos-results">
{ foos.map((foo, index) => <Foo {...{ foo, index }} key={foo.id}/>) }
</div>
...
Foo.js
const Foo = ({ foo, index }) => <div className="circle">...</div>
animation.css
.circle {
...
animation: progress .5s ease-out forwards;
}
The problem is when i append the new ones then the animation is triggered for all the lines of <Foo />.
The behavior expected is that the animation is triggered just for the new ones and not starting over with the existing ones too.
UPDATE
We have found the origin of the problem (it's not related to the uniqueness of key={foo.id})
if we change
const Foo = ({ foo, index }) => <div className="circle">...</div>
to
const renderFoo = ({ foo, index }) => <div className="circle">...</div>
And App.js to
...
<div className="foos-results">
{ foos.map((foo, index) => renderFoo({ foo, index })) }
</div>
...
It works
So why is this behavior like this in react ?
here is a sandbox based on #Jackyef code
This is quite an interesting one.
Let's look at the sandbox provided in the question.
Inside App, we can see this.
const renderItems = () => (
<div>
{items.map((item, index) => (
<div className="item" key={item.id}>
<span>
{index + 1}. {item.value}
</span>
</div>
))}
</div>
);
const Items = () => renderItems();
return (
<div className="App">
<h1>List of items</h1>
<button onClick={addItem}>Add new item</button>
<Items />
</div>
);
Seems pretty harmless right? The problem with this is that Items is declared in the App render function. This means that on each render, Items actually is now a different function, even though what it does is the same.
<Items /> is transpiled into React.createElement, and when diffing, React takes into account each components' referential equality to decide whether or not it is the same component as previous render. If it's not the same, React will think it's a different component, and if it's different, it will just create and mount a new component. This is why you are seeing the animation being played again.
If you declare Items component outside of App like this:
const Items = ({ items }) => (
<div>
{items.map((item, index) => (
<div className="item" key={item.id}>
<span>
{index + 1}. {item.value}
</span>
</div>
))}
</div>
);
function App() { /* App render function */}
You will see everything works as expected. Sandbox here
So, to summarise:
Referential equality matters to React when diffing
Components (function or class that returns JSX) should be stable. If they change between renders, React will have a hard time due to point number 1.
I don't think there is a way to disable this re-rendering animation, but I think there is a workaround that could solve this issue.
As we know that each div's css is reloaded every time, so the solution I can think of, is to create another css class rule (let this class be named 'circle_without_anim') with same css as class 'circle' but without that animation and while appending new div, just before appending change class of all divs that have class name 'circle' to 'circle_without_anim' that would make the changes and css to previous divs but just without that animation and the append this new div with class 'circle' making it the only div that have animation.
Formally the algorithm will be like:
Write another css class(different name for example prev_circle) with same rules as 'circle' but without the animation rule.
In Javascript just before appending new div with class 'circle', change class of all previous divs that have class named 'circle' to newly created class 'prev_circle' that do not have animation rule.
Append the new div with class 'circle'.
Result: It would give an illusion that the CSS of previous divs is not being reloaded as the css is same but without animation, but the new div has different css rule (animation rule) which is going to be reloaded.
With this code:
const Items = () => renderItems();
...
<Items />
React has no chance of knowing that Items in the current render is the same component as Items in the previous render.
Consider this:
A = () => renderItems()
B = () => renderItems()
A and B are different components, so if you have <B /> in the current render and <A /> instead of <B /> in the previous render, React will discard the subtree rendered by <A /> and render it again.
You are invoking React.createElement (since <Items /> is just a JSX syntax sugar for React.createElement(Items, ...)) every render, so React scraps the old <Items /> in the DOM tree and creates it again each time.
Check out this question for more details.
There are two solutions:
create Items component outside of the render function (as Jackyef suggested)
use render function ({ renderItems() } instead of <Items />)
I am making a memory game, CodeSandbox, and I am trying to accomplish the following:
Get the cards to be dealt initially face down (icon hidden so to speak)
onClick, the icon on the card that is clicked is revealed, none of the others is revealed
When the second card is clicked that icon is revealed, if they match the cards are removed anyway (that part works) but if they don't, the icons are hidden again.
I tried adding a "buttonMask" class in the parent div but it overrides my existing class. I tried display: none in the parent div also, no luck.
Other attempts included adding a state property to the Card.js component this.state.isHidden: true and trying to toggle the visibility. I read the docs, This, SO, and eddyerburgh. I'm stuck on this condition render, everything else works great, this piece got me
This is my conditional for dealing the cards
class Cards extends Component {
constructor(props) {
super(props);
}
render() {
const card = (
<div style={sty}>
{this.props.cardTypes ? (
this.props.cardTypes.map((item, index) => {
return (
<button style={cardStyle} value={item} key={index}>
{" "}
{item}{" "}
</button>
);
})
) : (
<div> No Cards </div>
)}
</div>
);
return <Card card={card} removeMatches={this.props.removeMatches}
/>;
}
Originally, when I first tried this, I was passing state to all the cards, not just individual cards, that was why I was not able to toggle them individually.
This was the original render in Cards.js
if (this.state.hidden) {
return (
<button
key={card + index}
style={btn}
onClick={this.revealIcon}
id={card}
>
<div style={btnMask}>{card}</div>
</button>
);
} else {
return (
<button
key={card + index}
style={btn}
onClick={this.revealIcon}
id={card}
>
<div>{card}</div>
</button>
);
}
You can conditionally render an element with an inline if with logical &&:
return(
<div>
{this.props.shouldRenderTheThing &&
<span>Conditionally rendered span</span>}
</div>
);
I have a list of elements created by using an array in react. On user click how can I make the clicked element active (by adding a CSS class) while making the other elements inactive (by removing the active class)?
My rendering of elements looks like this.
{this.props.people.map(function(person, i){
<div className='media' key={i} onClick={state.handleClick.bind(state,i,state.props)}>
<item className="media-body">{person.name}</item>
</div>
}
When the user clicks on one of these elements an active class will be added to the clicked 'media' element making the clicked element 'media active' while removing the 'active' class from the previously clicked element??
constructor(props) {
super(props);
this.state = { activeIndex: 0 };
}
handleClick(index, props) {
// do something with props
// ...
// now update activeIndex
this.setState({ activeIndex: index });
}
render() {
return (
<div>
{
this.props.people.map(function(person, index) {
const className = this.state.activeIndex === index ? 'media active' : 'media';
return (
<div className={className} key={index} onClick={handleClick.bind(this, index, this.props)}>
<item className="media-body">{person.name}</item>
</div>
);
}, this)
}
</div>
);
}
For the sake of clean code I personally would suggest you creating subcomponents to add functionality to mapped elements.
You could create a small subcomponent which simply returns the element which you want to add functionality to just like this :
...
this.state = {
active: false
}
...
return(
<div className=`media ${this.state.active ? 'active' : ''` onClick={()=>{this.setState({active: true})}}>
<item className="media-body">{this.props.name}</item>
</div>
)
...
And in your map function you simply pass the contents as properties:
{this.props.people.map(function(person, i){
<SubComponent key={i} {...person} />
}
That way you stay with clean code in the "root" component and can add complexity to your subcomponent.
In your handleClick method you could store in the component's state the clicked person (looks like the collection is of people). Then set the className conditionally based on, say, the person's id.
You can set the className using something like:
className={this.state.clickedPersonId === i ? 'media media--clicked' : 'media'}
(NB This is using i, the index of the item in the people array; you may want to use something a little more explicit, like the person's real Id.)