Strange scroll behaviour when using useState in React - javascript

I started implementing a carousel in react, furthermore I wanted to give it an infinite scroll effect, that is if you keep clicking right and reaches the end. It starts from the beginning.
The funny thing is, I had to use useState and I am noticing very strange behavior. It never reaches the last div but goes to the first one instantly (No scrolling effect) and scrolls to the second one.
This the main file where the scrolling is happening.
Carousel.tsx
import React, { FC, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useStyles, useStylesClasses } from "./CarouselCSS";
import ArrowRightAltIcon from '#mui/icons-material/ArrowRightAlt';
interface Props {
className?: string;
}
type Scroll = "left" | "right";
type Index = {
sign: "To" | "By";
value: number;
}
export const Carousel: FC<Props> = ({className, children}) => {
const {Root, Carousel} = useStyles();
const classes = useStylesClasses();
const ref = useRef(null);
const firstRun = useRef(true);
const [activeIndex, setActiveIndex] = useState<Index>({
sign: "To",
value: 0
});
// Triggers before rendering the page
// useLayoutEffect(()=>{
// console.log(ref.current.children.length);
// }, [ref])
useEffect(() => {
if(!firstRun.current)
if (activeIndex.sign === "To"){
console.log({"To": activeIndex.value})
ref.current.scrollTo({
left: (activeIndex.value*ref.current.clientWidth),
behavior: "smooth",
})
}
else{
console.log("By")
ref.current.scrollBy({
left: ref.current.clientWidth,
behavior: "smooth",
})
}
else
firstRun.current = false;
}, [activeIndex])
// controlling scrolling and making it infinite
const scroll = (value: Scroll) => {
switch(value){
case "left": {
console.log("Left")
if(activeIndex.value === 0){
setActiveIndex({sign: "By", value: ref.current.children.length - 1});
}
else{
setActiveIndex({sign: "To", value: activeIndex.value - 1});
}
}
break;
case "right": {
console.log("Right")
if(activeIndex.value === ref.current.children.length - 1){
setActiveIndex({sign: "To", value: 0});
}
else{
setActiveIndex({sign: "By", value: activeIndex.value + 1});
}
}
break;
}
}
return (
<Root>
<div onClick={() => {
scroll("left");
}
}
>
<ArrowRightAltIcon style={{transform: 'rotate(180deg)'}} className={classes.arrow}/>
</div>
<Carousel className={className} ref={ref}>
{children}
</Carousel>
<div onClick={() => {
scroll("right");
}}
>
<ArrowRightAltIcon className={classes.arrow}/>
</div>
</Root>
)
}
If this doesn't make proper sense, I am also attaching the code sandbox link to the whole thing.
Update
As suggested, the component re-renders and hence why the instant appearance of the 1st div. Now, what I am trying to understand here is, why it re-renders in certain cases and does scrolling in some? Since, I am using useState, shouldn't it re-render it every time?
https://codesandbox.io/embed/keen-wright-9n94s?fontsize=14&hidenavigation=1&theme=dark
Any help is appreciated!

That's because when you push the divs for moving, click events trigger re-render the component. Which means it doesn't scroll, it just shows you the new component with the updated activeIndex. This is why its behavior is not smooth. So you need to find other right way to scroll. To avoid moving to the first one when you hit the last one is to add last index check function into the scroll function. If it's the last index, just return nothing but it's not good solution. I suggest you to go find and learn how to move with scroll api in div box. After learning it, you will find out the solution.

Related

How to avoid scrollbar staying on top when loading more data (infinite scroll) in React

