Changing styles when scrolling React - reactjs

I want to add the scrolling effect. At the start, the elements have the opacity: 0.2 property. When element is reached in the browser window, it is to replace the property with opacity: 1. At this moment, when I scroll, all elements change the property to opacity: 1. How to make this value when element is reached in the browser, the rest of the elements have the property opacity: 0.2
class QuestionListItem extends Component {
constructor() {
super();
this.state = {
opacity: 0.2,
};
}
componentDidMount = () => {
window.addEventListener('scroll', () => {
this.setState({
opacity: 1,
});
});
};
render() {
const { question } = this.props;
const { opacity } = this.state;
return (
<div>
<li
key={question.id}
className="Test__questions-item"
style={{ opacity: `${opacity}` }}
ref={
(listener) => { this.listener = listener; }
}
>
<p>
{question.question}
</p>
<QuestionAnswerForm />
</li>
</div>
);
}
}
I want effect like this https://anemone.typeform.com/to/jgsLNG

A proper solution could look like this. Of course, this is just a concept. You can fine-tune the activation/deactivation logic using props from getBoundingClientRect other than top (e.g. height, bottom etc).
Important that you should not set the component's state on every single scroll event.
const activeFromPx = 20;
const activeToPx = 100;
class ScrollItem extends React.Component {
state = {
isActive: false
}
componentDidMount = () => {
window.addEventListener('scroll', this.handleScroll);
this.handleScroll();
};
handleScroll = () => {
const { top } = this.wrapRef.getBoundingClientRect();
if (top > activeFromPx && top < activeToPx && !this.state.isActive) {
this.setState({ isActive: true });
}
if ((top <= activeFromPx || top >= activeToPx) && this.state.isActive) {
this.setState({ isActive: false });
}
}
setWrapRef = ref => {
this.wrapRef = ref;
}
render() {
const { isActive } = this.state;
return (
<div
className={`scroll-item ${isActive && 'scroll-item--active'}`}
ref={this.setWrapRef}
>
{this.props.children}
</div>
)
}
}
class ScrollExample extends React.Component {
render() {
return (
<div className="scroll-wrap">
<ScrollItem>foo</ScrollItem>
<ScrollItem>bar</ScrollItem>
<ScrollItem>eh</ScrollItem>
</div>);
}
}
ReactDOM.render(<ScrollExample />, document.getElementById('root'))
.scroll-wrap {
height: 300vh;
background: lightgray;
padding-top: 55px;
}
.scroll-item {
height: 60vh;
background: lightcyan;
margin: 10px;
opacity: 0.2;
}
.scroll-item--active {
opacity: 1;
}
<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>
<div id="root"></div>

