Icon className doesn't update on state change - reactjs

I am trying simplest ever theme toggle in react with context and don't seem to be able to get the icon re-render when context changes. Everything else works just fine: colors, background image... It renders either of icons depending on initial state, but icon doesn't update when theme is toggled.
import React, { useContext } from "react"
import { ThemeContext } from "../../contexts/ThemeContext"
const ThemeToggle = () => {
const { isDarkMode, dark, light, toggleTheme } = useContext(ThemeContext)
const theme = isDarkMode ? dark : light
return (
<li
style={{ background: theme.bgPrimary, color: theme.text }}
onClick={toggleTheme}
>
<i className={theme.ico} />
</li>
)
}
export default ThemeToggle
Context:
import React, { Component, createContext } from "react"
export const ThemeContext = createContext()
class ThemeContexProvider extends Component {
state = {
isDarkMode: false,
light: {
text: "#333",
bgPrimary: "#eee",
bgSecondary: "#333",
ico: "fas fa-moon"
},
dark: {
text: "#ddd",
bgPrimary: "#000003",
bgSecondary: "#bbb",
ico: "fas fa-sun"
}
}
toggleTheme = () => {
this.setState({ isDarkMode: !this.state.isDarkMode })
}
render() {
return (
<ThemeContext.Provider
value={{ ...this.state, toggleTheme: this.toggleTheme }}
>
{this.props.children}
</ThemeContext.Provider>
)
}
}
export default ThemeContexProvider

