Tried example of "react-native-smooth-picker#1.1.3" to implement smooth scroll when on scrolling found handleChange method throws error which is,
TypeError: refPicker.current.scrollToIndex is not a function. (In 'refPicker.current.scrollToIndex({animated: false,index: index,viewOffset: -30})','refPicker.current.scrollToIndex' is undefined).
Also when scrolling values not properly selecting and set in to useState by index. Please suggest what is the issue here and how to fix it.
Here is the smooth picker code,
...
import SmoothPicker from "react-native-smooth-picker";
const opacities = {
0: 1,
1: 1,
2: 0.6,
3: 0.3,
4: 0.1,
};
const sizeText = {
0: 20,
1: 15,
2: 10,
};
const ItemToRender = ({item, index}, indexSelected, vertical) => {
const selected = index === indexSelected;
const gap = Math.abs(index - indexSelected);
let opacity = opacities[gap];
if (gap > 3) {
opacity = opacities[4];
}
let fontSize = sizeText[gap];
if (gap > 1) {
fontSize = sizeText[2];
}
return ;
};
export default function SmoothPickerScreen({ navigation }) {
const [ selected, setSelected ] = React.useState(1);
const [value, setValue] = React.useState(null);
const [dataRows, setdataRows] = React.useState([]);
const data = [{name: 'A1',id:1},{name: 'A2',id:2}, {name: 'A2',id:3}, {name: 'A4',id:4}, {name: 'A5',id:5}, {name: 'A6',id:6}, {name: 'A7',id:7}, {name: 'A8',id:8}, {name: 'A9',id:9}, {name: 'A10',id:10}];
React.useEffect(() => {
(async () => {
setdataRows(data);
})();
}, []);
function handleChange(index) {
// console.log(index, refPicker);
setSelected(index);
refPicker.current.scrollToIndex({
animated: false,
index: index,
viewOffset: -30,
});
}
return (
{}}
keyExtractor={(_, index) => index.toString()}
showsVerticalScrollIndicator={false}
data={dataRows.map((o)=> {
// let obj;
return o.name
})}
// data={Array.from({ length: 16 }, (_, i) => i)}
scrollAnimation
onSelected={({ item, index }) => {
let find = dataRows.find(o => o.name === item)
// console.log('in scroll',item,index, find)
setValue(find.id)
handleChange(index)
}}
renderItem={option => ItemToRender(option, selected, true)}
magnet
/>
)
}
Related
Hej, I advanced my FlatList in React Native with
a) inbox/archive views
and b) with standard filter functionalities.
It's working somehow, but is not production ready!
Can someone please check this (I think well-organized) code and tell me where I do what wrong?
What is not working:
a) FlatList does not always re-render/update when the stream state, which is its data prop, changes
b) FlatList does not remove an item immediately when I archive/unarchive via swipe functionality. I have to manually change the view to see ...
c) FlatList does not directly apply the filter on state, I have to click twice to make it happen ...
//React
import { View, StyleSheet, Pressable, Animated, FlatList } from "react-native";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";
//Internal
import SelectBtn from "./SelectBtn";
import SessionComponent from "./SessionComponent";
import LoadingOverlay from "../notification/LoadingOverlay";
import { SessionsContext } from "../../store/context-reducer/sessionsContext";
//External
import { Ionicons } from "#expo/vector-icons";
import { useNavigation } from "#react-navigation/native";
import { database, auth } from "../../firebase";
import { ref, onValue, remove, update } from "firebase/database";
function SessionStream() {
const navigation = useNavigation();
const sessionsCtx = useContext(SessionsContext);
const currentSessions = sessionsCtx.sessions;
const [isFetching, setIsFetching] = useState(true);
const [stream, setStream] = useState([]);
const [inbox, setInbox] = useState([]);
const [archive, setArchive] = useState([]);
const [filter, setFilter] = useState([]);
const sessionList = ["Sessions", "Archive"];
const sortList = ["ABC", "CBA", "Latest Date", "Earliest Date"];
useEffect(() => {
//Fetches all sessions from the database
async function getSessions() {
setIsFetching(true);
const uid = auth.currentUser.uid;
const sessionsRef = ref(database, "users/" + uid + "/sessions/");
try {
onValue(sessionsRef, async (snapshot) => {
const response = await snapshot.val();
if (response !== null) {
const responseObj = Object.entries(response);
const sessionsData = responseObj.map((item) => {
return {
id: item[1].id,
title: item[1].title,
laps: item[1].laps,
endTime: item[1].endTime,
note: item[1].note,
identifier: item[1].identifier,
date: item[1].date,
smed: item[1].smed,
externalRatio: item[1].externalRatio,
internalRatio: item[1].internalRatio,
untrackedRatio: item[1].untrackedRatio,
archived: item[1].archived,
};
});
sessionsCtx.setSession(sessionsData);
setIsFetching(false);
} else {
sessionsCtx.setSession([]);
setIsFetching(false);
}
});
} catch (err) {
alert(err.message);
setIsFetching(false);
}
}
getSessions();
}, []);
useEffect(() => {
//Sorts sessions into archived and unarchived
setInbox(
currentSessions.filter((session) => {
return session.archived === false || session.archived === undefined;
})
);
setArchive(
currentSessions.filter((session) => {
return session.archived === true;
})
);
}, [currentSessions, archiveHandler, unArchiveHandler, sessionsCtx, stream]);
if (isFetching) {
setTimeout(() => {
return <LoadingOverlay />;
}, 5000);
}
const onPressHandler = useCallback(
//Callback to open the session
(item) => {
navigation.navigate("Detail", {
sessionID: item.id,
});
},
[onPressHandler]
);
const rightSwipeActions = useCallback(
//Swipe actions for the session list
(item, swipeAnimatedValue) => {
return (
<View
style={{
flexDirection: "row",
width: 168,
height: 132,
}}
>
{item.archived === false ? (
<Pressable
onPress={archiveHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.swipePressed}
>
<View style={styles.archive}>
<Animated.View
style={[
styles.archive,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons
name="ios-archive-outline"
size={24}
color="white"
/>
</Animated.View>
</View>
</Pressable>
) : (
<Pressable
onPress={unArchiveHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.swipePressed}
>
<View style={styles.unArchive}>
<Animated.View
style={[
styles.unArchive,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons
name="md-duplicate-outline"
size={24}
color="white"
/>
</Animated.View>
</View>
</Pressable>
)}
<Pressable
onPress={deleteHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.pressed}
>
<View style={styles.trash}>
<Animated.View
style={[
styles.trash,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons name="trash-outline" size={24} color="white" />
</Animated.View>
</View>
</Pressable>
</View>
);
},
[rightSwipeActions]
);
const deleteHandler = useCallback(
(item) => {
try {
sessionsCtx.deleteSession(item.id); // delete from local context
const uid = auth.currentUser.uid;
const sessionRef = ref(
database,
"users/" + uid + "/sessions/" + item.id
);
remove(sessionRef); // delete from firebase
} catch (error) {
alert(error.message);
}
},
[deleteHandler]
);
const archiveHandler = (item) => {
try {
const id = item.id;
const updatedSession = {
...item, // copy current session
archived: true,
};
const uid = auth.currentUser.uid;
const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
update(sessionRef, updatedSession);
/* sessionsCtx.updateSession(id, updatedSession); */
//update inbox state
setInbox(
currentSessions.filter((session) => {
const updatedData = session.archived === false;
return updatedData;
})
);
//update archive state
setArchive(
currentSessions.filter((session) => {
const updatedData = session.archived === true;
return updatedData;
})
);
} catch (error) {
alert(error.message);
}
};
const unArchiveHandler = (item) => {
try {
const id = item.id;
const updatedSession = {
...item, // copy current session
archived: false,
};
const uid = auth.currentUser.uid;
const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
update(sessionRef, updatedSession);
/* sessionsCtx.updateSession(id, updatedSession); */
//update unarchived session list
setArchive((preState) => {
//remove the item from archived list
preState.filter((session) => session.id !== item.id);
return [...preState];
});
} catch (error) {
alert(error.message);
}
};
const selectSessionHandler = useCallback(
(selectedItem) => {
switch (selectedItem) {
case "Sessions":
setStream(inbox);
break;
case "Archive":
setStream(archive);
break;
}
},
[selectSessionHandler, inbox, archive]
);
const selectFilterHandler = (selectedItem) => {
//filter the session list
switch (selectedItem) {
case "ABC":
// Use the Array.sort() method to sort the list alphabetically in ascending order
const sortedList = stream.sort((a, b) => {
return a.title.localeCompare(b.title);
});
setStream((preState) => {
return [...sortedList];
});
break;
case "CBA":
// Use the Array.sort() method to sort the list alphabetically in descending order
const sortedList2 = stream.sort((a, b) => {
return b.title.localeCompare(a.title);
});
setStream((preState) => {
return [...sortedList2];
});
break;
case "Latest Date":
// Use the Array.sort() method to sort the list by date in descending order
const sortedList3 = stream.sort((a, b) => {
return b.date.localeCompare(a.date);
});
setStream((preState) => {
return [...sortedList3];
});
break;
case "Earliest Date":
// Use the Array.sort() method to sort the list by date in ascending order
const sortedList4 = stream.sort((a, b) => {
return a.date.localeCompare(b.date);
});
setStream((preState) => {
return [...sortedList4];
});
break;
}
};
const renderSessionItem = useCallback(({ item }) => {
return (
<Pressable
/* style={({ pressed }) => pressed && styles.pressed} */
onPress={onPressHandler.bind(null, item)}
key={item.id}
>
<SessionComponent
key={item.id}
title={item.title}
identifier={item.identifier}
date={item.date}
rightSwipeActions={rightSwipeActions.bind(null, item)}
smed={item.smed}
endTime={item.endTime}
/>
</Pressable>
);
}, []);
return (
<View style={styles.container}>
<View style={styles.menuRow}>
<SelectBtn
data={sortList}
onSelect={(item) => selectFilterHandler(item)}
/>
<SelectBtn
data={sessionList}
onSelect={(item) => selectSessionHandler(item)}
/>
</View>
<FlatList
data={stream}
renderItem={renderSessionItem}
keyExtractor={(item) => item.id}
extraData={stream}
/>
</View>
);
}
export default SessionStream;
What I already tried:
I tried ChatGPT the whole day yesterday ... ;-)
I tried updating the global state for sessions to trigger re-renders ...
I tried updating the local state as obj or via the spread operator to ...
I tried extraData prop at FlatList
I removed useCallback to make sure it doesnt somehow block ...
can you do this when you are updating your stream state
const oldStream = stream;
const newStream = newStream; // put the value that you have updated
const returnedTarget= Object.assign(stream, newStream);
setStream(returnedTarget);
The problem might be you are mutating the copy obj
ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
c) in your selectFilterHandler method you are updating filter and in the same method you are trying to use updated filter value. which is not possible as useState does not update value immediately.
b) in your archiveHandler i think you are not updating the setStream state. if you are trying to run
const selectSessionHandler = useCallback(
(selectedItem) => {
switch (selectedItem) {
case "Sessions":
setStream(inbox);
break;
case "Archive":
setStream(archive);
break;
}
},
[selectSessionHandler, inbox, archive]
);
this method whenever inbox/archive changes it will not work it will be work only when you call this method and any of the value in dependency array has changed. (you can use useEffect and pass and deps arr [inbox,archive] which will run every time and can update your state.
So I have this basic todo:
App.jsx
const ITEMS = [
{ id: 1, text: 'Example 1' },
{ id: 2, text: 'Example 2' },
{ id: 3, text: 'Example 3' }
];
export const App = () => {
const todoRefs = useSharedValue<Record<string, any>>({});
const closeOpenTodos = (id: number) => {
'worklet';
for (const key in todoRefs.value) {
if (Number(key) !== id) {
todoRefs.value[key].closeTodo();
}
}
};
return (
<ScrollView contentContainerStyle={styles.container}>
{ITEMS.map((item, index) => (
<Item key={index} {...{ ...item, todoRefs, closeOpenTodos }} />
))}
</ScrollView>
);
};
Item.jsx (i.e. todo)
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const BUTTON_WIDTH = 100;
const CONTAINER_HEIGHT = 80;
const TRANSLATE_X_THRESHOLD = -SCREEN_WIDTH * 0.7;
type ItemProps = {
id: number;
text: string;
todoRefs: Record<string, any>;
closeOpenTodos: (id: number) => void;
};
type ItemContext = {
translateX: number;
};
export const Item = ({ id, text, todoRefs, closeOpenTodos }: ItemProps) => {
const translateX = useSharedValue(0);
const containerHeight = useSharedValue(CONTAINER_HEIGHT);
const dismissItem = () => {
'worklet';
containerHeight.value = withTiming(0, { duration: 100 });
translateX.value = -SCREEN_WIDTH;
};
todoRefs.value = {
...todoRefs.value,
[id]: {
closeTodo: () => {
'worklet';
translateX.value = withTiming(0);
}
}
};
const panGestureEvent = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
ItemContext
>({
onStart: (_event, context) => {
closeOpenTodos(id);
context.translateX = translateX.value;
},
onActive: (event, context) => {
// Prevent swiping to the right
if (event.translationX > 0) {
translateX.value = 0;
return;
}
translateX.value = event.translationX + context.translateX;
},
onEnd: (event, context) => {
// If swiping to the right, close item
if (event.translationX > 0) {
translateX.value = 0;
return;
}
if (event.translationX + context.translateX < TRANSLATE_X_THRESHOLD) {
dismissItem();
return;
}
translateX.value = withSpring(-BUTTON_WIDTH);
}
});
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }]
};
}, []);
const animatedContainerStyle = useAnimatedStyle(() => {
return {
height: containerHeight.value
};
}, []);
return (
<Animated.View style={[styles.container, animatedContainerStyle]}>
<Pressable style={[styles.button]} onPress={() => dismissItem()}>
<Text style={styles.buttonText}>Delete</Text>
</Pressable>
<PanGestureHandler onGestureEvent={panGestureEvent}>
<Animated.View style={[styles.slider, animatedSliderStyle]}>
<Text style={styles.sliderText}>{text}</Text>
</Animated.View>
</PanGestureHandler>
</Animated.View>
);
};
Basically when a todo is swiped open, it reveals a delete button. I want it so that if another todo it swiped open, all others close.
So I'm passing down a todoRefs (which technically aren't "refs") and closeOpenTodos.
All works fine, except this approach has introduced a strange bug.
When I go to delete items, the last one keeps re-appearing.
Is there a better way to do this?
How can I create a reusable React hook with animation style with Reanimated 2? I have an animation that is working on one element, but if I try to use the same animation on multiple elements on same screen only the first one registered is animating. It is too much animation code to duplicate it everywhere I need this animation, so how can I share this between multiple components on the same screen? And tips for making the animation simpler is also much appreciated.
import {useEffect} from 'react';
import {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
const usePulseAnimation = ({shouldAnimate}: {shouldAnimate: boolean}) => {
const titleOpacity = useSharedValue(1);
const isAnimating = useSharedValue(false);
useEffect(() => {
if (shouldAnimate && !isAnimating.value) {
isAnimating.value = true;
titleOpacity.value = withRepeat(
withSequence(
withTiming(0.2, {duration: 700, easing: Easing.inOut(Easing.ease)}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
if (!shouldAnimate) {
cancelAnimation(titleOpacity);
}
},
),
),
-1,
false,
() => {
if (titleOpacity.value < 1) {
titleOpacity.value = withSequence(
withTiming(0.2, {
duration: 700,
easing: Easing.inOut(Easing.ease),
}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
isAnimating.value = false;
},
),
);
} else {
titleOpacity.value = withTiming(
1,
{
duration: 700,
easing: Easing.inOut(Easing.ease),
},
() => {
isAnimating.value = false;
},
);
}
},
);
} else {
isAnimating.value = false;
cancelAnimation(titleOpacity);
}
}, [shouldAnimate, isAnimating, titleOpacity]);
const pulseAnimationStyle = useAnimatedStyle(() => {
return {
opacity: titleOpacity.value,
};
});
return {pulseAnimationStyle, isAnimating: isAnimating.value};
};
export default usePulseAnimation;
And I am using it like this inside a component:
const {pulseAnimationStyle} = usePulseAnimation({
shouldAnimate: true,
});
return (
<Animated.View
style={[
{backgroundColor: 'white', height: 100, width: 100},
pulseAnimationStyle,
]}
/>
);
The approach that I've taken is to write my Animations as wrapper components.
This way you can build up a library of these animation components and then simply wrap whatever needs to be animated.
e.g.
//Wrapper component type:
export type ShakeProps = {
// Animation:
children: React.ReactNode;
repeat?: boolean;
repeatEvery?: number;
}
// Wrapper component:
const Shake: FC<ShakeProps> = ({
children,
repeat = false,
repeatEvery = 5000,
}) => {
const shiftY = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => ({
//Animation properties...
}));
const shake = () => {
//Update shared values...
}
// Loop every X seconds:
const repeatAnimation = () => {
shake();
setTimeout(() => {
repeatAnimation();
}, repeatEvery);
}
// Start Animations on component Init:
useEffect(() => {
// Run animation continously:
if(repeat){
repeatAnimation();
}
// OR ~ call once:
else{
shake();
}
}, []);
return (
<Animated.View style={[animatedStyles]}>
{children}
</Animated.View>
)
}
export default Shake;
Wrapper Component Usage:
import Shake from "../../util/animated-components/shake";
const Screen: FC = () => {
return (
<Shake repeat={true} repeatEvery={5000}>
{/* Whatever needs to be animated!...e.g. */}
<Text>Hello World!</Text>
</Shake>
)
}
From their docs:
CAUTION
Animated styles cannot be shared between views.
To work around this you can generate multiple useAnimatedStyle in top-level loop (number of iterations must be static, see React's Rules of Hooks for more information).
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
I am trying to animate the changing of data in my Victory Native Pie Chart. I fetch the data from an API on a recurring timeout set up in the ComponentDidMount method. Once the data is retrieved I set it to a state variable which is passed to the data prop in the VictoryPie component with the animate props enabled as the docs show.
I am following this article and the Victory Chart docs for animations but mine does not behave in the same way as these examples.
Currently it sets the data correctly but without any smooth animation. It instantly jumps from initial state value to the fetched data value. Only time I see an animation is when the fetched data returns a zero value after the previous fetch had a value that wasn't zero.
export default class HaloXPChart extends Component {
constructor(props) {
super(props);
this.state = {
gamertag: this.props.gamertag ? this.props.gamertag : '',
dateRetrievalInterval: 1,
totalXp: 0,
startXp: 0,
spartanRank: 0,
xpChartData: [
{x: 'xp earned', y: 0},
{x: 'xp remaining', y: 1}
],
loading: false
};
}
componentDidMount() {
this.setState({loading: true});
this.recursiveGetData();
}
async recursiveGetData(){
this.setState({indicator: true}, async () => {
await this.getData()
this.setState({indicator: false});
let timeout = setTimeout(() => {
this.recursiveGetData();
}, this.state.dateRetrievalInterval * 60 * 1000);
});
}
async getData(){
await Promise.all([
fetch('https://www.haloapi.com/stats/h5/servicerecords/arena?players=' + this.state.gamertag, fetchInit),
fetch('https://www.haloapi.com/metadata/h5/metadata/spartan-ranks', fetchInit)
])
.then(([res1, res2]) => {
return Promise.all([res1, res2.json()])
}).then(([res1, res2) => {
const xp = res1.Results[0].Result.Xp;
const spartanRank = res1.Results[0].Result.SpartanRank;
this.setState({totalXp: xp});
const currentRank = res2.filter(r => r.id == this.state.spartanRank);
this.setState({startXp: currentRank[0].startXp});
this.setState({loading: false}, () => {
this.setState({xpChartData: [
{x: 'xp earned', y: this.state.totalXp - this.state.startXp},
{x: 'xp remaining', y: 15000000 - (this.state.totalXp - this.state.startXp)}
]});
});
})
.catch((error) => {
console.log(JSON.stringify(error));
})
}
render() {
return(
<View>
<Svg viewBox="0 0 400 400" >
<VictoryPie
standalone={false}
width={400} height={400}
data={this.state.xpChartData}
animate={{
easing: 'exp',
duration: 2000
}}
innerRadius={120} labelRadius={100}
style={{
labels: { display: 'none'},
data: {
fill: ({ datum }) =>
datum.x === 'xp earned' ? '#00f2fe' : 'black'
}
}}
/>
</Svg>
</View>
);
}
}
Try setting endAngle to 0 initially and set it to 360 when you load the data as described in this GitHub issue:
Note: Pie chart animates when it's data is changed. So set the data initially empty and set the actual data after a delay. If your data is coming from a service call it will do it already.
const PieChart = (props: Props) => {
const [data, setData] = useState<Data[]>([]);
const [endAngle, setEndAngle] = useState(0);
useEffect(() => {
setTimeout(() => {
setData(props.pieData);
setEndAngle(360);
}, 100);
}, []);
return (
<VictoryPie
animate={{
duration: 2000,
easing: "bounce"
}}
endAngle={endAngle}
colorScale={props.pieColors}
data={data}
height={height}
innerRadius={100}
labels={() => ""}
name={"pie"}
padding={0}
radius={({ datum, index }) => index === selectedIndex ? 130 : 120}
width={width}
/>
)
so i currently have a screen with a horizontal scroll view that contains my category tabs. When a tab is pressed, a flatlist is rendered below with all the posts specific for that category (this part is working correctly).
When the screen initially loads the first tab is selected by default and all the posts for that tab are rendered below.
Now, i have applied pagination in my backend and when i tested it on postman it is working.
My problem is:
eg. if i select tab 1, the first page (page=0) posts get rendered then when i scroll down, the page count increases to 2 but no posts get rendered. Also, then when i select tab 2, the page count doesnt reset back 0 it continues from where it left off in tab 1.
in tab 1 if i keep scrolling down to page 10, then i select tab 2.
What i want is the page count in tab 2 to start at 0.
what is happening the page count in tab 2 starts at 10.
Here is my code:
const getCategoryPostsAPI = (id,page)=>client.get(`/categories/${id}?page=${page}`)
function tabScreen({ navigation,route }) {
const [posts, setPosts] = useState([]);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const[page,setPage]=useState(0);
const loadPosts = async (id) => {
setLoading(true);
const response = await getCategoryPostsAPI(id,page) //API call
setLoading(false);
if(refreshing) setRefreshing(false);
if (!response.ok) return setError(true);
setError(false);
if(page===0){
setPosts(response.data)
}else{
setPosts([...posts,...response.data]);
}};
useEffect(() => {
loadPosts(1,page);
}, [page]);
const categories = [
{
label: "Sports",
id: 1,
},
{
label: "Fashion",
id: 2,
},
{
label: "News",
id: 3,
},
{
label: "Cooking",
id: 4,
},
{
label: "Education",
id: 5,
}]
const handleLoadMore = ()=>{
setPage(page+1);
}
const[label,setLabel]=useState('Sports')
const setLabelFilter=label=>{
setLabel(label)
}
const [currentCategoryId, setCurrentCategoryId] = useState()
const toggleBrands = (categoryId) => {
setCurrentCategoryId(categoryId)
setLabel(label)
};
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
>
{categories.map(e=>(
<TouchableOpacity
key={e.id}
onPress={()=>{toggleBrands(e.id),
setLabelFilter(e.label),
loadPosts(id=e.id),
setPage(0) // here i am trying to set the page to 0 on tab click
but it is not working
}}
selected={e.id === currentCategoryId}
>
<Text>{e.label}</Text>
</TouchableOpacity>
))}
</ScrollView>
<FlatList
data={currentCategoryId ? posts.filter(post=>post.categoryId===currentCategoryId
):posts.filter(post=>post.categoryId===1)}
keyExtractor={(post) => post.id.toString()}
renderItem={({ item,index }) => (
<Card
title={item.title}
subTitle={item.subTitle}
onPress={() => navigation.navigate(routes.POST_DETAILS, {post:item,index})}
/>
onEndReached={handleLoadMore}
onEndReachedThreshold={0.000001}
initialNumToRender={10}
)}
/>
I think i implemented my pagination wrong hence why my handleloadmore does not work because i have used the same handleloadmore in different screens and it is working.
I'm guessing you want the page to be reset to 0 when you click on another category and you want to paginate through the data when scrolling through a single category. If so, try this
const getCategoryPostsAPI = (id, page) => client.get(`/categories/${id}?page=${page}`);
const categories = [
{
label: "Sports",
id: 1,
},
{
label: "Fashion",
id: 2,
},
{
label: "News",
id: 3,
},
{
label: "Cooking",
id: 4,
},
{
label: "Education",
id: 5,
}
];
function tabScreen({ navigation, route }) {
const [posts, setPosts] = useState([]);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(0);
const [label, setLabel] = useState(categories[0]?.label);
const [currentCategoryId, setCurrentCategoryId] = useState(1);
const loadPosts = async (id, page = 0) => {
setLoading(true);
const response = await getCategoryPostsAPI(id, page)
setLoading(false);
if (refreshing) setRefreshing(false);
if (!response.ok) return setError(true);
setError(false);
if (page === 0) {
setPosts(response.data)
} else {
setPosts([...posts, ...response.data]);
}
};
useEffect(() => {
if (page > 0)
loadPosts(currentCategoryId, page);
}, [page]);
useEffect(() => {
setPage(0);
loadPosts(currentCategoryId, 0);
}, [currentCategoryId])
const handleLoadMore = () => {
setPage(page + 1);
};
const setLabelFilter = label => {
setLabel(label);
};
const toggleBrands = (categoryId) => {
setCurrentCategoryId(categoryId);
};
const postsToBeDisplayed = () => posts.filter(post => post.categoryId === currentCategoryId);
return (
<>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
>
{categories.map(e => (
<TouchableOpacity
key={e.id}
onPress={() => {
toggleBrands(e.id);
setLabelFilter(e.label);
}}
selected={e.id === currentCategoryId}
>
<Text>{e.label}</Text>
</TouchableOpacity>
))}
</ScrollView>
<FlatList
data={postsToBeDisplayed()}
renderItem={({ item, index }) => (
<Card
title={item.title}
subTitle={item.subTitle}
onPress={() => navigation.navigate(routes.POST_DETAILS, { post: item, index })}
/>
)}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.000001}
initialNumToRender={10}
keyExtractor={(post) => post.id.toString()}
/>
</>
)
};
However, if you want to implement the tab feature, I would recommend using react-native-tab-view. This will make the whole process a lot easier.