I'm making an infinite scroll up of the chat room, judging to see the last message and loading the data, but the scrollbar remains at the top of the page, you know, it will automatically execute the call api infinitely,how to keep the scrollbar at the previous message in the position
If you have a reference of the new element that is added, you can use element.scrollIntoView() to make sure it's visible after rendering.
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
But:
In chat-rooms, this may not always be the right thing to do, as the user may have scrolled up to copy/paste something, and losing the position would be annoying. So check if the chat was already on the bottom before you scroll via JS.
You can take a look at how twitch-chat works to get a good example. The user gets an info if there are new chat messages.
This solution is for library https://github.com/ankeetmaini/react-infinite-scroll-component
The structure I used is based on props passed to a child component. Parent component is doing the fetching and controlling the state.
Parent Component:
const fetchScrollData = () => {
// Here we set that we are not able to do scrolling fetching
setIsLoadingScrollData(true);
// Apply here some pagination calculation
searchData()
.then((tempData) => {
// Here i am saving the id of the first message
// I had before scrolling
setShouldScrollToId(data[0].id);
const newData = tempData.concat(data);
setData(newData);
// Apply here some logic, depending if you have finished or not
setHasMoreData(true);
})
.then(() => {
setTimeout(() => {
// Change the timeout accordingly
// hack for setting the isloading to false when everything is done
setIsLoadingScrollData(false);
}, 1500);
});
};
Child Component
type PropsType = {
fetchScrollData: () => void;
hasMoreData: boolean;
data: string[];
isLoadingScrollData: boolean;
shouldScrollToId: string;
};
const ChildComponent: FC<PropsType> = (props: PropsType) => {
useEffect(() => {
if (props.shouldScrollToId !== '') {
const element = document.getElementById(props.shouldScrollToId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
});
....
return (
<div
className="message-list overflow-scroll"
id="scrollableDiv"
style={{
display: 'flex',
flexDirection: 'column-reverse'
}}>
<InfiniteScroll
dataLength={props.data.length}
hasMore={props.hasMoreData && !props.isLoadingScrollData}
inverse={true}
next={props.fetchScrollData}
scrollableTarget="scrollableDiv"
style={{ display: 'flex', flexDirection: 'column-reverse', perspective: '1px' }}
loader={<></>}>
<div>
{props.data.map((d, index) => {
// Here i am setting the id which the useEffect will use to do the scroll
<div key={index} id={d.id} >div - #{message}</div>;
})}
</div>
{props.isLoadingScrollData ? <Spinner></Spinner> : null}
</InfiniteScroll>
</div>
I had some problems with the loader, so I decided not use it. Instead I created my own Spinner Component and make it appear or disappear according to the prop which shows if we are still fetching data from the scroll.

How to change background based on page url react js?

I have a simple app with a few pages, now I would like to change the background color based on page URL
using react js,
What is expected?:
When a pathname is /movies I want to change the background to red
Here is what I have so far
import React from 'react'
function Testing() {
const[moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState('green');
const getMoviesUrl = window.location.pathname;
if(getMoviesUrl == '/movies'){
setMoviesUrlBackgroundColor('red');
}else{
setMoviesUrlBackgroundColor('green');
}
return (
<div>
<Container style={{backgroundColor:moviesUrlBackgroundColor}}>
Testing
</Container>
</div>
)
}
export default Testing
const Container = styled.div`
background-color:green
`;
Unfortunately, I am getting the following URL
app.js:38323 Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
What do I need to do this working?
You should have an extra check to make sure you have set background color or not.Your current code is causing rerender infinte times
import React from 'react'
function Testing() {
const[moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState('green');
const [bgFlag, setbgFlag] = useState(false);
const getMoviesUrl = window.location.pathname;
if(!bgFlag){
setMoviesUrlBackgroundColor(getMoviesUrl == '/movies' ? 'red' : 'green')
setbgFlag(true)
}
return (
<div>
<Container style={{backgroundColor:moviesUrlBackgroundColor}}>
Testing
</Container>
</div>
)
}
export default Testing
const Container = styled.div`
background-color:green
`;
Use an useEffect block so you can perform side-effects effectively.
useEffect(() => {
if(getMoviesUrl === '/movies'){
console.log("running")
setMoviesUrlBackgroundColor('red');
}else{
setMoviesUrlBackgroundColor('green');
}
},[getMoviesUrl]);
The problem is that you call setMoviesUrlBackgroundColor without wrapping in an effect which results in getting called recursively.
In order to fix this, you just simple set state as needed in this case is your pathname has been changed:
React.useEffect(() => {
if (getMoviesUrl == '/movies'){
setMoviesUrlBackgroundColor('red');
} else{
setMoviesUrlBackgroundColor('green');
}
}, [getMoviesUrl])
Ok, I think I got it, you're creating an infinite loop on your first if statement:
if (getMoviesUrl == '/movies') {
// every time you change state, that causes your component
// to re-render, and when it re-renders again you're checking
// changing your state AGAIN, so it's an infinite loop
setMoviesUrlBackgroundColor('red');
}
I'll would recommend using react-route for this and getting the url from params, and then update the background color on componentDidMount or useEffect hook when component mounts for the first time, to prevent infinite loops.
Window.location.pathname was resetting state each render, so it needs to be placed within the useEffect hook to prevent re-renders.
Also, this would be a good use-case to pass props to your styled component.
Also included this in the code below.
Here's a link to a codesandbox I made with the solution.
https://codesandbox.io/s/musing-mirzakhani-njmsh?file=/src/random.js:0-620
import React, { useState, useEffect } from "react";
import styled from "styled-components";
const Container = styled.div`
background: ${(props) => props.backgroundColor || "green"};
`;
const Testing = () => {
const [moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState(
"green"
);
useEffect(() => {
const getMoviesUrl = window.location.pathname;
if (getMoviesUrl === "/movies") {
setMoviesUrlBackgroundColor("yellow");
}
}, [moviesUrlBackgroundColor]);
return (
<div>
<Container backgroundColor={moviesUrlBackgroundColor}>Test</Container>
</div>
);
};
export default Testing;
Cheers!

A better way to write this React Class Component with Hooks?

I have a section with a fixed height. I don't know when the component mounts (first renders) whether the content coming in will fit or not. If it does NOT fit, then I need to render a 'Read More' button.
It looks like this:
I wrote this originally as a Class component using the lifecycle methods DidMount/DidUpdate:
Class Component
import React, { createRef } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
class ParagraphList extends React.Component {
state = {
overflowActive: false,
}
wrapper = createRef() // so we can get a ref to the height container
isOverflowing(el) {
if (el) return el.offsetHeight < el.scrollHeight
}
componentDidMount() {
this.setState({ overflowActive: this.isOverflowing(this.wrapper.current) })
}
componentDidUpdate() {
if (this.wrapper.current && !this.state.overflowActive) {
this.setState({
overflowActive: this.isOverflowing(this.wrapper.current),
})
}
}
handleClick() {
this.setState({ overflowActive: false })
}
render() {
const { moreButtonText, titleText, paragraphs, theme } = this.props
return (
<>
<Section overflowActive={this.state.overflowActive}>
{this.state.overflowActive || !this.wrapper.current ? (
<StyledHeightContainer ref={this.wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(this)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
}
export default ParagraphList
My best way to explain the flow:
When the component mounts, the flag is false and we have no reference to the div so the StyledHeightContainer will try to render and thus provide a ref to it
In componentDidMount -> try to set the overflow flag (which will be false because at this point we do not yet have rendering completed so the ref will be null). But by setting the flag anyway, we queue an additional render pass
1st INITIAL rendering completes -> we have a ref to the div now
The 2nd (queued) render occurs, firing the componentDidUpdate -> we can calculate the overflow and set the flag to true when the content overflows
When the user clicks the button -> set the flag to false, which will trigger a re-render and hence the StyledHeightContainer will be removed from the DOM.
Functional Component With Hooks
Sandbox of the code
When I re-wrote this as a functional component using Hooks, I ended up with this:
import React, { createRef, useEffect, useState } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
const ParagraphList = ({ moreButtonText, titleText, paragraphs, theme }) => {
const [overflowActive, setOverflowActive] = useState(false)
const [userClicked, setUserClicked] = useState(false)
const wrapper = createRef(false) // so we can get a ref to the height container
const isOverflowing = el => {
if (el) return el.offsetHeight < el.scrollHeight
}
useEffect(() => {
if (!userClicked && !overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
}, [userClicked]) // note: we only care about state change if user clicks 'Read More' button
const handleClick = () => {
setOverflowActive(false)
setUserClicked(true)
}
return (
<>
<Section theme={theme} overflowActive={overflowActive}>
{!userClicked && (overflowActive || !wrapper.current) ? (
<StyledHeightContainer ref={wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(null)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
export default ParagraphList
I was surprised that I needed to add another state (userClicked), which is how I force the 2nd render to occur (ie. the equivalent to the componentDidUpdate in the class solution).
Is this correct or can someone see a more concise way to write the 2nd solution?
NOTE
One of the reasons I ask is because in the console I get this warning:
48:6 warning React Hook useEffect has missing dependencies:
'overflowActive' and 'wrapper'. Either include them or remove the
dependency array react-hooks/exhaustive-deps
and I don't THINK I want to add them to the dependency array, as I don't want to trigger rendering when they change...?
I really enjoyed while solving the query.
Here is the implementation: https://codesandbox.io/s/react-using-hooks-in-section-component-5gibi?file=/src/ParagraphList.js
First of all, I was thinking of
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, [wrapper]);
But if we do this, it will again call the useEffect as when we'll click on the Read more button. Because it was comparing the reference of the wrapper and not it's value.
So, to avoid the reference comparison we have to use the useCallback hook.
const isOverflowingNode = node => {
return node.offsetHeight < node.scrollHeight;
};
const wrapper = useCallback(node => {
if (node !== null) {
setOverflowActive(isOverflowingNode(node));
}
}, []);
I came across the beautiful discussion: https://github.com/facebook/react/issues/14387
For more information:
https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
Thanks for the question :)
You could add an extra useEffect(() => (...),[]) that acts like componentDidMount(). And another useEffect(() => (...)) that acts like componentDidUpdate(). Then you should be able to get rid of userClicked.
This is a good link on how the lifestyle methods work with hooks. https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, []);
useEffect(() => {
if (!overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
});
The second one might need to be useLayoutEffect if you are wanting the update to happen after the layout.

React.js Drag and Drop - infinite loops on Drop

I have a table with Drag and Drop method and everything works fine except Drop. So when I want to Drop item into the table's cell there's an error which is caused by infinite loop.
i would be grateful If somebody could explain the reason of this error.
import React, {Component} from 'react';
import { DropTarget } from 'react-dnd';
function collect(connect, monitor) {
return {
connectDropTarget: connect.dropTarget,
isOver: monitor.isOver,
item: monitor.getItem,
}
}
class DropCells extends Component {
state = {
tdValue: ""
};
render() {
const { connectDropTarget, isOver, item} = this.props;
const backgroundColor = isOver ? 'lightgreen' : 'white';
const dropped = isOver? this.setState({tdValue: this.props.item}): this.state.tdValue;
return connectDropTarget(
<td className="target" style={{ background: backgroundColor }}>
{dropped}
</td>
);
}
}
export default DropTarget('item', {}, collect)(DropCells);
You have a call to this.setState({tdValue: this.props.item}) inside of your render function. Every time your component re-renders, it will call this.setState which will trigger another render.

React Native: TextInput with state and AsyncStorage

When typing on the keyboard I was seeing some warnings about the input being ahead of the JS code..
Native TextInput(react native is awesome) is 4 events ahead of JS - try to make your JS faster.
So added the debounce and got this to "work":
...
import { debounce } from 'lodash'
...
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
data,
indexRef: data.reduce((result, item, index) => {
result[item.title] = index
return result
}, {}),
ready: false,
}
this.updatePitch = this.updatePitch.bind(this)
this.saveLocally = debounce(this.saveLocally, 300).bind(this)
}
...
updatePitch(id, text) {
// Copy the data
let data = [...this.state.data]
const index = data.findIndex(obj => obj.id == id)
data[index].pitch = text
// Update the state
this.setState({ data }, this.saveLocally(data))
}
saveLocally(data) {
try {
AsyncStorage.setItem('data', JSON.stringify(data))
this.forceUpdate()
} catch (error) {
// Well..
}
}
render() {
...
BTW: I'm doing some "prop drilling" for now - but planning to do use the Context API (react 16.3)
The warning seems to have gone by adding debounce (1).. But I'm seeing some strange issues - particularly on the iPhone 8 plus simulator (not seeing the same on iPhone 6 simulator or Android device)
Issues observed:
TextInput don't expand - it just add scolling (expands on iPhone 6 and Android device)
Some layout issues in the FlatList - seems like it has problems finding correct height of list elements..
What is the best practice for fast JS code and saving to both state and AsyncStorage?
(1) One other way than using debounce could be to use getDerivedStateFromProps and add some sort of timer pushing the state to the parent component after some period of time.. But wasn't sure that this would make the JS code faster. So didn't try it.
UPDATE (again)
I open sourced the entire code since it is too hard to give all the needed information in a SO post when the code is so nested.
The entire code is here:
https://github.com/Norfeldt/LionFood_FrontEnd
(I know that my code might seem messy, but I'm still learning..)
I don't expect people to go in and fix my code with PR (even though it would be awesome) but just give me some code guidance on how to proper deal with state and AsyncStorage for TextInput.
I know I have some style issues - would love to fix them, but also comply with SO and keep it on topic.
Update II
I removed forceUpdate and replaced FadeImage with just vanilla react native Image.
but I'm still seeing some weird issues
Here is my code
import React from 'react'
import {
StyleSheet,
SafeAreaView,
FlatList,
StatusBar,
ImageBackground,
AsyncStorage,
Platform,
} from 'react-native'
import SplashScreen from 'react-native-splash-screen'
import LinearGradient from 'react-native-linear-gradient'
import { debounce } from 'lodash'
import Section from './Section'
import ButtonContact from './ButtonContact'
import { data } from '../data.json'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
data,
indexRef: data.reduce((result, item, index) => {
result[item.title] = index
return result
}, {}),
ready: false,
}
}
async componentDidMount() {
SplashScreen.hide()
try {
let BusinessPlan = await AsyncStorage.getItem('BusinessPlan')
if (BusinessPlan !== null) {
// We have data!!
let data = JSON.parse(BusinessPlan)
data = this.state.data.map(item => {
const index = data.findIndex(obj => obj.id == item.id)
const pitch = index >= 0 ? data[index].pitch : ''
return { ...item, pitch }
})
this.setState({ data, ready: true })
} else {
this.setState({ ready: true })
}
} catch (error) {
// Error retrieving data
}
}
updatePitch = (id, text) => {
// Copy the data
let data = [...this.state.data]
const index = data.findIndex(obj => obj.id == id)
data[index].pitch = text
// Update the state
this.setState({ data }, this.saveLocally(data))
}
saveLocally = data => {
try {
AsyncStorage.setItem('BusinessPlan', JSON.stringify(data))
} catch (error) {
// Well..
}
}
render() {
return (
<LinearGradient
style={{ flex: 1 }}
start={{ x: 0.0, y: 0.25 }}
end={{ x: 0.5, y: 1.0 }}
colors={['#000000', '#808080', '#000000']}
>
<StatusBar
barStyle={'light-content'}
backgroundColor={Platform.OS == 'iOS' ? 'transparent' : 'black'}
/>
<SafeAreaView>
<ImageBackground
source={require('../images/BackgroundImage.png')}
style={{ width: '100%', height: '100%' }}
resizeMode={'cover'}
>
<FlatList
data={this.state.data}
initialNumToRender="16"
keyExtractor={item => item.id}
renderItem={({ item }) => (
<Section
id={item.id}
title={item.title}
pitch={item.pitch}
updatePitch={debounce(this.updatePitch, 1000)}
questions={item.questions}
ready={this.state.ready}
/>
)}
ListFooterComponent={<ButtonContact />}
style={{
backgroundColor: 'transparent',
borderColor: '#000',
borderWidth: StyleSheet.hairlineWidth,
}}
/>
</ImageBackground>
</SafeAreaView>
</LinearGradient>
)
}
}
const styles = StyleSheet.create({
sectionHeader: {
fontSize: 24,
marginHorizontal: 5,
},
})
(I also updated my git repo)
Update III
It seems that the setup I have for state and AsyncStorage works fine with a debounce. The issues I was seeing was because I'm draining the CPU (next step to fix).
I tried your code:
"I'm seeing some strange issues - particularly on the iPhone 8 plus
simulator (not seeing the same on iPhone 6 simulator or Android
device)" ==> I confirmed this
The app takes about 100% CPU.
After a while trying I figured out:
"I'm seeing some strange issues - particularly on the iPhone 8 plus
simulator (not seeing the same on iPhone 6 simulator or Android
device)" ==> doesn't right, just wait a little TextInput will expand.
There are nothing wrong with state and AsyncStorage. I didn't get any warning.
The root issue is the animation in FadeImage :
The app render many Carousel, and each Carousel has AngleInvestor, and FadeImage. The problem is FadeImage
FadeImage run Animated with duration 1000 => CPU is overloaded
==> That why TextInput add scroll then take a long time to expand, and FlatList look like has problem, but not. They are just slowly updated.
Solution:
Try to comment FadeImage, you will see the problem gone away.
Don't start so many animations as the same time. Just start if it appears (Ex: the first card in Carousel )
UPDATE
I got your problem: typing fastly cause setState call so many times.
You use can debounce for this situation:
In App.js
render() {
console.log('render app.js')
...
<Section
id={item.id}
title={item.title}
pitch={item.pitch}
updatePitch={debounce(this.updatePitch, 1000)} // the key is here
questions={item.questions}
ready={this.state.ready}
/>
You can change the delay and watch the console log to see more. As I tried, delay about 500 can stop the warning.
P/s: You should try to remove forceUpdate

Categories

Resources