How to handle mouse event in stateless component in React - reactjs

All:
I wonder if I use stateless component, how can I handle mouse event to change component style, for example:
const Com = (props) => {
var hltStyle = false;
function highlight(){
// I do not know what put here
}
var hltStyle = {
backgroundColor: !hltStyle?"lightgreen": "tomato"
}
return (
<div style={hltStyle} onMouseOver={ highlight } >HOVER ME</div>
)
}
What I want just hover this component and change background color. There is some other logic inside highlight, that is why I can not simply use CSS
Thanks

You can achieve that using something like this
const Com = () => {
function over(e){
e.target.style.backgroundColor="red";
}
function out(e){
e.target.style.backgroundColor='';
}
return <div onMouseOver={over} onMouseOut={out}>HOVER ME </div>;
}
Anyway, if you feel that you need to declare some variables to use them as the state, you should use a normal component instead of a stateless one.
jsfiddle

What about using classic CSS for simple hover effects?
<div class="el-to-hover"></div>
somewhere in css-file:
.el-t-hover {
background: transparent
transition: background .3s ease-in-out;
}
.el-to-hover:hover {
background: red
}

Related

Change classname dynamically in React

I'm trying to change a class name dynamically in react.
I am importing the classes from a related css file like this:
import classes from "./Board.module.css";
And in my Board component, I want to return a classname based on something I generate.
It can be "card" "card activate " "card disable", and I have 3 classes in my css file
.card {
do card something
}
.card.activate {
do card activate something
}
.card.disable {
do card disable something
}
How can I do it because concatenating doesn't seem to be working
Edit: I am trying to to this:
import "./Board.module.css"
const Card = (props) => {
const itemClass =
"card" + (props.item.stat ? " active " + props.item.stat : "");
return (
<div className={itemClass} onClick={() => props.clickHandler(props.id)}>
<label>{props.item.content}</label>
</div>
);
};
export default Card;
and the CSS is :
.card.wrong{
background-color: red;
}
.card.correct{
background-color: green;
}
.card.active{
transform: rotateY(0);
}
I am doing so that every time I click a card, I change its class name to active and something and based on that I do a color but the class is undefined so I don't know what to do
Have you tried looking the element in inspect mode to see whether the classes are getting attached without spaces?
Eg. card active is getting applied as cardactive.
However, here's how you'd do it :
// Board.module.css
.card{
do card somthing
}
.card.activate{
do card activate somthing
}
.card.disable{
do card disable somthing
}
Here's a snippet of the component which uses the classes dynamically.
A dummy condition is used to simulate the truthy/falsy behavior that changes the class. Replace this using your custom logic.
Using string literals you can dynamically toggle between the classes as shown below:
import "./Board.module.css";
const MyComponent = () => {
const [condtion, setCondition] = useState(false); // this is the condition that toggles the class
return (
<>
<span className={`card ${condition ? 'activate' : 'disable'}`}>Hello world</span>
</>
Note: the activate and disable class-Names are strings and not variables.

How to change the background of a styled.div if the window is scrolled to the top?

I would like to change the background of a div if the window is scrolled to the top. I have figured out how to determine that in React, and when console.logging my isAtTop variable it will change based on the scroll, however the actual div never seems to receive as the styled.div doesn't change colors. I have made a codePen: https://codesandbox.io/s/xenodochial-chebyshev-4poi3?file=/src/App.js
import "./styles.css";
import React from 'react'
import styled from 'styled-components'
export default function App() {
let isAtTop = true;
let bg = `linear-gradient(to right, #797cd2, #393e9e)`;
function handleScroll() {
isAtTop = window.scrollY === 0;
isAtTop
? (bg = `linear-gradient(to right, #797cd2, #393e9e)`)
: (bg = "white");
console.log({ isAtTop });
}
React.useEffect(() => {
console.log("UseEffect Run");
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
const ColorChangeDiv = styled.div`
background: ${bg};
position: sticky;
width: 100%;
top: 0;
height: 100px;
`
return (
<>
<ColorChangeDiv>
{isAtTop.toString()} ? Hello my background should be {bg}
</ColorChangeDiv>
<div style={{height: '500rem'}}></div>
</>
);
}
I changed your code a little, now I will explain what exactly changed.
Made the functions arrow, so it looks cleaner
Wrapped the scroll event handler in useCallback to exclude unnecessary calls to useEffect, since now this function is in useEffect dependencies and it is a good practice to specify all dependencies used in useEffect.
The background color is now stored in the state. You were storing in a variable, the component is not re-rendered on changing the let variable. This means that even changing the value of the variable, the component will not display itself with the new values. We need a rerender. A change in state causes a rerender. This means that if you want to change the component depending on the changed values, then store them in a state to call the rerender :)
I brought the styled component out, it looks cleaner and plus, it makes no sense to keep it inside the APP component, you can pass the values ​​to the styled component in props. Check out the example below, now the background is passed as props
You have long default background values. And it repeats itself. Such long repetitive things are better to be translated into constants with good understandable names. See I brought it up as DEFAULT_BG.
https://codesandbox.io/s/boring-pasteur-18v0r?file=/src/App.js:361-372
import React, { useCallback, useState, useEffect } from "react";
import styled from "styled-components";
const DEFAULT_BG = `linear-gradient(to right, #797cd2, #393e9e)`;
const ColorChangeDiv = styled.div`
background: ${(p) => p.bg};
position: sticky;
width: 100%;
top: 0;
height: 2000px;
`;
const App = () => {
const [bg, setBg] = useState(DEFAULT_BG);
const handleScroll = useCallback(() => {
setBg(window.scrollY === 0 ? DEFAULT_BG : "white");
}, []);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<>
<ColorChangeDiv bg={bg}>
Hello my background should be {bg}
</ColorChangeDiv>
<div style={{ height: "500rem" }}></div>
</>
);
};
export default App;

How can I add a fade-in animation for Nextjs/Image when it loads?

I'm using next/image, which works great, except the actual image loading in is super jarring and there's no animation or fade in. Is there a way to accomplish this? I've tried a ton of things and none of them work.
Here's my code:
<Image
src={source}
alt=""
layout="responsive"
width={750}
height={height}
className="bg-gray-400"
loading="eager"
/>
According to the docs I can use the className prop, but those are loaded immediately and I can't figure out any way to apply a class after it's loaded.
I also tried onLoad, and according to this ticket, it isn't supported:
https://github.com/vercel/next.js/issues/20368
NextJS now supports placeholder. You can fill the blurDataURL property with the base64 string of the image which you can easily get using the lib plaiceholder on getServerSideProps or getStaticProps. Then to make the transition smoothly you can add transition: 0.3s;
Quick sample:
export const UserInfo: React.FC<TUserInfo> = ({ profile }) => {
return (
<div className="w-24 h-24 rounded-full overflow-hidden">
<Image
src={profile.image}
placeholder="blur"
blurDataURL={profile.blurDataURL}
width="100%"
height="100%"
/>
</div>
);
};
export async function getServerSideProps(props: any) {
const { username } = props.query;
const userProfileByName = `${BASE_URL}/account/user_profile_by_user_name?user_name=${username}`;
const profileResponse = await (await fetch(userProfileByName)).json();
const profile = profileResponse?.result?.data[0];
const { base64 } = await getPlaiceholder(profile.profile_image);
return {
props: {
profile: {
...profile,
blurDataURL: base64,
},
},
};
}
index.css
img {
transition: 0.3s;
}
======== EDIT ==========
If you have the image in the public folder for ex, you don't need to do the above steps, just statically import the asset and add the placeholder type. NextJS will do the rest. Also, make sure to make good use of the size property to load the correct image size for the viewport and use the priority prop for above-the-fold assets. Example:
import NextImage from 'next/image'
import imgSrc from '/public/imgs/awesome-img.png'
return (
...
<NextImage
src={imgSrc}
placeholder='blur'
priority
layout="fill"
sizes="(min-width: 1200px) 33vw, (min-width: 768px) 50vw, 100vw"
/>
)
I wanted to achieve the same thing and tried to use the onLoad event, therefore. The Image component of nextJs accepts this as prop, so this was my result:
const animationVariants = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}
const FadeInImage = props => {
const [loaded, setLoaded] = useState(false);
const animationControls = useAnimation();
useEffect(
() => {
if(loaded){
animationControls.start("visible");
}
},
[loaded]
);
return(
<motion.div
initial={"hidden"}
animate={animationControls}
variants={animationVariants}
transition={{ ease: "easeOut", duration: 1 }}
>
<Image
{...p}
onLoad={() => setLoaded(true)}
/>
</motion.div>
);
}
However, the Image does not always fade-in, the onLoad event seems to be triggered too early if the image is not cached already. I suspect this is a bug that will be fixed in future nextJS releases. If someone else finds a solution, please keep me updated!
The solution above however works often, and since onLoad gets triggered every time, it does not break anything.
Edit: This solution uses framer-motion for the animation. This could also be replaced by any other animation library or native CSS transitions
You could try use next-placeholder to achieve this sort of effect
Yes, its possible to capture the event where the actual image loads. I found an answer to this on Reddit and wanted to repost it here for others like me searching for an anwser.
"To get onLoad to work in the NextJS image component you need make sure it's not the 1x1 px they use as placeholder that is the target.
const [imageIsLoaded, setImageIsLoaded] = useState(false)
<Image
width={100}
height={100}
src={'some/src.jpg'}
onLoad={event => {
const target = event.target;
// next/image use an 1x1 px git as placeholder. We only want the onLoad event on the actual image
if (target.src.indexOf('data:image/gif;base64') < 0) {
setImageIsLoaded(true)
}
}}
/>
From there you can just use the imageIsLoaded boolean to do some fadein with something like the Framer Motion library.
Source: https://www.reddit.com/r/nextjs/comments/lwx0j0/fade_in_when_loading_nextimage/

How state of a react hook must update if it has Map?

I have multiple(more than 15) div tags as tiles. I need to emphasis each one if mouse hover on it. So each tag has onMouseEnter/Leave functions as bellow.
<div
key={key}
onMouseEnter={onMouseEnter(key)}
onMouseLeave={onMouseLeave(key)}
>
...
</div>
Also I put each tiles key in a Map data structure.
const onMouseEnter = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.set(key, true)
setIsHover(newIsHover)
}
}
const onMouseLeave = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.delete(key)
setIsHover(newIsHover)
}
}
Since component is hook it put its state in a useState.
const [isHover, setIsHover] = useState(new Map())
What is happening here:
Always I enter a tile: onMouseEnter function called and its key added to map (as expected)
When I leave a tile: always onMouseLeave called but sometimes key is removed (as expected) and tile turned back to its normal shape but sometimes it does not(problem is here, in this situation map updated at setIsHover in onMouseLeave but it does not changed in the component!).
I think map updated as expected but when I move on new tile it does not understand that yet. So it overwrite it with what it has.
PS: example added. Move between tiles with high speed!
Like the class-based components, calls to update state are asynchronous and get queued up. Try using functional state updates to ensure these queued-up updates correctly update the previous state. This should fix race conditions between quick successive setIsHover calls with the same key.
Notice if you move slowly enough between tiles they correctly highlight and unhighlight, but more quickly (like a swipe) and 2 or more can get stuck until you again slowly exit the tile.
const onMouseEnter = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.set(key, true);
return newIsHover;
});
}
}
const onMouseLeave = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.delete(key);
return newIsHover;
});
}
}
But I should note that this is a lot of leg work for simply applying some component styling, especially hovering. It could more simply be achieved using CSS.
tileStyles.css
.tile {
background-color: lightgray;
border: 3px solid black;
height: 100px;
line-height: 100px;
margin: 10px;
text-align: center;
width: 100px;
}
.tile:hover {
border-color: red;
}
tile.jsx
import React from "react";
import { withStyles } from "#material-ui/core";
import "./tileStyles.css";
const styles = {
container: { display: "flex", width: "600px", flexWrap: "wrap" }
};
const Tiles = ({ classes: { container }, tiles }) => {
return (
<div className={container}>
{tiles.map((tl, key) => {
return (
<div className="tile" key={key} name={key}>
hi
</div>
);
})}
</div>
);
};
export default withStyles(styles)(Tiles);
The normal and hovered styles are applied together (at the same time) and CSS/html will manage when it hovered or not. The component no longer requires event listeners and doesn't need to maintain internal state.
Explanation
what means "...calls to update state are asynchronous and get queued up."?
When you call this.setState or a useState update function the update doesn't happen synchronously right then and there, but they are queued up during the current render cycle and batch processed in the order in which they were queued. Perhaps this demo will help illustrate what happens. What confounds this issue is the fact that event processing is also asynchronous, meaning that, when events occur their registered callbacks are placed in the event queue to be processed.

