In my project I have a (horizontal) FlatList that I want to trigger a function when a new page is visible. I've understood that I need to specify viewabilityConfig and onViewableItemsChanged. However, when I do I keep getting this error msg: "Changing onViewableItemsChanged on the fly is not supported".
I'm a newbie, so looking at the other posts about the same error didnt make me any smarter.
Any ideas?
const DATA = [
20201202 = {
id: "20201202",
data: day1,
},
2020120 = {
id: "2020120",
data: day2,
},
20201204 = {
id: "20201204",
data: day3,
},
]; */
const LiveScreen = (props, { navigation }) => {
const FlatListItem = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title.day}</Text>
</View>
);
// FLATLIST
const renderFlatList = ({ item }) => <FlatListItem title={item} />;
// VIEWABILITY
const viewabilityConfig = {
waitForInteraction: true,
// At least one of the viewAreaCoveragePercentThreshold or itemVisiblePercentThreshold is required.
viewAreaCoveragePercentThreshold: 95,
itemVisiblePercentThreshold: 75,
};
return (
<View style={{ flex: 1, width: "100%", backgroundColor: "#F4F5F5" }}>
<View style={{ flex: 1 }}>
<SafeAreaView>
<FlatList
horizontal
pagingEnabled
data={DATA}
renderItem={renderFlatList}
keyExtractor={(item) => item.id}
initialScrollIndex={5}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={(viewableItems, changed) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
}}
/>
</SafeAreaView>
</View>
</View>
);
};
const styles = StyleSheet.create({});
export default LiveScreen;
I have removed the excess code..
Thanks in advance
viewabilityConfig needs to be done in the constructor to avoid this error.
constructor (props) {
super(props)
this.viewabilityConfig = {
waitForInteraction: true,
// At least one of the viewAreaCoveragePercentThreshold or itemVisiblePercentThreshold is required.
viewAreaCoveragePercentThreshold: 95,
itemVisiblePercentThreshold: 75,
}
}
<FlatList
viewabilityConfig={this.viewabilityConfig}
...
/>
For functional components, it needs to be done with Ref hook.
const viewabilityConfig = useRef({
waitForInteraction: true,
// At least one of the viewAreaCoveragePercentThreshold or itemVisiblePercentThreshold is required.
viewAreaCoveragePercentThreshold: 95,
itemVisiblePercentThreshold: 75,
})
Late to the party, but the issue isn't the viewabilityConfig, it's the onViewableItemsChanged.
In a function component, you need to separate out the handler for onViewableItemsChanged and place it in a ref like so:
const handleViewableItemsChanged = useRef(({viewableItems, changed})=>{
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed)
});
<FlatList
...
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={handleViewableItemsChanged.current}
/>
The same effect can be done with a useCallback hook with an empty dependency array, but this way is a bit more clean.
Related
one place I seem to be stuck is on is being able to populate an array of objects, which are used for a FlatList later on.
I get this data from my FireStore – for each document, it will push the objects into ‘const DATA = []’
But when I run the ‘getUsers()’ inside of UseEffect, it only updates ‘DATA’ inside of the method, it doesn’t update the global variable.
Im new to react native, so im probably missing something important within my structure. Thanks for any assistance tho!
I need the format of DATA to look like this example:
My Code:
const MainScreen = () => {
const DATA = [];
const navigation = useNavigation()
const [selectedId, setSelectedId] = useState(null);
const usersCollectionRef = collection(db, "Posts");
const [Data, setData]= useState([]);
LogBox.ignoreLogs(['Setting a timer']);
LogBox.ignoreLogs(['AsyncStorage has been extracted']);
LogBox.ignoreAllLogs(true);
useEffect(() => {
getUsers();
console.log(DATA);
}, []);
const getUsers = async () => {
const data = await getDocs(usersCollectionRef);
data.forEach(doc =>{
const dataG = (doc.id, "=>", doc.data());
DATA.push({
id: doc.id,
title: dataG.status +" "+ dataG.Type,
status: dataG.status,
location: dataG.Location,
type: dataG.Type,
injured: dataG.Injured,
collar: dataG.Collar,
color: dataG.Colour,
username: dataG.username,
description: dataG.Description,
imagesrc: dataG.picture });
})
};
const Item = ({ item, onPress, backgroundColor, textColor }) => (
<View style={styles.ContentBox}>
<TouchableOpacity onPress={onPress} style={[styles.item, backgroundColor]}>
<Text style={[styles.title, textColor]}>{item.title}</Text>
<Text style={styles.ContentText}>By: {item.username}</Text>
</TouchableOpacity>
<Image source = {{uri: item.imagesrc}}
style = {{ width: 200, height: 200, alignSelf:'center' }}/>
<Text style={styles.ContentText}>Animal: {item.type}</Text>
<Text style={styles.ContentText}>Location: {item.location}</Text>
<Text style={styles.ContentText}>Injured: {item.injured}</Text>
<Text style={styles.ContentText}>Colour: {item.color}</Text>
<Text style={styles.ContentText}>Has a Collar: {item.collar}</Text>
<Text style={styles.ContentText}>Description: {item.description}</Text>
</View>
);
const renderItem = ({ item }) => {
const backgroundColor = item.status === "lost" ? '#b80909' : '#098231';
const color = item.id === selectedId ? 'white' : 'white';
return (
<Item
item={item}
onPress={() => setSelectedId(item.id)}
backgroundColor={{ backgroundColor }}
textColor={{ color }}
/>
);
};
const PostScreen = () =>{
navigation.navigate('PostScreen');
}
return (
<SafeAreaView style={styles.container}>
<View style={styles.MainFeed}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
extraData={selectedId}
/>
</View>
)
instead of pushing data in a variable and then updating the state, you can do it like this directly -
setData([...Data,{
id: doc.id,
title: dataG.status +" "+ dataG.Type,
status: dataG.status,
location: dataG.Location,
type: dataG.Type,
injured: dataG.Injured,
collar: dataG.Collar,
color: dataG.Colour,
username: dataG.username,
description: dataG.Description,
imagesrc: dataG.picture
}])
The answer is that after doing DATA.push(...), I needed to setData(DATA).
I have some buttons in my flatlist like below
const renderItem = ({ item }) => <Item name={item.name} slug={item.slug} />;
const Item = ({ name, slug }) => {
return (
<TouchableOpacity
delayPressIn={0}
onPress={() => {
dispatch(setLanguage(slug));
}}
>
<View
style={[
styles.item,
{ backgroundColor: languages == slug ? "#940062" : "black" },
]}
>
<Text style={styles.title}>{name}</Text>
</View>
</TouchableOpacity>
);
};
return (
<SafeAreaView style={styles.container}>
<FlatList
horizontal={true}
data={jsonLang}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
/>
</SafeAreaView>
);
};
The above code works fine, when I click it is changing the background? But background color change is delayed by 1 second. Is this the right approach to change the background color of the button?
Thank you.
P.S: the setlanguage is my reducer in my redux
setLanguage: (state, action) => {
state.language = action.payload;
},
It works pretty fast, here's an example I created.
Perhaps the delay is in the reducer. So I artificially slowed it down.
const setLanguage = (languages) => {
return (dispatch) => {
setTimeout(()=>{
dispatch({
type: "setLanguage",
value: languages
});
}, 1000) // <--------------------- delay
};
}
but now we need to make the style apply faster. I added another field to next_languages state:
const initialState = {
languages: "1",
next_languages: "1"
}
and modified the code like this:
const setLanguage = (languages) => {
return (dispatch) => {
dispatch({ // <-------------- for apply style quiqly
type: "setNextLanguage",
value: languages
});
setTimeout(()=>{
dispatch({
type: "setLanguage",
value: languages
});
}, 1000)
};
}
also added the costant to the component:
const fastLang = languages === next_languages
? languages
: next_languages;
Finally, the styles are set like that and still quickly
{ backgroundColor: fastLang == slug ? "#940062" : "yellow" }
I think you can even get by with one variable (perhaps this contradicts the logic of work), but this is already refactoring.
I hope I could help.
the 1-second delay not depending on background color changing time, but it depends on what setLanguage do?
if setLanguage change the app Language this means the all component that uses this Selector will re-render
you can split background color in separator state, this will change background fast, but change language it still takes 1-second
solution with react state just for explanation (you can use Redux)
//add this line
const [selectedLanguage, setSelectedLanguage] = react.useState(languages);
const renderItem = ({ item }) => <Item name={item.name} slug={item.slug} />;
const Item = ({ name, slug }) => {
return (
<TouchableOpacity
delayPressIn={0}
onPress={() => {
setSelectedLanguage(slug); //add this line, will update color immediately
dispatch(setLanguage(slug));
}}
>
<View
style={[
styles.item,
//use selectedLanguage here
{ backgroundColor: selectedLanguage === slug ? "#940062" : "black" },
]}
>
<Text style={styles.title}>{name}</Text>
</View>
</TouchableOpacity>
);
};
};
I am attempting to use react-native-slider with Expo AV to create a seekbar, but am having trouble updating the 'value' state of slider. When I try to set it to currentPosition/durationPosition, it errors out, likely because initially these values are NaN. I CAN display current/duration however.
My best guess is that I need a way to wait until my mp3 is loaded before rendering the SeekBar. I probably also need to do a better job of separating components and keep PlayerScreen very minimal. I've messed around with this code so much I can barely remember what I've tried... Getting close to ditching Expo because react-native-track-player looks easier to work with and I've heard some bad things about Expo. Anyways, here's where I'm at now
export default class PlayerScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
isPlaying: false,
playbackObject: null,
volume: 1.0,
isBuffering: false,
paused: true,
currentIndex: 0,
durationMillis: 1,
positionMillis:0,
sliderValue:0,
isSeeking:false,
}
}
async componentDidMount() {
try {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
shouldDuckAndroid: true,
staysActiveInBackground: true,
playThroughEarpieceAndroid: true
})
this.loadAudio()
} catch (e) {
console.log(e)
}
}
async loadAudio() {
const { currentIndex, isPlaying, volume} = this.state
try {
const playbackObject = new Audio.Sound()
const source = {
uri: this.props.route.params.item.uri
}
const status = {
shouldPlay: isPlaying,
volume,
}
playbackObject.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate)
await playbackObject.loadAsync(source, status, true)
this.setState({playbackObject})
var sliderValue = this.state.positionMillis/this.state.durationMillis
} catch (e) {
console.log(e)
}
}
handlePlayPause = async () => {
const { isPlaying, playbackObject } = this.state
isPlaying ? await playbackObject.pauseAsync() : await playbackObject.playAsync()
this.setState({
isPlaying: !isPlaying
})
}
onPlaybackStatusUpdate = status => {
this.setState({
isBuffering: status.isBuffering,
durationMillis: status.durationMillis,
positionMillis: status.positionMillis,
})
}
render() {
const { item } = this.props.route.params;
return (
<View style={globalStyles.container}>
<Header />
<View style={globalStyles.subHeader}>
<Text style={globalStyles.title}>{ item.title }</Text>
</View>
<View style={styles.text}>
<Text>{ item.text }</Text>
</View>
<SeekBar
durationMillis={this.state.durationMillis}
positionMillis={this.state.positionMillis}
sliderValue={this.state.sliderValue}
/>
And here's the SeekBar component:
const SeekBar = ({
positionMillis,
durationMillis,
sliderValue
}) => {
return (
<View style={styles.container}>
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1 }} />
<Text style={[styles.text, { width: 40 }]}>
{positionMillis + ' / ' + durationMillis}
</Text>
</View>
<Slider
minimumValue={0}
maximumValue={1}
value={sliderValue}
style={styles.slider}
minimumTrackTintColor='#fff'
maximumTrackTintColor='rgba(255, 255, 255, 0.14)'
/>
</View>
);
};
export default SeekBar;
put
<SeekBar
durationMillis={this.state.durationMillis}
positionMillis={this.state.positionMillis}
sliderValue={this.state.sliderValue}
/>
in the screen component and
const SeekBar = ({
positionMillis,
durationMillis,
sliderValue
}) => {
sliderValue = positionMillis/durationMillis
return (
<View style={styles.container}>
<View style={{ flexDirection: 'row' }}>
<View style={{ flex: 1 }} />
in the SeekBar component
I'm new to react native, so please be kind! I am trying to populate a Flatlist using a JSON.
Below is my JSON data
{
"h1":{
"baseprice":899,
"description":"Upto Waist length Hair",
"imageUrl":"https://i.imgur.com/0IgYzAv.jpg'",
"price":799,
"time":"25 min",
"title":"Nourishing Hair Spa",
"type":"Service"
},
"h2":{
"baseprice":899,
"description":"Touch Up of length less than 4 inches",
"imageUrl":"https://i.imgur.com/q7ts4PZ.jpg",
"price":799,
"time":"45 min",
"title":"INOA Root Touch Up",
"type":"Service"
}
}
Here is the code that I used to push the JSON data in to my Flatlist
export default class Office extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
dataSource: [],
};
}
componentDidMount() {
return fetch("https://stylmate1.firebaseio.com/hair.json")
.then((response) => response.json())
.then((responseJson) => {
this.setState({
isLoading: false,
dataSource: responseJson,
});
console.log(dataSource);
})
.catch((error) => {
console.error(error);
});
}
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<ActivityIndicator />
</View>
);
}
return (
<View style={styles.container}>
<FlatList
data={this.state.dataSource}
renderItem={(item) => <Text>{item.title}</Text>}
/>
</View>
);
}
}
As soon as I refresh the App I get an error
Can't find variable : dataSource
But if I console.log(responseJson); then I get the complete JSON object.
I don't know what am I doing wrong here. Please help me in fixing this.
Your FlatList is supposed to look like this:
return (
<View style={styles.container}>
<FlatList
data={this.state.dataSource}
renderItem={({item}) => <Text>{item[1].title}</Text>}
/>
</View>
);
You have to destructure the item in order to use it.
Output:
Working Example code:
import React, { useState, useEffect } from 'react';
import { Text, View, FlatList, StyleSheet } from 'react-native';
import Constants from 'expo-constants';
import { Card } from 'react-native-paper';
export default function App() {
const [dataSource, setData] = useState([]);
useEffect(() => {
fetch('https://stylmate1.firebaseio.com/hair.json')
.then((response) => response.json())
.then((responseJson) => {
setData(responseJson);
console.log(responseJson);
})
.catch((error) => {
console.error(error);
});
}, []);
return (
<View style={styles.container}>
{dataSource ? (
<FlatList
data={Object.entries(dataSource)}
renderItem={({ item }) => (
<View style={{ padding: 10 }}>
<Card>
<Text style={styles.paragraph}>{item[1].title}</Text>
</Card>
</View>
)}
/>
) : null}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
Expo Snack Live Demo
The Error happens because you are trying to log dataSource which is wrong it should be this.state.dataSource.
console.log(dataSource);
change it to
console.log(this.state.dataSource);
in your .then() block
firstly,
console.log(dataSource);
should change to
console.log(this.state.dataSource);
However, you may not get the correct console.log since set state is async. You get better and accurate answer with responseJson
Secondly, flatList requires an array of objects structure, you need to convert them using the following methods. I moved your "h1", and "h2" into the object and label them as key.
const original = {
"h1":{
"baseprice":899,
"description":"Upto Waist length Hair",
"imageUrl":"https://i.imgur.com/0IgYzAv.jpg'",
"price":799,
"time":"25 min",
"title":"Nourishing Hair Spa",
"type":"Service"
},
"h2":{
"baseprice":899,
"description":"Touch Up of length less than 4 inches",
"imageUrl":"https://i.imgur.com/q7ts4PZ.jpg",
"price":799,
"time":"45 min",
"title":"INOA Root Touch Up",
"type":"Service"
}
}
const newArray = Object.keys(original).map( key => ({...original[key], key }))
console.log(newArray)
So I'm doing something fairly simple in with react-native. I have a function that creates a copy of the state which is initially an array, I then update the copy and then I would eventually call my setState in order to update the original array. However, when I mutate the copy, for some odd reason it also mutates the array from the state even if I don't call setState. I tried everything that I thought might fix it, using slice, [...copy], splicing it, nothing, it still mutates, and why, I just don't understand why it mutates? If anyone can help me that would be really appreciated.
Here is the function
function completeTaskHandler(goalName, taskId, taskIndex, updateAction) {
const taskSnapShot = tasks.slice();
console.log(taskSnapShot);//logs the copy
console.log(tasks);// logs the original array
taskSnapShot[taskIndex].isComplete = "true";
console.log(taskSnapShot);//results in mutated array
console.log(tasks);//Also results in mutated array why??
};
Here is the full Code block
import React, { useState } from "react";
import { View, StyleSheet, TouchableOpacity } from "react-native";
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import { useSelector, useDispatch } from "react-redux";
import { Ionicons } from "#expo/vector-icons";
//Custom Components
import Task from "../../components/local/goals/EditGoalTask";
//Header Custom Component
import CustomBackButton from "../../components/HeaderButtonDark";
//Controllers
import { DefaultText, SmallText, HeaderText, SmallTextItalic } from "../../controllers/TextController";
//Constants
import Colors from "../../constants/Colors";
//Redux reducers
import { udpateTask } from "../../store/actions/user";
const EditGoal = ({ navigation, route }) => {
//Initialize variables
const dispatch = useDispatch();
const { goalId, goalNameFromAddPage } = route.params;
let selectedGoal = useSelector(state => state.userReducer.goals.find((goal) => goal.id === goalId));
if (goalNameFromAddPage) {
selectedGoal = useSelector(state => state.userReducer.goals.find((goal) => goal.goalName === goalNameFromAddPage));
}
//Deconstruct needed variables
const { goalName, startDate, status, tasksArrayOfObjects } = selectedGoal;
//Initialize States
const [tasks, setTasks]= useState(tasksArrayOfObjects.fitler((task) => {
if (!task.isComplete) {
return true;
} else {
return false;
}
}));
//Methods
function deleteTaskHandler(goalName, taskId, taskIndex, updateAction) {
const taskSnapShot = [...tasks];
taskSnapShot.splice(taskIndex, 1);
setTasks(taskSnapShot);
//dispatch(udpateTask(goalName, taskId, updateAction));
};
/* Has issues */
function completeTaskHandler(goalName, taskId, taskIndex, updateAction) {
const taskSnapShot = tasks.slice();
console.log(taskSnapShot);
console.log(tasks);
taskSnapShot[taskIndex].isComplete = "true";
console.log(taskSnapShot);
console.log(tasks);
/*
function copyArray(arr) {
const arrCopy = arr.slice();
const copy = arrCopy.splice(0, arrCopy.length);
return copy;
}
console.log(tasks);
const taskSnapShot = copyArray(tasks);
taskSnapShot[taskIndex].isComplete = true;
console.log(taskSnapShot);
console.log(tasks);
*/
//dispatch(udpateTask(goalName, taskId, updateAction));
};
navigation.setOptions({
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={CustomBackButton}>
<Item
title="BACK"
iconName="md-close"
onPress={() => navigation.popToTop()}
/>
</HeaderButtons>
),
});
return(
<View style={styles.screen}>
<View style={styles.header}>
<View style={styles.goalContainer}>
<SmallText>Goal:</SmallText>
<HeaderText>{goalName}</HeaderText>
</View>
<View style={styles.goalStatusContainer}>
<SmallText style={styles.headerTextMargin}>Started: {startDate}</SmallText>
<View style={{ flexDirection: "row" }}>
<SmallText>Finished: </SmallText>
<SmallTextItalic>{status}</SmallTextItalic>
</View>
</View>
</View>
<View style={styles.pageDescription}>
<DefaultText>
Here is where you can add, delete, or track the steps you need to do in
order to achieve your goal.
</DefaultText>
</View>
<View style={styles.taskContainer}>
{tasks.map((task, index) => {
return <Task
title={task.taskName}
key={"key"+index}
deleteTask={deleteTaskHandler.bind(this, goalName, task.id, index, "delete")}
completeTask={completeTaskHandler.bind(this, goalName, task.id, index, "complete")}
/>
})}
<View style={styles.touchableContainer}>
<TouchableOpacity onPress={() => alert()}>
<View style={{...styles.task, ...styles.addAStepContainer}}>
<View style={styles.addAStep}>
<Ionicons style={{ marginRight: 5 }} name="ios-add" size={23} color={Colors.grey} />
<DefaultText style={{ color: Colors.grey, fontSize: 18 }}>Add a step</DefaultText>
</View>
</View>
</TouchableOpacity>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
screen: {
paddingHorizontal: 10,
},
header: {
flexDirection: "row",
height: 80,
//alignItems: 'center',
},
pageDescription: {
paddingVertical: 10,
},
goalContainer: {
flex: 1,
},
goalStatusContainer: {
flex: 1,
alignItems: "flex-end",
},
headerTextMargin: {
marginBottom: 4,
},
touchableContainer: {
borderRadius: 10,
overflow: "hidden",
},
taskContainer: {
//borderWidth: 1,
},
addAStepContainer: {
paddingVertical: 20,
},
addAStep: {
flexDirection: "row",
},
});
export default EditGoal;
This is a little bit tricky. When you do:
const taskSnapShot = tasks.slice();
you create a new array, but inside that array, you won't duplicate the objects from tasks but just make a reference to these objects. So basically, taskSnapShot is an array of references. So when you modify an object inside taskSnapShot, you also modify it in tasks.
To solve this problem you have to duplicate the object that you want to modify:
const taskSnapShot = [...tasks];
taskSnapShot[taskIndex] = {...taskSnapShot[taskIndex], isComplete: true};