I'm having trouble deciding the difference between these two patterns of rendering in React. Hopefully someone can shed some light on this matter.
Pattern 1: React's Conditional Render
https://facebook.github.io/react/docs/conditional-rendering.html
class List extends React.Component {
state = {
menu: false,
}
handleMouseOver = () => {
this.setState({
menu: true
});
}
handleMouseLeave = () => {
this.setState({
menu: false
});
}
render() {
const { menu } = this.state;
return (
<li
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
>
{menu && <Menu />}
</li>
);
}
}
Pattern 2: display: none
.menu {
display: none;
}
.li:hover .menu {
display: block;
}
const List = () => (
<li className="li"><Menu className="menu"/></li>
);
Question:
If I need to render 100 of these items in a single page, which pattern should I go for?
How can I determine which pattern is better?
Are there any performance benefit of using one over the other?
I tend to use display: none in situations where there's a simple condition to show something (e.g hover, etc).
If it's a bit more complex (e.g. click a checkbox to hide an element) then I go with conditional rendering.
The reason behind this is that I don't want to cause state changes and trigger an update for something as trivial as a hover state, and don't want to fiddle around with obscure css classes for things that will have to involve code anyway.
Again, this is my personal preference.
TL;DR: CSS if super simple (e.g. hover), conditional​ render if more logic involved
Related
I am creating a list of divs, which was created with map.
function renderButtons(){
const options = [...Array(10).keys()] // returns [0,1,2...9]
return _.map(options, (option)=> renderOption(option))
}
function renderOption(option:number){
return (
<div className="option-container" onClick={() => setLowerContainerVisible(true)}>
<img alt="" src={"./images/feedback-icons/icon-"+option.toString()+".svg"}/>
{option+1}
</div>
)
}
this renders a list of divs, and I was able to change each div background, when hover, like this:
.option-container{
width: 76px;
height: 100px;
background-color: #7777ff;
display: flex;
}
.option-container:hover{
background-color: #adadf3;
}
I wish to be able to click on a div, and change its background color to white. everything I try will change the background of all the 10 divs to white. How can I make it so only the clicked one is changed?
I suggest that you use renderOption and renderButtons as two components rather than plain functions. In the RenderButtons component, you can use some state to maintain which item is clicked, and within RenderOption you can control whether the background color is white or not based on wehther or not the current rendered button is the clicked option. In your .map() method, you can use component rather than a function call <RenderOption option={option} ... />.
See example below:
const {useState} = React;
function RenderButtons() {
const [clickedItem, setClickedItem] = useState(-1);
return Array.from(
Array(10).keys(),
option => <RenderOption isClicked={clickedItem === option} option={option} setClicked={setClickedItem}/>
);
}
function RenderOption({isClicked, option, setClicked}) {
const handleClick = () => {
// setLowerContainerVisible(true) / other code to run when you click
setClicked(option); // set to current option
}
return (
<div className={"option-container " + (isClicked ? "clicked" : "")} onClick={handleClick}>
{option+1}
</div>
)
}
ReactDOM.render(<RenderButtons />, document.body);
.option-container {
width: 76px;
height: 100px;
background-color: #7777ff;
display: flex;
}
.option-container.clicked, .option-container:hover {
background-color: #adadf3;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
The className is a little messy as it involves a ternary, to clean this up it might be worth looking into using a node package such as classnames which allows you to easily build a list of classes based on conditions.
Do it in the event listener function:
<div className="option-container" onClick={highlightAndsetLowerContainerVisible}>
function highlightAndsetLowerContainerVisible(event){
event.preventDefault();
setLowerContainerVisible(true)
event.currentTarget.style.backgroundColor = "red";
}
You then might also want to reset the background color of the other divs
I have dynamic list of 'posts' where I wanted to truncate the text (if it goes beyound a certain # of lines) and show a Read More button that users can click to show the entire text.
In VueJS, I decided to attach a ref to the div I want to append the button to (if the text is truncated).
The component is just a button really but it has some stylings and behaviors I want to copy over. The reason why this got more complicated then it needs to (bad thing?) is because I'm doing the truncating with CSS. I understand that using Javascript might have been easier.
So anyways, how can I dynamically add a component to this div (or its parent) using Javascript only? My own reference to the location would be the ref item.
// code after the promise of getting the posts has resolved in the created() hook
.then(() => {
const posts = this.$refs.posts
posts.forEach(p => {
if (this.Overflown(f)) {
// I want to attach a component (AwesomeButtonComponent) to this p div.
}
}
})
And for clarity:
HTML:
<div v-for="post in posts">
<div class="postBody ref="posts">{{ post.body }}</div>
</div>
isOverflown(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
The CSS that is truncating the text
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
ALTERNATIVE POSSIBILITIES:
This button will only have ONE functionality, so it being a component is not important and adding styling isn't so difficult.
.then(() => {
const announcementBodies = this.$refs.announcementBody;
announcementBodies.forEach(a => {
if (this.isOverflown(a)) {
const button = document.createElement('button');
button.innerText = 'Click Me';
button.onClick = 'doThis';
a.parentElement.appendChild(button);
}
});
In which case the difficult part would be to add a v-on:click directive to that button and then target that specific tag to remove the clamp css attribute.
Following our discussion into comments, I'll show you 2 way you can do this and try to explain the difference between them and let you decide how you will achieve this.
EXAMPLE ONE
The first example is the shortest I could do. This will need every post to have an isOverflow attribute. There is many way to do it client or server side. The other example will not need it.
<div v-for="post in posts">
{{(post.isOverflow == true) ? post.body.substring(0,3)+'...' : post.body}} <button v-on:click="post.isOverflow = !post.isOverflow">{{(post.isOverflow == true) ? 'SHOW MORE' : 'SHOW LESS'}}</button>
</div>
This is not beautiful, but it work and it let you understand that you can manipulate the post inside the v-for. Each button will be automatically associate with the right post, so when you will click it, only the post associated will be affected.
EXAMPLE TWO
The other example i'll give you is by creating a new component for each post. Let's start with the v-for:
<post-component v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></post-component>
And the new component:
<template>
<div v-bind:class="{'postsBody': isOverflow}">
{{post.body}}
<button v-on:click="changeState()">{{(post.isOverflow) ? 'SHOW LESS' : 'SHOW MORE'}}</button>
</div>
</template>
<script>
export default {
props: {
post:{}
},
data() {
return {
isOverflow: true
}
},
methods: {
changeState: function() {
this.isOverflow = !this.isOverflow;
}
},
}
</script>
<style> //Please, put this in a CSS file, it's only for the example purpose.
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
CONCLUSION
In the end, both of them will have the same result. The difference is what you prefer. I tried to show you two different way to let you understand how things work with Vue. Let me know if you need more explanations.
I'm trying to rewrite one of my JS plugins to react, as a way of learning.
I have a panel that when hidden/shown needs to be updated with several classnames as well as some that need to wait for a css animation to complete (why the timer).
How should I do this in a react way? Using querySelector to change classnames seem very wrong..?
Detailed explanation
When showPanel is triggered the following need to happen
the body/html element need updated css (hence me adding classes)
an existing overlay fades in (adding a class to that)
the modal div is displayed (adding a class for that)
the modal div is told to be active AFTER the animation has been run (hence the timer and class "am-animation-done")
What I preferably would like to have/learn is best practice to do this in reactjs. I'm thinking a toggle state that when triggered sets the state to visible/hidden and if set to "visible" the class changes below happens. My biggest issue is the timer thing.
showPanel = () => {
document.querySelector('body').classList.add('am-modal-locked');
document.querySelector('html').classList.add('am-modal-locked');
document.querySelector('.am-overlay').classList.add('fadein');
document.querySelector('.am-modal').classList.add('am-show');
const timer = setTimeout(() => {
document.querySelector('.am-modal').classList.add('am-animation-done');
}, 500);
return () => clearTimeout(timer);
};
hidePanel = () => {
document.querySelector('.am-modal').classList.remove('am-show');
document.querySelector('.am-modal').classList.remove('am-animation-done');
document.querySelector('.am-overlay').classList.add('fadeout');
const timer = setTimeout(() => {
document.querySelector('.am-overlay').classList.remove('fadein');
document.querySelector('.am-overlay').classList.remove('fadeout');
document.querySelector('body').classList.remove('am-modal-locked');
document.querySelector('html').classList.remove('am-modal-locked');
}, 500);
return () => clearTimeout(timer);
};
Source code updated for clarifaction
This is a lot simpler in React, here's an example with hooks
function Panel() {
const [hidden, setHidden] = useState(false);
const toggleCallback = useCallback(() => setHidden(hidden => !hidden), []);
const cls = hidden ? 'hide' : 'show';
return (
<div className={cls}>
<button onClick={toggleCallback}>Toggle</>
</div>
)
}
You can use the state to dinamically change classnames inside your component
className={this.state.isPanelVisible}
And maybe instead of setting it as boolean you can set your variable to the class you need at the moment.
React working with virtual DOM so you should play with state and change class of that particular element like below example:
constructor(props) {
super(props);
this.state = {'active': false, 'class': 'album'};
}
handleClick(id) {
if(this.state.active){
this.setState({'active': false,'class': 'album'})
}else{
this.setState({'active': true,'class': 'active'})
}
}
<div className={this.state.class} onClick={this.handleClick.bind(this.data.id}>
<p>Data</p>
</div>
In very basic use cases you can write the logic inside of the class itself.
<div className={active ? "active" : "disabled"} />
In more advanced cases I would suggest to use something like classnames package.
https://www.npmjs.com/package/classnames
<div className={classNames({ foo: true, bar: true, boo: false })} />
Which would result in div having class foo and bar
This is mainly regarding one component, but if you really have to affect class of something so far away as body would be, than you are most likely gonna need useQuerySelector or put the state somewhere high and then base the logic on it.
Yes that's not a very good way to do it. Instead you should use state variables to toggle your classes as well. There is no need to manually manipulate DOM. The you can set up your timeout inside the callback of your first setState to change state again.
Maybe something like this:
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
class1: 'on',
class2: 'off'
}
}
toggle = () => {
this.setState({class1: 'off'}, () => {
setTimeout(() => {
this.setState({class2: 'on'})
}, 2000)
})
}
render() {
const {class1, class2} = this.state;
return (
<div>
<h1 className={`${class1} ${class2}`} onClick={this.toggle}>Class toggle</h1>
</div>
)
}
}
use this approach for change styling on state change
<div className={`rest_of_classes ${isClassChange ? 'change_class_name': ''}`} />
I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.
I'm attempting to do an animation with React and CSS classes. I have created a live demo, if you visit it and click the Start button you will see the text fade in and up one by one. This is the desired animation that I am after.
However, there seems to be issues of consistency when you hit Start multiple times and I cannot pinpoint why.
The Issue: Below is a recording of the issue, you can see the number 1 is not behaving as expected.
live demo
The process: Clicking Start will cancel any previous requestAnimationFrame' and will reset the state to it's initial form. It then calls the showSegments() function with a clean state that has no classNames attached to it.
This function then maps through the state adding a isActive to each segment in the state. We then render out the dom with a map and apply the new state.
This should create a smooth segmented animation as each class gets dropped one by one. However when i test this in Chrome (Version 56.0.2924.87 (64-bit)) and also on iOS, it is very inconsistent, sometimes it works perfectly, other times the first DOM element won't animate, it will just stay in up and visible it's completed transitioned state with "isActive".
I tried to replicate this issue in safari but it worked perfectly fine, I'm quite new to react so i am not sure if this is the best way to go about things, hopefully someone can offer some insight as to why this is behaving quite erratic!
/* MotionText.js */
import React, { Component } from 'react';
import shortid from 'shortid';
class MotionText extends Component {
constructor(props) {
super(props);
this.showSegments = this.showSegments.bind(this);
this.handleClickStart = this.handleClickStart.bind(this);
this.handleClickStop = this.handleClickStop.bind(this);
this.initialState = () => { return {
curIndex: 0,
textSegments: [
...'123456789123456789123456789123456789'
].map(segment => ({
segment,
id: shortid.generate(),
className: null
}))
}};
this.state = this.initialState();
}
handleClickStop() {
cancelAnimationFrame(this.rafId);
}
handleClickStart(){
cancelAnimationFrame(this.rafId);
this.setState(this.initialState(), () => {
this.rafId = requestAnimationFrame(this.showSegments);
});
}
showSegments() {
this.rafId = requestAnimationFrame(this.showSegments);
const newState = Object.assign({}, this.state);
newState.textSegments[this.state.curIndex].className = 'isActive';
this.setState(
{
...newState,
curIndex: this.state.curIndex + 1
},
() => {
if (this.state.curIndex >= this.state.textSegments.length) {
cancelAnimationFrame(this.rafId);
}
}
);
}
render(){
const innerTree = this.state.textSegments.map((obj, key) => (
<span key={obj.id} className={obj.className}>{obj.segment}</span>
));
return (
<div>
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<hr />
<div className="MotionText">{innerTree}..</div>
</div>
)
}
}
export default MotionText;
Thank you for your time, If there any questions please ask
WebpackBin Demo
Changing the method to something like this works
render(){
let d = new Date();
const innerTree = this.state.textSegments.map((obj, key) => (
<span key={d.getMilliseconds() + obj.id} className={obj.className}>{obj.segment}</span>
));
return (
<div>
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<hr />
<div className="MotionText">{innerTree}..</div>
</div>
)
}
How this helps is that, the key becomes different than previously assigned key to first span being rendered. Any way by which you can make the key different than previous will help you have this animation. Otherwise React will not render it again and hence you will never see this in animation.