My onScroll event is not firing in react js.
I am trying to set infinite scrolling in react js , but my onScroll event is not firing away.
it fetches posts from api and send it to the Post component. and i am rendering the post in the Post component.
Feed.js
import { Box } from "#mui/material";
import Post from "./Post";
import About from "./About";
import { useEffect, useState } from "react";
import { getInfiniteScroll } from "../apis/posts";
const Feed = () => {
const [data, setData] = useState([]);
const [skip, setSkip] = useState(0);
useEffect(() => {
fetchPosts();
document
.getElementById("scroll_div")
.addEventListener("scroll", handleScroll, true);
// Remove the event listener
return () => {
document
.getElementById("scroll_div")
.removeEventListener("scroll", handleScroll, true);
};
}, [skip]);
const fetchPosts = async () => {
try {
const newPosts = await getInfiniteScroll(skip);
setData(newPosts);
} catch (error) {
console.log(error.message);
}
};
const handleScroll = (e) => {
const { offsetHeight, scrollTop, scrollHeight } = e.target;
console.log(e.target);
if (offsetHeight + scrollTop >= scrollHeight) {
setSkip(data?.length);
}
console.log(skip);
};
return (
<Box
flex={4}
sx={{ padding: { xs: "0", sm: "0px 20px " }, overflow: "auto" }}
onScroll={handleScroll}>
<Post post={data} />
<Box
sx={{
display: { sm: "none", xs: "block" },
justifyContent: "center",
alignItems: "center",
paddingBottom: "50px",
}}>
<About />
</Box>
</Box>
);
};
export default Feed;
Please help !!
I don't know if onScroll is supported by material's Box component. What you can instead do is use refs
function App() {
const ref = useRef()
useEffect(() => {
const handleScroll = (e) => {
// Do your stuff here
};
ref.current?.addEventListener("scroll", handleScroll);
return () => ref.current?.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box ref={ref} />
);
}
Related
In a media player application, I try to use "expo-av" library to build a playlist. everything is working fine. But when I press on the backbutton, it is not behaving properly. I tried in many way. but nothing works for me.
I tried while handling backButton, like, sound.unloadAsync(), sound.stopAsync(), setSound(null).
import React, { useEffect, useState } from 'react';
import {
View,
BackHandler,
Text,
TouchableWithoutFeedback,
StyleSheet,
} from 'react-native';
import * as Progress from 'react-native-progress';
import { connect } from 'react-redux';
import { MaterialCommunityIcons } from '#expo/vector-icons';
import { Audio } from 'expo-av';
const sectionsAllCards = [
{
id: 'audio-01',
name: 'Body scan: Generic under mindfulness',
link: 'Bodyscan.m4a',
}
];
const MusicPlayerList = ({ navigation, route, ...props }) => {
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [audioIndex, setAudioIndex] = useState(0);
const [soundObject, setSoundObject] = useState(null);
const audioSources = [
require('../../assests/musics/Bodyscan.m4a')
];
const togglePlayback = async () => {
if (isPlaying) await soundObject.pauseAsync();
else await soundObject.playAsync();
setIsPlaying(!isPlaying);
};
const onPlaybackStatusUpdate = (status) => {
setProgress(status.positionMillis / status.durationMillis);
};
useEffect(() => {
const loadAudio = async () => {
const source = audioSources[audioIndex];
const sound = new Audio.Sound();
try {
await sound.loadAsync(source);
setSoundObject(sound);
sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
} catch (error) {
console.log(error);
}
};
loadAudio();
}, [audioIndex]);
async function handleBackButtonClick() {
navigation.navigate('LoginSignup');
return true;
}
useEffect(() => {
BackHandler.addEventListener(
'hardwareBackPress',
handleBackButtonClick,
);
return () => {
BackHandler.removeEventListener(
'hardwareBackPress',
handleBackButtonClick,
);
};
}, []);
const handleOnPress = async (index) => {
if (index === audioIndex) togglePlayback();
else {
setIsPlaying(false);
setProgress(0);
await soundObject.stopAsync();
setSoundObject(null);
setAudioIndex(index);
}
};
return (
<View style={{ backgroundColor: '#efefef', flex: 1 }}>
{sectionsAllCards.map((card, index) => (
<TouchableWithoutFeedback
key={card.id}
onPress={() => handleOnPress(index)}
>
<View style={styles.boxContainer}>
<Text style={styles.audioText}>{card.name}</Text>
<View style={styles.audioIconContainer}>
{progress >= 0 && progress <= 1 && (
<View>
<Progress.Circle
style={styles.progress}
progress={audioIndex === index ? progress : 0}
indeterminate={false}
showsText={false}
size={60}
borderWidth={2}
color={'#479162'}
/>
<Text
style={{
position: 'absolute',
left: 11,
top: 10,
}}
>
<MaterialCommunityIcons
name={
isPlaying && audioIndex === index
? 'pause'
: 'play'
}
size={38}
style={{ color: '#479162' }}
/>
</Text>
</View>
)}
</View>
</View>
</TouchableWithoutFeedback>
))}
</View>
);
};
const styles = StyleSheet.create({
boxContainer: {
},
audioText: {
},
});
const mapStateToProps = (state) => ({
accessToken: state.auth.accessToken,
});
export default connect(mapStateToProps, {})(MusicPlayerList);
Now I have created this custom hook to perform lazy loading,which takes redux slice action as input and
import { useState, useEffect, useCallback, useRef } from "react";
import { useDispatch } from "react-redux";
function useLazyFetch(fetchAction) {
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const loadMoreRef = useRef(null);
const handleObserver = useCallback(async(entries) => {
const [target] = entries;
console.log(target.isIntersecting);
if (target.isIntersecting) {
console.log("INTERSECTING.....");
await new Promise((r) => setTimeout(r, 2000));
setPage((prev) => prev + 1);
}
}, []);
useEffect(() => {
const option = {
root: null,
rootMargin: "0px",
threshold: 1.0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
}, [handleObserver]);
const fetchApi = useCallback(async () => {
try {
setLoading(true);
await new Promise((r) => setTimeout(r, 2000));
dispatch(fetchAction(page))
setLoading(false);
} catch (err) {
console.error(err);
}
}, [page,fetchAction,dispatch]);
useEffect(() => {
fetchApi();
}, [fetchApi]);
return { loading, loadMoreRef };
}
export default useLazyFetch;
I am using this in my component like this, here you can see I am tracking div in the bottom using loadMoreRef from useLazyFetch, Now when I am commenting out the fetchApi(); from custom hook its working as expected, on scroll its logging INTERSECTING... in the console but the moment I try to execute the action through fetchApi() my whole app goes into loop,the div tracker with ref comes to top and it fetches the posts but after immediately that action repeats the tracker comes to top and page becomes empty & it fetches next set of posts,I can see that my list is getting appended new set of posts to state in redux dev tool instead of completely setting new state, but in UI it's rendering all posts again and again whic is causing the loop,how can I avoid this ?
import { CircularProgress, Grid, IconButton, Typography } from "#mui/material";
import { Box } from "#mui/system";
import React, { useEffect,useRef,useState } from "react";
import AssistantIcon from "#mui/icons-material/Assistant";
import Post from "../components/Post";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../redux/postSlice";
import AddPost from "../components/AddPost";
import useLazyFetch from "../hooks/useLazyFetch";
export default function Home() {
const dispatch = useDispatch();
// const api = `https://picsum.photos/v2/list`
const { status, posts } = useSelector((state) => state.post);
const {loading,loadMoreRef} = useLazyFetch(getPosts)
useEffect(() => {
dispatch(getPosts());
}, []);
return (
<Box>
<Box borderBottom="1px solid #ccc" padding="8px 20px">
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<Typography variant="h6">Home</Typography>
</Grid>
<Grid item>
<IconButton>
<AssistantIcon />
</IconButton>
</Grid>
</Grid>
</Box>
<Box height="92vh" sx={{ overflowY: "scroll" }}>
<AddPost />
<Box textAlign="center" marginTop="1rem">
{status === "loading" && (
<CircularProgress size={20} color="primary" />
)}
</Box>
{status === "success" &&
posts?.map((post) => <Post key={post._id} post={post} />)}
<div style={{height:"50px",width:"100px",backgroundColor:"red"}} ref={loadMoreRef}>{loading && <p>loading...</p>}</div>
</Box>
</Box>
);
}
And here is my redux action & state update part
const initialState = {
status: "idle",
posts: []
};
export const getPosts = createAsyncThunk("post/getPosts", async (page) => {
console.log(page);
console.log("calling api ...");
const { data } = await axios.get(`/api/posts?page=${page}`);
return data;
});
export const postSlice = createSlice({
name: "post",
initialState,
reducers: {},
extraReducers: {
[getPosts.pending]: (state, action) => {
state.status = "loading";
},
[getPosts.fulfilled]: (state, action) => {
state.status = "success";
state.posts = [...state.posts,...action.payload.response.posts] ;
},
[getPosts.rejected]: (state, action) => {
state.status = "failed";
},
}
this is the solution that is working
import { CircularProgress, Grid, IconButton, Typography } from "#mui/material";
import { Box } from "#mui/system";
import React, { useEffect,useMemo } from "react";
import AssistantIcon from "#mui/icons-material/Assistant";
import Post from "../components/Post";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../redux/postSlice";
import AddPost from "../components/AddPost";
import useLazyFetch from "../hooks/useLazyFetch";
export default function Home() {
const { status, posts } = useSelector((state) => state.post);
const {loading,loadMoreRef} = useLazyFetch(getPosts)
const renderedPostList = useMemo(() => (
posts.map((post) => {
return( <Post key={post._id.toString()} post={post} />)
})
), [posts])
return (
<Box>
<Box borderBottom="1px solid #ccc" padding="8px 20px">
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<Typography variant="h6">Home</Typography>
</Grid>
<Grid item>
<IconButton>
<AssistantIcon />
</IconButton>
</Grid>
</Grid>
</Box>
<Box height="92vh" sx={{ overflowY: "scroll" }}>
<AddPost />
<Box textAlign="center" marginTop="1rem">
{status === "loading" && (
<CircularProgress size={20} color="primary" />
)}
</Box>
{renderedPostList}
<div style={{height:"50px",width:"100px",backgroundColor:"red"}} ref={loadMoreRef}>{loading && <p>loading...</p>}</div>
</Box>
</Box>
);
}
}
I used useMemo hook to memoize and it works as expected
I want to fetch only values of '1' and '3' from the Arraytesting(array part). Please check the Image so that you will understand my question. Can I solve this by coding inside Flatlist part or do I need to change the Firestore data fetching method?
And if I fetch data from Array there should not be an empty field for the remaining value of 0, 2. Yesterday I posted a question that question was similar to this question but this time I am trying to fetch only array data. #DrewReese supported me on that problem. Please check my previous question link- React Native Firebase Firestore data not fetching properly
import React, { useState, useEffect } from 'react';
import { ActivityIndicator, FlatList, View, Text } from 'react-native';
import {firebase} from '../config';
const Testing = ({ navigation }) =>{
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [users, setUsers] = useState([]);
useEffect(() => {
const subscriber = firebase.firestore()
.collection('testing')
.onSnapshot(querySnapshot => {
const users = [];
querySnapshot.forEach(documentSnapshot => {
users.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setUsers(users);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}, []);
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={users}
renderItem={({ item }) => (
<View style={{ height: 50, flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>ID: {item.One}</Text>
<Text>Name: {item.five}</Text>
</View>
)}
/>
);}
export default Testing;
Hey you can try something like this :
filtering out for index 1 and index 3
import React, { useState, useEffect } from 'react';
import { ActivityIndicator, FlatList, View, Text } from 'react-native';
import {firebase} from '../config';
const Testing = ({ navigation }) =>{
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [users, setUsers] = useState([]);
const [filteredusers, setFiltered] = useState([])
useEffect(() => {
const subscriber = firebase.firestore()
.collection('testing')
.onSnapshot(querySnapshot => {
const users = [];
querySnapshot.forEach(documentSnapshot => {
users.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setUsers(users);
//setting here
const newFiltered = users?.filter((data,index) => (index === 1 || index ===3) );
setFiltered(newFiltered)
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}, []);
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={filteredusers} // users is now filtered
renderItem={({ item }) => (
<View style={{ height: 50, flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>ID: {item?.id}</Text>
<Text>Name: {item?.name}</Text>
</View>
)}
/>
);}
export default Testing;
The idea I have for this to work is to copy my main array from firebase (Array with all my info) into a temporary array. Then I will filter this temporary array and have it displayed in flatlist. The onPress for the buttons serves to select the type of information to be filtered (Eg type 1 or 2 or 3).
import React, { useState, setState, useEffect } from 'react';
import { View, Text, StyleSheet, ImageBackground, FlatList, Button, TouchableHighlight } from 'react-native'
import Card from '../components/Card';
import colors from '../config/colors';
import {MaterialCommunityIcons} from '#expo/vector-icons'
import { useNavigation } from '#react-navigation/core';
import AppButton from '../components/AppButton'
import { SearchBar } from 'react-native-screens';
import { Firestore, getDoc, collection, getDocs,
addDoc, deleteDoc, doc,
query, where, onSnapshot
} from 'firebase/firestore';
import {db} from '../../firebase';
import { async } from '#firebase/util';
//run: 1
//swim: 2
//cycle: 3
function EventsPage(props) {
const [events, setEvents] = useState([]);
const [bool, setBool] = useState(false);
const [arr, setArr] = useState([])
const colRef = collection(db, 'events');
const q = query(colRef, where('type', '>=', 0))
onSnapshot(q, (snapshot) => {
const events = []
snapshot.docs.forEach((doc) => {
events.push({...doc.data()}) //put the data into an array
})
if (bool === false) {
setEvents(events);
setBool(true)
}
})
const updateSearch = search => {
setArr({ search }, () => {
if (type == search) {
setArr({
data: [...arr.temp]
});
return;
}
arr.data = arr.temp.filter(function(item){
return item.name.includes(search);
}).map(function({date, subTitle, title, type}){
return {date, subTitle, title, type};
});
});
};
return (
<View style = {styles.container}>
<ImageBackground
source = {require('../assets/splash-page.jpg')}
style = {{width: '100%', height: '100%'}}
blurRadius={8}
>
<View style = {styles.backIcon}>
<MaterialCommunityIcons name='arrow-left-bold' color="black" size={35} onPress={() => {
navigation.replace("Connect_me")
}} />
</View>
<Text style = {styles.text}>EVENTS</Text>
<View style = {styles.buttons}>
<Button title={'All'} />
<Button title={'Run'} onPress = {updateSearch(1)}/>
<Button title={'Swim'} onPress = {updateSearch(2)}/>
<Button title={'Cycle'} onPress = {updateSearch(3)}/>
</View>
<FlatList
data={arr.data}
renderItem={({ item }) => (
<View style = {styles.cardContainer}>
<Card title={item.title} subTitle = {item.subTitle}/>
</View>
)}
/>
<Text></Text>
<Text></Text>
<Text></Text>
<Text></Text>
<Text></Text>
</ImageBackground>
</View>
// trolling
);
}
const styles = StyleSheet.create({
text: {
fontSize: 80,
textAlign:'center',
paddingBottom: 20,
color: colors.primary,
fontWeight: 'bold',
},
container: {
backgroundColor: 'white',
},
cardContainer: {
width: '95%',
alignSelf: 'center'
},
backIcon: {
paddingLeft: 10,
paddingTop: 30,
},
searchIcon: {
paddingLeft: 10,
paddingTop: 15,
position: 'relative',
left: 330,
bottom: 47
},
buttons: {
flexDirection: 'row',
padding: 20,
}
})
export default EventsPage;
Side effects code like listening to data change events should be handled inside the useEffect hook.
The app continues to listen to data change events for each component re-render which causes an infinity loop.
useEffect(() => {
const unsubscribe = onSnapshot(q, (snapshot) => {
const events = [];
snapshot.docs.forEach((doc) => {
events.push({ ...doc.data() }); //put the data into an array
});
if (bool === false) {
setEvents(events);
setBool(true);
}
});
// unsubscribe to data change events when component unmount
return () => {
unsubscribe();
};
}, []);
I am trying to make authentication for a React JS project but, I am getting the following error:
Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I was reading other questions (like this and this) but I guess my situation is different since I set all the useState once the component is created.
I have used different approaches to get this done but neither of them worked for me.
I added the methods in the ContextWrapper component like this:
import React, { useState, useEffect } from "react";
import { useCookies } from "react-cookie";
import jwt from "jsonwebtoken";
import { fetchLogin, fetchVerify } from "../fake-server";
import Context from "./context";
export default ({ children }) => {
const [token, setToken] = useState(null);
const [message, setMessage] = useState(null);
const [loading, setLoading] = useState(false);
const [cookies, setCookie, removeCookie] = useCookies(["token"]);
useEffect(() => {
console.log(cookies);
if (cookies.token) {
setToken(cookies.token);
}
}, []);
useEffect(() => {
if (token) {
setCookie("token", JSON.stringify(token), { path: "/" });
} else {
removeCookie("token");
}
console.log(token);
}, [token]);
function login(email, password) {
fetchLogin(email, password)
/*.then(response => response.json())*/
.then(data => {
const decoded = jwt.decode(data.token);
const token = {
token: data.token,
...decoded
};
setToken(token);
})
.catch(error => {
console.log("error", error);
setMessage({
status: 500,
text: error
});
});
}
function verify() {
fetchVerify(cookies.token)
/*.then(response => response.json())*/
.then(data => {
setToken(data);
})
.catch(error => {
setToken(null);
setMessage({
status: 500,
text: error
});
});
}
function logout() {
setToken(false);
}
const value = {
login,
verify,
logout,
token,
message,
setMessage,
loading,
setLoading
};
return <Context.Provider value={value}>{children}</Context.Provider>;
};
And then this is my Login component:
import React, { useState, useContext, useEffect } from "react";
import {
Grid,
Card,
Typography,
CardActions,
CardContent,
FormControl,
TextField,
Button
} from "#material-ui/core";
import { Redirect } from "react-router-dom";
import { makeStyles } from "#material-ui/core/styles";
import Context from "../context/context";
const useStyles = makeStyles(theme => ({
root: {
height: "100vh",
display: "flex",
justifyContent: "center",
alignContent: "center"
},
title: {
marginBottom: theme.spacing(2)
},
card: {},
formControl: {
marginBottom: theme.spacing(1)
},
actions: {
display: "flex",
justifyContent: "flex-end"
}
}));
export default ({ history, location }) => {
const context = useContext(Context);
const classes = useStyles();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
if (context.token) {
return <Redirect to="/" />;
}
useEffect(() => {
context.setLoading(false);
}, []);
function onSubmit(e) {
e.preventDefault();
context.login(email, password);
}
return (
<Grid container className={classes.root}>
<Card className={classes.card}>
<form onSubmit={onSubmit}>
<CardContent>
<Typography variant="h4" className={classes.title}>
Login
</Typography>
<FormControl className={classes.formControl} fullWidth>
<TextField
type="text"
variant="outlined"
label="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</FormControl>
<FormControl className={classes.formControl} fullWidth>
<TextField
type="password"
variant="outlined"
label="Password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</FormControl>
</CardContent>
<CardActions className={classes.actions}>
<Button type="submit" variant="contained" color="secondary">
Login
</Button>
</CardActions>
</form>
</Card>
</Grid>
);
};
Here I created this sandbox to reproduce the error that happens when login() method is triggered. So, what am I doing wrong?
Move your condition under the useEffect.
useEffect(() => {})
if (context.token) {
return <Redirect to="/" />;
}
Because your if condition was affecting the subsequent hook below it, as it wouldn't run if context.token is true.
This violates the Rules of Hooks.
Pro Tip: Add ESlint Plugin as suggested in the React-Docs to get those warnings/errors while you code.
In my case, l use useWindowDimension to get height within a View's style. This mistake costs me more than 3 days of endless search until l realized useWindowDimension is a hook too. So the proper way to use it is this:
const window = useWindowDimension();
return (
<View style={{ height: window.height, }} />
);
Move the redirect logic after the hook.
useEffect(() => {
context.setLoading(false);
}, []);
if (context.token) {
return <Redirect to="/" />;
}
In your home component you are redirecting user if userIsValid id not true. And this filed will always be undefined as it is not defined in context. Add this field in the context and try.
Hope this will solve the issue.
The error from console tells you exactly where the problem is. You can not create hooks conditionally. This hook is a problem and needs to appear before return statement. This probably means you should change loading condition to work as expected.
useEffect(() => {
context.setLoading(false);
}, []);