Rearrange react components on resize

Is this even possible(title)? I need it, because in css #media rule needs to change some element's css. CSS part works. Problem occurs because it needed to be followed by rearranging react components. I have these conditions whom(both of them), pass. It should be, when window resizes, css get applied and after getting window width, components get rearranged following css change.
I have this in constructor:
this.myInput = React.createRef();
and this:
componentDidMount () {
this.setState({
myWidth: this.state.myWidth=this.myInput.current.offsetWidth
});
}
and this in Render():
render(){
const btnText = this.state.erase ? "Populate" : "Erase" ;
const handleClick = e => this.fullScreen(e.target.id);
const EditorHead1 = <EditorHead id={"item1"} style={this.state.stilEditor} className={this.state.headEdKlasa} onClick={handleClick} title={this.state.attr}/>;
const PreviewHead1 = <PreviewHead id={"item2"} style={this.state.stilPreview} className={this.state.headViewKlasa} onClick={handleClick} title={this.state.attr}/>;
const BtnEraser1 = <BtnEraser id={"eraser"} onClick={this.eraseFields} type={"button"} className={"btn btn-danger btn-lg"} title={"Erase & populate both fields"} value={btnText}/>;
const Editor1 = <Editor id={"editor"} onChange={this.handleChange} className={this.state.editorKlasa} value={this.state.markdown} placeholder={"Enter ... some kind a text!? ..."} title={"This is rather obvious isnt it? Its editor window Sherlock :D"}/>;
const Preview1 = <Preview id={"preview"} className={this.state.previewKlasa} dangerouslySetInnerHTML={{__html: marked(this.state.markdown, { renderer: renderer })}} title={"Its a preview window, Sherlock ;)"}/>;
const Arrow1 = <Arrow id={"arrow"}/>;
if(this.state.myWidth<=768){
alert("Alternative");
alert(this.state.myWidth);
return (
<div id="inner2" ref={this.myInput} className="grid-container animated zoomIn" style={{height: this.state.inner2H}} onDoubleClick={this.inner2Height}>
{EditorHead1}
{Editor1}
{PreviewHead1}
{Preview1}
{BtnEraser1}
{Arrow1}
</div>
);
}
if(this.state.myWidth>768){
alert("Normal");
alert(this.state.myWidth);
return (
<div id="inner2" ref={this.myInput} className="grid-container animated zoomIn" style={{height: this.state.inner2H}} onDoubleClick={this.inner2Height}>
{EditorHead1}
{PreviewHead1}
{BtnEraser1}
{Editor1}
{Preview1}
{Arrow1}
</div>
);
}
}
Currently rearranging only works if you, after resize, refresh browser or “run” again codepen.
resize event should be listened in order to keep track of element width changes. It's preferable to debounce event handlers for events that can be fired often, which resize is:
import debounce from 'lodash.debounce';
...
myInput = React.createRef();
setMyWidth = () => {
this.setState({
myWidth: this.myInput.current.offsetWidth
});
}
onResize = debounce(this.setMyWidth, 100);
componentDidMount() {
this.setMyWidth();
window.addEventListener('resize', this.onResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
Depending on what element offsetWidth is, element reference may be redundant, document.body.clientWidth can be tracked instead.
Also, this.state.myWidth=... is a mistake, this.state shouldn't be changed directly outside component constructor.

Resources