You can include an isInViewport-like implementation as this one: https://gist.github.com/davidtheclark/5515733 then use it on your component.
componentDidMount = () => {
window.addEventListener('scroll', (event) => {
if (isElementInViewport(event.target) {
this.setState({
opacity: 1,
});
}
});
};
There's also read-to-use react-addons for this: https://github.com/roderickhsiao/react-in-viewport

Related

setting ref on functional component

I am trying to change this class based react component to a functional component but i am gettig an infinite loop issue on setting the reference, i think its because of on each render the ref is a new object.
How could i convert the class based component to a functional component
index-class.js - Ref
class Collapse extends React.Component {
constructor(props) {
super(props);
this.state = {
showContent: false,
height: "0px",
myRef: null,
};
}
componentDidUpdate = (prevProps, prevState) => {
if (prevState.height === "auto" && this.state.height !== "auto") {
setTimeout(() => this.setState({ height: "0px" }), 1);
}
}
setInnerRef = (ref) => this.setState({ myRef: ref });
toggleOpenClose = () => this.setState({
showContent: !this.state.showContent,
height: this.state.myRef.scrollHeight,
});
updateAfterTransition = () => {
if (this.state.showContent) {
this.setState({ height: "auto" });
}
};
render() {
const { title, children } = this.props;
return (
<div>
<h2 onClick={() => this.toggleOpenClose()}>
Example
</h2>
<div
ref={this.setInnerRef}
onTransitionEnd={() => this.updateAfterTransition()}
style={{
height: this.state.height,
overflow: "hidden",
transition: "height 250ms linear 0s",
}}
>
{children}
</div>
</div>
);
}
}
what i have tried so far.
index-functional.js
import React, { useEffect, useState } from "react";
import { usePrevious } from "./usePrevious";
const Collapse = (props) => {
const { title, children } = props || {};
const [state, setState] = useState({
showContent: false,
height: "0px",
myRef: null
});
const previousHeight = usePrevious(state.height);
useEffect(() => {
if (previousHeight === "auto" && state.height !== "auto") {
setTimeout(
() => setState((prevState) => ({ ...prevState, height: "0px" })),
1
);
}
}, [previousHeight, state.height]);
const setInnerRef = (ref) =>
setState((prevState) => ({ ...prevState, myRef: ref }));
const toggleOpenClose = () =>
setState((prevState) => ({
...prevState,
showContent: !state.showContent,
height: state.myRef.scrollHeight
}));
const updateAfterTransition = () => {
if (state.showContent) {
this.setState((prevState) => ({ ...prevState, height: "auto" }));
}
};
return (
<div>
<h2 onClick={toggleOpenClose}>{title}</h2>
<div
ref={setInnerRef}
onTransitionEnd={updateAfterTransition}
style={{
height: state.height,
overflow: "hidden",
transition: "height 250ms linear 0s"
}}
>
{children}
</div>
</div>
);
};
usePrevious.js - Link
import { useRef, useEffect } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export { usePrevious };
The problem here is you set your reference to update through setState and useEffect (which is what causes you the infinite loop).
The way you would go by setting references on functional components would be as followed:
const Component = () => {
const ref = useRef(null)
return (
<div ref={ref} />
)
}
More info can be found here: https://reactjs.org/docs/refs-and-the-dom.html

Move a square using the coordinate system in reactjs

I need to move this square using _dragTask() function, and i also need to do to it by using the coordinate system. Anyone can do that?
import React from 'react';
import './App.css';
class App extends React.Component {
componentDidMount() {
}
constructor(props) {
super(props);
this.state = {
MainContainer: {backgroundColor: "#282c34", display: "flex", minHeight: "100vh"},
ObjectMap: [],
}
this._createObject = this._createObject.bind(this);
this._createBox = this._createBox.bind(this);
this._buttonCreateBox1 = this._buttonCreateBox1.bind(this);
this._dragTask = this._dragTask.bind(this);
}
_createObject = (object) => {
var ObjectMap = this.state.ObjectMap;
ObjectMap.push(object);
this.setState({ObjectMap: ObjectMap});
}
_createBox = (style) => {
var object = {position: "absolute", top: this.state.positionX, left: this.state.positionY};
var styleObject = Object.assign({},
object, style
)
return (
<div style={styleObject} draggable="true" onDragEnd={(event) => {this._dragTask(event)}}>
</div>
);
}
_dragTask(event) {
event.persist();
this.setState({positionX: event.screenX, positionY: event.screenY}, () => { console.log("Page X:"+ this.state.positionX + " Page Y:" + this.state.positionY); });
}
_buttonCreateBox1 = () => {
this._createObject(this._createBox({backgroundColor: "white", width: "300px", height: "300px"}));
}
render() {
return (
<div style={this.state.MainContainer}>
<button type="button" onClick={ this._buttonCreateBox1 }>Click Me!</button>
{
this.state.ObjectMap.map((item, index) => {
return item
})
}
</div>
);
}
}
export default App;
Right now, the object it created by printed in the screen by the mainloop, and added to it by _createObject() function.
just change your _dragTask method to this
_dragTask(event) {
event.target.style.top = event.clientY + "px";
event.target.style.left = event.clientX + "px";
}
and change the style of the wrapper to have position: relative
constructor(props) {
super(props);
this.state = {
MainContainer: {
backgroundColor: "#282c34",
position: "relative",
display: "flex",
minHeight: "100vh"
},
ObjectMap: []
};
...
checkout this sandbox to see full example

Implementing an online game in react

I wrote a small game in React which consists of three components,
App <= MainInfo <= Game. The essence of the game is to press the START GAME button and quickly click on all the blue cubes that disappear after clicking. At the end, an alert window pops up with the time and number of cubes pressed, but there are bugs:
After loading, you can click on the blue cubes and they will disappear without pressing START GAME. (So you can even win the game with a zero time counter).
If you press START GAME a couple of times in a row, the counter is accelerated, and after winning, if you press the button, the time continues.
How to make a REFRESH button to restart the game after winning and not reload the entire page?
Link to game - https://quintis1212.github.io/react-game/build/index.html
import React from 'react';
import './App.css';
import MainInfo from './GameContent/MainInfo';
function App() {
return (
<div className="App">
<MainInfo />
</div>
);
}
export default App;
import React, {Component} from 'react';
import Game from './GameTable/Game';
class MainInfo extends Component {
state = {
count:0,
timer:0,
initGame: false
}
shouldComponentUpdate(){
return this.state.initGame;
}
logToConsole = (e)=> {
if (e.target.className === 'Element') {
this.setState((state)=>{
return {count: state.count + 1}
});
e.target.className = 'Element-empty';
console.log(e.target.className)
}
}
timerUpdated(){ this.setState((state)=>{
return {timer: state.timer + 1}
}); }
initGameHandler =()=>{
this.setState({initGame: true})
console.log('refreshHandler')
this.timerID=setInterval(() => { this.timerUpdated() }, 1000);
}
finishGameHandler = (score)=>{
console.log(score)
if(this.state.count === score-1) {
alert('-----GAME OVER-----'+
'YOUR TIME: '+this.state.timer+'seconds'+
' YOUR COUNT: '+this.state.count+'points');
clearInterval(this.timerID)
this.setState({initGame: false})
}
}
render(){
return(
<div>
<p>Timer : <strong>{this.state.timer} seconds </strong></p>
<p>Counter :<strong>{this.state.count}</strong></p>
<button onClick={this.initGameHandler}>START GAME</button>
<Game click={this.logToConsole} updateData={this.finishGameHandler} />
</div>
)
}
}
export default MainInfo;
import React,{Component} from 'react';
class Game extends Component {
shouldComponentUpdate(){
return false
}
render() {
let item;
let count = 0;
let arr = [];
for (let i = 0; i < 70; i++) {
arr.push(Math.floor(Math.random() * 10));
}
item = arr.map((el,i,arr) => {
count++
return el < 2 ? arr[i-1] < 4?<div key={count} className='Element-empty'></div>:<div onClick={(e)=>{this.props.click(e)}} key={count} className='Element'></div> : <div key={count} className='Element-empty'></div>
})
// console.log(item.filter(el => el.props.className == 'Element'))
let score = item.filter(el => el.props.className === 'Element')
let scoreLenhgth=score.length
return(
<div onClick={() => { this.props.updateData(scoreLenhgth)}} >
{item}
</div>
)
}
}
export default Game;
I made quite some changes but got it all to work, didn't put any comments on the changes so please comment if you have any questions.
function App() {
return (
<div className="App">
<MainInfo />
</div>
);
}
class MainInfo extends React.Component {
state = {
count: 0,
timer: 0,
initGame: false,
};
timerUpdated() {
this.setState(state => {
return { timer: state.timer + 1 };
});
}
initGameHandler = () => {
this.setState({ initGame: true });
this.timerID = setInterval(() => {
this.timerUpdated();
}, 1000);
};
gameClickHandler = (correct, itemsLeft) => {
this.setState(
{
count: this.state.count + (correct ? 1 : 0),
},
() => {
if (itemsLeft === 0 || !correct) {
alert(
'-----GAME OVER-----' +
'YOUR TIME: ' +
this.state.timer +
' seconds ' +
' YOUR COUNT: ' +
this.state.count +
' points'
);
clearInterval(this.timerID);
this.setState({
initGame: false,
count: 0,
timer: 0,
});
}
}
);
};
render() {
return (
<div>
<p>
Timer :{' '}
<strong>{this.state.timer} seconds </strong>
</p>
<p>
Counter :<strong>{this.state.count}</strong>
</p>
{this.state.initGame || (
<button onClick={this.initGameHandler}>
START GAME
</button>
)}
{this.state.initGame && (
<Game
click={this.logToConsole}
updateData={this.gameClickHandler}
blocks={2}
/>
)}
</div>
);
}
}
const shuffle = array => {
const copy = [...array];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
};
class Game extends React.PureComponent {
constructor(props) {
super(props);
this.state = this.setGame();
}
setGame = () => {
const arr = shuffle(
[...new Array(70)].map((_, i) => i)
);
const selected = arr.slice(0, this.props.blocks || 5);
return { arr, selected };
};
render() {
return (
<div
onClick={e => {
const itemClicked = Number(
e.target.getAttribute('data-id')
);
const correct = this.state.selected.includes(
itemClicked
);
this.props.updateData(
correct,
this.state.selected.length - (correct ? 1 : 0)
);
this.setState(state => ({
selected: state.selected.filter(
v => v !== itemClicked
),
}));
}}
>
{this.state.arr.map((_, i) => (
<div
key={i}
data-id={i}
className={
!this.state.selected.includes(i)
? 'Element-empty'
: 'Element'
}
></div>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
.App {
text-align: center;
}
.Element {
display: inline-block;
width: 40px;
height: 40px;
background-color: cornflowerblue;
border: 2px solid rgb(68, 209, 179);
}
.Element-empty {
display: inline-block;
width: 40px;
height: 40px;
background-color: rgba(158, 147, 100, 0.394);
border: 2px solid rgb(68, 209, 179);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

React-motion with redux

I am trying to animate adding a new comment in my React + Redux app with react-motion.
class Comments extends Component {
getDefaultStyles() {
const { comments } = this.props;
return comments.map((c, i) => {
return {
key: "" + i,
style: { top: -50, opacity: 0.2 },
data: c
}
});
}
getStyles() {
const { comments } = this.props;
return comments.map((c, i) => {
return {
key: "" + i,
style: {
top: spring(0, presets.gentle),
opacity: spring(1, presets.gentle)
},
data: c
}
})
}
willEnter() {
return { top: -50, opacity: 0.2 }
}
render() {
let { comments } = this.props;
return (
<div>
<TransitionMotion defaultStyles={this.getDefaultStyles()} styles={this.getStyles()} willEnter={this.willEnter}>
{styles =>
<div>
{
styles.map((c) => {
return (
<Comment key={c.key} comment={c.data} style={{...c.style, overflow: 'hidden'}} />
)
})
}
</div>
}
</TransitionMotion>
</div>
)
}
}
Then the style is passed to the first div in Comment component.
While loading the comments the animation is OK. But user add a comment, and then the fetchComments method is called to fetch all the comments, the animation does not occur. Is this something to do with redux? I am passing my comment using mapStateToProps and connect.
The problem was with the key. The animation was occuring, but at the bottom of the comments, as mapping through them assigned them a key based on index in the array. When I changed the key to contain comment.id number it started working properly!

Implementing a momentary indicator in React

My goal is to have a save indicator that flashes a save icon when data has just been saved (not while it's being saved), as an indication to the user that their edit was successful. React seems better suited towards state than one-off "actions," but this was the best I was able to come up with:
import React, { PureComponent, PropTypes } from 'react';
import Radium from 'radium';
import moment from 'moment';
import { ContentSave as SaveIcon } from 'material-ui/svg-icons';
class SaveIndicator extends PureComponent {
getStyles = () => {
if (!this.props.saving) return { opacity: 0 };
return {
animation: 'x 700ms ease 0s 3 normal forwards',
animationName: saveAnimation,
};
};
render() {
return <div style={styles.root}>
<div style={{ display: 'flex' }}>
<div style={{ marginRight: 16 }}>
Last saved {moment(this.props.lastSaved).fromNow()}
</div>
<div
style={this.getStyles()}
onAnimationEnd={() => this.props.onIndicationComplete()}
>
<SaveIcon color="#666" />
</div>
</div>
</div>
}
}
const saveAnimation = Radium.keyframes({
'0%': { opacity: 0 },
'50%': { opacity: 1 },
'100%': { opacity: 0 },
});
const styles = {
root: {
display: 'inline-block',
},
};
SaveIndicator.defaultProps = {
saving: false,
};
SaveIndicator.propTypes = {
lastSaved: PropTypes.object.isRequired,
onIndicationComplete: PropTypes.func,
saving: PropTypes.bool,
};
export default Radium(SaveIndicator)
It works, but is there a way I could streamline this and make it even shorter?
How about this. I had a component a while back that needed something similar to what you're describing. I'll paste it in because it works fully but the strategy is kind of like this: pass in a time to start the animation. This prop triggers a function to start the animation which grabs the difference between that time and "now". It iteratively sets the state to close the gap between the initial time and now until it exceeds the passed in duration prop.
class SaveIndicator extends Component {
static propTypes = {
children: Types.element,
// time in milliseconds when the save is started; can change
indicationStartTime: Types.number.isRequired,
// time in milliseconds that the animation will take to fade
duration: Types.number,
// time in milliseconds to wait between renderings
frameRate: Types.number,
};
static defaultProps = {
duration: 7000,
frameRate: 100,
}
constructor(props) {
super(props);
this.state = { opacity: 0 };
}
componentDidMount() {
this.startAnimation();
}
componentWillReceiveProps({ indicationStartTime }) {
if (indicationStartTime !== this.props.indicationStartTime) {
this.startAnimation();
}
}
startAnimation() {
const { indicationStartTime, duration, frameRate } = this.props;
const now = new Date().getTime();
const newOpacity = 1 - ((now - indicationStartTime) / duration);
if (now - indicationStartTime < duration) {
this.setState({ opacity: newOpacity }, () =>
setTimeout(::this.startAnimation, frameRate)
);
} else {
this.setState({ opacity: 0 });
}
}
render() {
const { children } = this.props;
const { opacity } = this.state;
const style = { opacity };
return <div style={style}>{children}</div>;
}
}

Resources