I fixed this installing dedicated react fa package, still don't know why above doesn't work. This works though:
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome"
import { faMoon } from "#fortawesome/free-solid-svg-icons"
import { faSun } from "#fortawesome/free-solid-svg-icons"
//.....
return (
<li
style={{ background: theme.bgPrimary, color: theme.text }}
onClick={toggleTheme}
>
{isDarkMode ? (
<FontAwesomeIcon icon={faSun} />
) : (
<FontAwesomeIcon icon={faMoon} />
)}
</li>

I suspect the spaces in your ico property could be the issue. Normally this isn't an issue for state/props. Context is possibly to blame for this. This might fix it:
<i className={`fas ${theme.ico}`} />
Replace your context ico properties with just the class that changes fa-moon and fa-sun

You could have used key props in the icon tag. Keys help React identify which items have changed. So it can rerender your icon.
{isDarkMode ? (
<i key={1} icon={faSun} />
) : (
<i key={2} icon={faMoon} />
)}

Related

I dont know how to fix useSprint using class

I received this error :
Line 21:28: React Hook "useSpring" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks.
I want to make a transition with the opacity and when I click the button appears the image or disappears.
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import { useSpring, config, animated } from "react-spring";
import './Experience.css';
class Experience extends Component {
constructor(props) {
super(props);
this.state = {
showA: false
};
}
render() {
// const [showA, setShowA] = useState(false);
const fadeStyles = useSpring({
config: { ...config.molasses },
from: { opacity: 0 },
to: {
opacity: this.state.showA ? 1 : 0
},
});
return (
<div style={{ padding: "15px" }} className="App">
<h2>Fade Demo</h2>
<div>
<animated.div style={fadeStyles}>
<img src={`https://a.wattpad.com/useravatar/valery2080.256.603024.jpg)`} alt="hola"/>
</animated.div>
<br />
<button onClick={() => this.setState(val => !val)}>Toggle</button>
</div>
</div>
);
}
}
export default withTranslation()(Experience);
You need to convert the class component to a functional component. Following is the implementation of Experience Component to a functional component.
Note: Make sure to add the CSS file in your implementation.
Following is the codesandbox link for your reference: https://codesandbox.io/s/jolly-wescoff-bnqm4
import React, { useState, Component } from "react";
import { withTranslation } from "react-i18next";
import { useSpring, config, animated } from "react-spring";
const Experience = () => {
const [showA, setShowA] = useState(false);
const fadeStyles = useSpring({
config: { ...config.molasses },
from: { opacity: 0 },
to: {
opacity: showA ? 1 : 0
}
});
return (
<div style={{ padding: "15px" }} className="App">
<h2>Fade Demo</h2>
<div>
<animated.div style={fadeStyles}>
<img
src={`https://a.wattpad.com/useravatar/valery2080.256.603024.jpg)`}
alt="hola"
/>
</animated.div>
<br />
<button onClick={() => setShowA(!showA)}>Toggle</button>
</div>
</div>
);
};
export default withTranslation()(Experience);

React js hover an element and disable hover when leave the element

I have an application in react js, there is a list of elements and when the user hover over the element the background color should change to red. At the moment it works. Also i want when i leave the element unhover, the element should be without red background, but now when i leave the element the color does not dissappear. Basically the hover should affect only that element where the user hover.
import "./styles.css";
import React, { memo, useEffect, useState } from "react";
const Element = ({ id }) => {
const styles = {
background: "red"
};
const [isHovered, setIsHovered] = useState(false);
return (
<div
className={isHovered ? "hovered" : ""}
onMouseOver={() => {
setIsHovered(true);
}}
>
hello {id}
</div>
);
};
export default function App() {
return (
<div className="App">
{[0, 1, 2].map((el) => {
return <Element key={el} id={el} />;
})}
</div>
);
}
demo: https://codesandbox.io/s/compassionate-einstein-9v1ws?file=/src/App.js:0-557 Question: Who can help to solve the issue?
Instead of onMouseOver, you can use onMouseEnter and onMouseLeave to apply the styles based on the isHovered state.
Here is your updated code.
import "./styles.css";
import React, { memo, useEffect, useState } from "react";
const Element = ({ id }) => {
// const styles = {
// background: "red"
// };
const [isHovered, setIsHovered] = useState(false);
console.log({ isHovered });
return (
<div
// style={{ backgroundColor: isHovered ? "red" : "transparent" }}
className={"hovered"}
// onMouseEnter={() => setIsHovered(true)}
// onMouseLeave={() => setIsHovered(false)}
>
hello {id}
</div>
);
};
export default function App() {
return (
<div className="App">
{[0, 1, 2].map((el) => {
return <Element key={el} id={el} />;
})}
</div>
);
}
You can also do the same thing using the className without using the state variable, with just targeting the hover class. I've used the both in the sandbox.
In case of doing only with className, you can just add a className to the div
className={"hovered"}
then, update the styles of the class in the css file.
.hovered:hover {
background: red;
}
Updated sandbox

Export variable - React

I have a component called Component1 in which I have the following code:
import React, { useState } from "react";
import Popover from "material-ui-popup-state/HoverPopover";
import Fab from "#material-ui/core/Fab";
import {
usePopupState,
bindHover,
bindPopover
} from "material-ui-popup-state/hooks";
import PaletteIcon from "#material-ui/icons/Palette";
import Colors from "./Colors";
const DEFAULT_COLOR = "red";
const COLORS = [/*list of colors*/];
const Component1 = ({ classes }) => {
const popupState = usePopupState({
variant: "popover",
popupId: "demoPopover"
});
const [selectedColor, setSelectedColor] = useState(DEFAULT_COLOR);
return (
<div className="box" style={{ backgroundColor: selectedColor }}>
<Fab variant="extended" {...bindHover(popupState)}>
<PaletteIcon />
</Fab>
<Popover
>
<div className="color-palette">
{COLORS.map((color) => (
<Colors
key={color}
selected={selectedColor === color}
onClick={setSelectedColor}
color={color}
/>
))}
</div>
</Popover>
</div>
);
};
export default Component1;
This component is imported in Component2 where the code is:
import React from "react";
import Component1 from "./Component1";
import Fab from "#material-ui/core/Fab";
import DeleteIcon from "#material-ui/icons/Delete";
function Component2(props) {
function handleClick() {
props.onDelete(props.id);
}
return (
<div className="note" style={{ backgroundColor: "selectedColor" }}>
<h1>{props.title}</h1>
<p>{props.content}</p>
<Fab onClick={handleClick}>
<DeleteIcon fontSize="small" />
</Fab>
<HoverPopover />
</div>
);
}
export default Component2;
In component2 I need to use the const selectedColor for styling purpose for div with class="note". However the issue is when I select colors from COLORS list the background-color of div with class="note" is not changing. I tried many options but I don't understand how to do it correctly. Please tell me how to do it right.
To share the "selectedColor" variable, which is actually a state, you would have to pass it through the props to the child component
Your "Component2" should declare the state "selectedColor", and this state and its function must be passed by the props to your "Component1".
https://reactjs.org/tutorial/tutorial.html#lifting-state-up

State managed in Context is showing undefined

I'm implementing a dark theme to understand React context. I successfully created a dark mode that saves mode to local storage and determined if the user prefers dark mode from local storage.
However now I want to refactor to keep the theme state in context.
I have moved the code in a theme context however I get the error stating
ERROR: TypeError: Cannot read property 'darkMode' of undefined
I can't seem to work it out. I was under the impression that I could pass the state in this case darkMode and setDarkMode into my App component using useContext?
import React, { useContext } from 'react';
import ThemeContextProvider, { ThemeContext } from './contexts/ThemeContext';
function App() {
const { darkMode, setDarkMode } = useContext(ThemeContext);
return (
<div className='App'>
<ThemeContextProvider>
<div className={darkMode ? 'dark-mode' : 'light-mode'}>
<nav>
<div className='toggle-container'>
<span style={{ color: darkMode ? 'grey' : 'yellow' }}>☀︎</span>
<span className='toggle'>
<input
checked={darkMode}
onChange={() => setDarkMode((prevMode) => !prevMode)}
type='checkbox'
className='checkbox'
id='checkbox'
/>
<label htmlFor='checkbox' />
</span>
<span style={{ color: darkMode ? '#9c27b0' : 'grey' }}>☽</span>
</div>
</nav>
<main>
<h1>{darkMode ? 'Dark Mode' : 'Light Mode'}</h1>
<h2>Toggle the switch to change theme</h2>
</main>
</div>
</ThemeContextProvider>
</div>
);
}
and the ThemeContext
import React, { createContext, useState, useEffect } from 'react';
export const ThemeContext = createContext();
const ThemeContextProvider = (props) => {
const [darkMode, setDarkMode] = useState(getInitialMode);
useEffect(() => {
localStorage.setItem('dark', JSON.stringify(darkMode));
getPrefColourScheme();
}, [darkMode]);
function getInitialMode() {
const isReturningUser = 'dark' in localStorage;
const savedMode = JSON.parse(localStorage.getItem('dark'));
const userPrefersDark = getPrefColourScheme();
// if mode was saved -> dark / light
if (isReturningUser) {
return savedMode;
// if preferred colour scheme is dark -> dark
} else if (userPrefersDark) {
return true;
// otherwise -> light
} else {
return false;
}
}
function getPrefColourScheme() {
if (!window.matchMedia) return;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return (
<ThemeContext.Provider value={{ darkMode, setDarkMode }}>
{props.children}
</ThemeContext.Provider>
);
};
export default ThemeContextProvider;
Forgive my ignorance i'm struggling to wrap my head around this problem.
Any help would be grateful thanks.
I think you can only use
const { darkMode, setDarkMode } = useContext(ThemeContext);
whenever some component above the one that uses this hook has a <Context.Provider>
However, you're using this hook inside your App component - it's not a child of your Provider.
What you can do is separate the children to a new component and use the hook there, or render your <App /> as a child of your <ThemeContextProvider> (which means moving your <ThemeContextProvider> to another place)
Option 1
const FooComp = () => {
const { darkMode, setDarkMode } = useContext(ThemeContext);
return (
<div className={darkMode ? 'dark-mode' : 'light-mode'}>
<nav>
<div className='toggle-container'>
<span style={{ color: darkMode ? 'grey' : 'yellow' }}>☀︎</span>
<span className='toggle'>
<input
checked={darkMode}
onChange={() => setDarkMode((prevMode) => !prevMode)}
type='checkbox'
className='checkbox'
id='checkbox'
/>
<label htmlFor='checkbox' />
</span>
<span style={{ color: darkMode ? '#9c27b0' : 'grey' }}>☽</span>
</div>
</nav>
<main>
<h1>{darkMode ? 'Dark Mode' : 'Light Mode'}</h1>
<h2>Toggle the switch to change theme</h2>
</main>
</div>
)
}
then in App
function App() {
return (
<div className='App'>
<ThemeContextProvider><FooComp /></ThemeContextProvider>
)
}
or Option 2
in the place you're rendering App you do
<ThemeContextProvider><App /></ThemeContextProvider>
and you remove ThemeContextProvider from App
If you are using a class component and getting this issue
context is stored in a variable called context in a class component
import React, {Component, useContext} from 'react';
import {ThemeContext} from '../contexts/ThemeContext';
class Navbar extends Component {
static contextType = ThemeContext
render() {
console.log(this.context);
return (
<nav>
<h1>Content Apps</h1>
<ul>
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
);
}
}
export default Navbar;

React countup animation starts immediately after the page loading , should start when scrolled to the component (without jquery)

I have a react single page app, with multiple components. For the 5th component(visible only when scrolled down) I have a counter . Now I am using react-countup library to achieve the counter function. However , the counter starts soon as the page is loaded . Is it possible for countup to begin once we scroll down to the component.
Animation happens only once(which is good)after the page is loaded, but I would like the counter not to begin soon after the page is loaded, but when user scrolls down to the component the first time.
My code looks like this:
render() {
return (
<div className={style.componentName}>
<h2>Heading</h2>
<div className={style.col}>
<div>My counter</div>
<CountUp className={style.countup} decimals={1} start={0} end={25} suffix=" %" duration={3} />
</div>
</div>)}
Updated code:
import CountUp, { startAnimation } from 'react-countup';
import VisibilitySensor from 'react-visibility-sensor';
class className extends Component {
state = {
scrollStatus: true
};
onVisibilityChange = isVisible => {
if (isVisible) {
if (this.state.scrollStatus) {
startAnimation(this.myCountUp);
this.setState({ scrollStatus: false });
}
}
}
render() {
return (
<div className={style.componentName}>
<h2>Heading</h2>
<VisibilitySensor onChange={this.onVisibilityChange} offset = {{ top:
10}} delayedCall>
<CountUp className={style.countup} decimals={1} start={0} end={25}
suffix=" %" duration={3} ref={countUp => { this.myCountUp= countUp;}}/>
</VisibilitySensor>
</div>)}
}
The API may have changed since last year. I manage to make this work with this code now :
import React from "react";
import CountUp from "react-countup";
import VisibilitySensor from 'react-visibility-sensor';
const MyComponent = () => (
<>
<CountUp end={100} redraw={true}>
{({ countUpRef, start }) => (
<VisibilitySensor onChange={start} delayedCall>
<span ref={countUpRef} />
</VisibilitySensor>
)}
</CountUp>
</>
);
export default App;
I use this component inside a tab, so the redraw={true} prop is only here to redraw the animation on tabChange.
Per React CountUp's README, you can use the startAnimation hook to manually kick off the animation. Combine this with something like react-visibility-sensor, and you can wait to kick off the animation until it is visible in the user's browser.
import React, {Component} from 'react';
import CountUp, {startAnimation} from 'react-countup';
import './App.css';
import VisibilitySensor from 'react-visibility-sensor';
const style = {
componentName: {},
col: {},
countup: {},
};
class App extends Component {
constructor(props) {
super(props);
this.onVisibilityChange = this.onVisibilityChange.bind(this); // Bind for appropriate 'this' context
}
onVisibilityChange(isVisible) {
if (isVisible) {
startAnimation(this.myCountUp);
}
}
render() {
return (
<div className={style.componentName}>
<h2>Heading</h2>
<div className={style.col}>
<div>My counter</div>
<VisibilitySensor
onChange={this.onVisibilityChange}
delayedCall // Prevents react apps triggering elements as visible before styles are loaded
>
<CountUp className={style.countup} decimals={1} start={0} end={25} suffix=" %" duration={3}
ref={countUp => { this.myCountUp = countUp; }} // From react-countup README
/>
</VisibilitySensor>
</div>
</div>
);
}
}
export default App;
As is, it will startAnimation every time you scroll to the countup. If you want to only do that once, just add a piece of state that gets set after the first render (and then prevent it from doing startAnimation again based on that altered state).
Less elegant (not recommended) ways to accomplish the same effect might include:
Use the built-in animation triggers (i.e. changing the props duration, end, start) by setting them equal to some state that changes when the user scrolls down
Leveraging the onStart prop, called before the animation starts, to delay starting the animation until the user scrolls down
EDIT: Update to address your second question
Unfortunately, it looks like the react-countup library doesn't expose a way to prevent startAnimation on startup.
But we can hack together a fairly elegant fix by manipulating the end prop using state instead:
import React, {Component} from 'react';
import CountUp, {startAnimation} from 'react-countup';
import './App.css';
import VisibilitySensor from 'react-visibility-sensor';
const style = {
componentName: {},
col: {},
countup: {},
};
class App extends Component {
state = {
didViewCountUp: false
};
onVisibilityChange = isVisible => {
if (isVisible) {
this.setState({didViewCountUp: true});
}
}
render() {
return (
<div className={style.componentName}>
<h2 style={{fontSize: '40em'}}>Heading</h2>
<VisibilitySensor onChange={this.onVisibilityChange} offset={{
top:
10
}} delayedCall>
<CountUp className={style.countup} decimals={1} start={0} end={this.state.didViewCountUp ? 25 : 0}
suffix=" %" duration={3} />
</VisibilitySensor>
</div>)
}
}
export default App;
Here's my implementation. It just runs once and also doesn't re-render every time the component enters viewport to check visibility.
Dependencies:
1. react-countup v.4.3.2
2. react-visibility-sensor v.5.1.1
import React, { useState } from "react";
import CountUp from "react-countup";
import VisibilitySensor from "react-visibility-sensor";
const Ticker = ({ className, ...rest }) => {
const [viewPortEntered, setViewPortEntered] = useState(false);
return (
<CountUp {...rest} start={viewPortEntered ? null : 0}>
{({ countUpRef }) => {
return (
<VisibilitySensor
active={!viewPortEntered}
onChange={isVisible => {
if (isVisible) {
setViewPortEntered(true);
}
}}
delayedCall
>
<h4 className={className} ref={countUpRef} />
</VisibilitySensor>
);
}}
</CountUp>
);
};
export default Ticker;
Here's how to use it:
<Ticker className="count" end={21} suffix="M+" />
Here is my solution using class based component
Note: import two Libraries first run this code
react-visibility-sensor
react-countup
import React from "react";
import CountUp from "react-countup";
import VisibilitySensor from 'react-visibility-sensor';
class CountDown extends React.Component {
render() {
return (
<React.Fragment>
<CountUp start={0} end={100} prefix="+" duration="2">
{({ countUpRef, start }) => (
<VisibilitySensor onChange={start} delayedCall>
<span ref={countUpRef} />
</VisibilitySensor>
)}
</CountUp>
</React.Fragment>
)
}
}
export default CountDown;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
The docs for that library have a way to manually start the counter. I would use that approach to start the counter once a user has scrolled to the required distance.
import React, { Component } from 'react';
import CountUp, { startAnimation } from 'react-countup';
const MyComponent = () => (
<div>
<CountUp className="CountUp" start={0} end={100} duration={3} ref={(countUp) => {
this.myCountUp = countUp;
}} />
<button className="Button" onClick={(event) => {
startAnimation(this.myCountUp);
}}>Count me up!</button>
</div>
);
export default App;
Link to Github. Read the README at the very bottom.
you can have look at my functional component to achieve this
import React from "react";
import { Box } from "#material-ui/core";
import CountUp from "react-countup";
import VisibilitySensor from "react-visibility-sensor";
export default function Counter() {
const [focus, setFocus] = React.useState(false);
return (
<Box component="div">
<CountUp start={focus ? 0 : null} end={100} duration={5} redraw={true}>
{({ countUpRef }) => (
<div>
<span ref={countUpRef} />
<VisibilitySensor
onChange={isVisible => {
if (isVisible) {
setFocus(true);
}
}}
>
<a>+</a>
</VisibilitySensor>
</div>
)}
</CountUp>
</Box>
);
}
Simply add below prop
enableScrollSpy: true
<CountUp enableScrollSpy={true} end={75}/>
react-visibility-sensor doesn't seem to be maintained at the moment and produces warnings related to findDOMNode which is deprecated.
I've used react-waypoint in my functional component which makes the animation only trigger once when the user has the child of the waypoint component in view.
// Ticker.tsx
import React, { useState } from "react";
import CountUp from "react-countup";
import { Waypoint } from "react-waypoint";
type TickerProps = {
end: number;
suffix: string;
}
const Ticker: React.FC<TickerProps> = ({ end, suffix }) => {
const [viewPortEntered, setViewPortEntered] = useState(false);
const onVWEnter = () => {
setViewPortEntered(true);
}
return (
<Waypoint onEnter={onVWEnter} >
<div>
{viewPortEntered && <CountUp end={end} suffix={suffix} start={0} /> }
</div>
</Waypoint>
);
};
export default Ticker;
<CountUp
className={styles.number}
start={0}
end={number}
enableScrollSpy={true}
scrollSpyDelay={0}
suffix={index !== -1 ? "+" : ""}
/>
try this
scroll spy should do the work
Here's an alternative solution using react-in-viewport package. Idea is to define this element and use <CountUpInViewport> instead of <CountUp>.
This example fires on the first entry. One could do variations on this to refire every time it enters/leaves the viewport.
import React from 'react';
import { useRef } from 'react';
import CountUp from 'react-countup';
import { useInViewport } from 'react-in-viewport';
let CountUpInViewport = props => {
const ref = useRef();
const { enterCount } = useInViewport(ref);
return (
<div ref={ref}>
{
enterCount > 0 ?
<CountUp key='in' {...props}></CountUp> :
<CountUp key='out' {...props}></CountUp> // ensures drawn when not visible
}
</div>
);
}

Resources