React Native: Why I can not pass a reference to another screen? - reactjs

I have 3 screens and I can navigate between them. The first one is "Reiew Screen" where the user can put a raiting, write a title and description of their review. The second one (most problematic for me) is the media screen where the user can select multiple photos to support their review. The third screen is "Review screen" where the user can again double check and edit their review (if they need to).
On the review screen I see the photos the user has selected and if I want I can remove some of them. If I want, I also can edit the title and the description of the review.
Editing the photos and keeping the edit consistent with the "Media Screen" is done using refs. I am using ImageBrowser from here so that I can keep consistent state between MediaScreen and ReviewScreen when the user deletes the photos. The problem is when I want to edit the title and the description. If I want to edit title.description I need to naigate from Reiew page to Rate page (because Rate page contains the title and the description). When I press "done" I need to navigate back to Review page where I see the updated title and the updated description as well as the photos which I have selected on the previous steps. When I navigate from Rate page to Review page I have error that the reference of ImageBrowser I have created is null. I tried passing this reference between the screens, as a property but for some reason it seems I am passing undefined.
I am thinking maybe the refs object which I have created have life span and it is expired?
I am posting the pages and how I navigate between them:
Rate Screen
const RateScreen = (props) => {
const { place } = props.route.params;
//imageBrowser is the object which I try to retrieve from the Review page, but it comes as undefined
const { finalObject, imageBrowser } = props.route.params;
console.log("RATE SCREEN IS BEING RERENDERED: ", props.route.params);
const [rating, setRating] = useState(0);
const [title, setTitle] = useState(null);
const [description, setDescription] = useState(null);
// again trying to retrieve the refs
const [gallery, setGallery] = useState(props.route.params.imageBrowser);
function ratingCompleted(value) {
// not relevant
}
function updateTitle(value) {
//not relevant
}
function updateDescription(value) {
//not relevant
}
useEffect(() => {
// way to fetch some value for imageBrowser but it is still null
console.log("useeffect for image browser is being rendered");
if (imageBrowser) {
setGallery(imageBrowser);
}
}, [imageBrowser]);
function navigate() {
if (finalObject) {
//in the case I have edited the object I navigate back to Review Screen
props.navigation.navigate("Review Screen", {
finalObject: {
...finalObject,
rating: rating,
title: title,
description: description,
},
// it comes as null so my code throws an exception
imageBrowser: gallery,
});
} else {
// In the case I have just created a review I am just navigating to the next step which is Add Media screen
props.navigation.navigate("Add Media", {
rateObject: { ...rateObject, place: place },
});
}
}
useLayoutEffect(() => {
props.navigation.setOptions({
headerRight: () => navigate()
),
});
}, [rating, title, description]);
return (
<View>
<TextInput
value={title}
onChangeText={(value) => updateTitle(value)}
/>
<TextInput
value={description}
onChangeText={(value) => updateDescription(value)}
/>
</View>
);
};
export default RateScreen;
Part of my Add Media Screen where I create the refs, and I pass it to the Review Page:
const GalleryScreen = (props) => {
const { selected, setPhotos, setImageBrowser } = props.route.params;
const [newSelected, setNewSelected] = useState(selected);
//using refs
const imageBrowserRef = useRef(null);
useEffect(() => {
setNewSelected(selected);
}, []);
useEffect(() => {
if (imageBrowserRef) {
setImageBrowser(imageBrowserRef);
}
}, [imageBrowserRef]);
const renderSelectedComponent = (number) => {
return (
//not related
);
};
return (
<View style={styles.viewStyle}>
<ImageBrowser
// creating the ref
ref={imageBrowserRef}
max={10}
callback={(promise) => {
//not related
}}
renderSelectedComponent={renderSelectedComponent}
/>
</View>
);
};
useLayoutEffect(() => {
props.navigation.setOptions({
headerRight: () => (
<TouchableOpacity
onPress={() => {
props.navigation.navigate("Review Screen", {
finalObject: {
...rateObject,
photos: photos,
},
// passing the refs, which for this step works fine
imageBrowser: imageBrowserRef,
});
}}
>
<AntDesign name="check" size={24} color="white" />
</TouchableOpacity>
</View>
),
});
Finally my "Review Page":
import {
renderTitle,
renderRating,
renderDescription,
renderMedia,
} from "./renderItems";
const ReviewScreen = (props) => {
//at this step imageBrowser is not null and my interactions with the browser are as expected
const { finalObject, imageBrowser } = props.route.params;
const { navigation } = props;
const [editedObject, setEditedObject] = useState(finalObject);
const [rating, setRating] = useState(finalObject.rating);
function ratingCompleted(value) {
//not relevant
}
function removePhoto(item) {
//again as I expect, the removed photo is removed from BOTH screens (Media screen and Review screen)
let index = imageBrowser.current.state.photos.indexOf(item);
let updatedPhotos = editedObject.photos.filter(function (photo) {
return photo != item;
});
imageBrowser.current.selectImage(index);
setEditedObject({
...editedObject,
photos: updatedPhotos,
});
}
useLayoutEffect(() => {
//doesnt matter
),
headerLeft: (props) => (
<HeaderBackButton
{...props}
onPress={() => {
// if I go back to Add Media screen, thanks to refs both screens are consistent
navigation.navigate("Add Media", {
rateObject: editedObject,
});
}}
/>
),
});
}, [editedObject]);
return (
<View>
{renderRating(editedObject, ratingCompleted)}
// with render title I am trying to pass imageBrowser refs as a property but it doesnt work
{renderTitle(
{ ...editedObject, rating: rating },
imageBrowser,
props.navigation
)}
//similar with render description
{renderDescription(
{ ...editedObject, rating: rating },
imageBrowser,
props.navigation
)}
//render media does not navigate to anywhere, it simply displays the photos
{renderMedia(editedObject, removePhoto)}
</View>
);
};
My renderTitle mthod:
export const renderTitle = (finalObject, imageBrowser, navigation) => {
return (
<SafeAreaView style={{ flexDirection: "row", height: 60 }}>
<ScrollView style={{ marginHorizontal: 10, marginTop: 10 }}>
<Text style={styles.titleStyle}>{finalObject.title}</Text>
</ScrollView>
<TouchableOpacity
onPress={() => {
make sure imageBrowser is not null and it is not
console.log("trying to edit title: ", imageBrowser.current.props);
//again on this page I see that imageBrowser is not null, but passing it to the "Rate page" somehow magically it is null
navigation.navigate("Rate Your Visit", {
finalObject: finalObject,
imageBrowser: imageBrowser,
});
}}
>
<MaterialIcons name="edit" size={24} color="#0696d4" />
</TouchableOpacity>
</SafeAreaView>
);
};
Any ideas are welcome

Related

How do I access the data from a referenced firestore document?

My data in firestore is structured as shown in the pictures. In my code, I am using a map of the exercises array within my workout document like this:
{workout.exercises.map((exercise) => {
return (
<Text>Want exercise name here</Text>
)
I believe I have to use doc.data() from getDoc(exercise).then((doc) => to get the actual values, but I can't seem to figure out how to actually render the exercise name. It's driving me crazy, any help would be greatly appreciated!
This is the exercise object referenced in the exercises array from the picture above:
EDIT: If the exercises array only has one element, it works properly, but if it has >1, the data seems to keep rendering infinitely which results in flashing and changing text.
EDIT2: Here's my full component code in case that helps:
export function WorkoutDisplayScreen({ navigation }) {
const [workouts, setWorkouts] = React.useState([]);
const workoutsArray = [];
const [isFirstLoad, setIsFirstLoad] = useState(true);
const [exerciseData, setExerciseData] = useState({});
async function getData(exercise) {
await getDoc(exercise).then((doc) => {
setExerciseData(doc.data());
});
}
const getWorkouts = async () => {
console.log('Firebase READ & WRITE');
const workoutsCollection = collection(db, 'Users', auth.currentUser.uid, 'Workouts');
const workoutsDocs = await getDocs(workoutsCollection);
workoutsDocs.forEach((workout) => {
workoutsArray.push(workout.data());
});
setWorkouts(workoutsArray);
};
useEffect(() => {
console.log('Rendering...');
if (isFirstLoad) {
getWorkouts();
}
setIsFirstLoad(false);
}, []);
return (
<ScreenContainer>
<View style={styles.headerrow}>
<TouchableOpacity
onPress={() => {
navigation.goBack();
}}
>
<AntDesign name="leftcircle" size={24} color="black" style={styles.headericon} />
</TouchableOpacity>
<Text style={styles.header}>My Workouts</Text>
</View>
{workouts &&
workouts.map((workout) => {
return (
<View key={workout.id}>
<View style={styles.workoutnamecontainer}>
<Text style={styles.workoutnametext}>{workout.name}</Text>
</View>
{workout.exercises.map((exercise, index) => {
getData(exercise);
return (
<View style={styles.exercisecontainer} key={index}>
<Text style={styles.exercisetext}>{exerciseData.name}</Text>
<Text style={styles.exercisetext}>{exerciseData.type}</Text>
<Text style={styles.exercisetext}>{exerciseData.weight}</Text>
<Text style={styles.exercisetext}>{exerciseData.reps}</Text>
</View>
);
})}
</View>
);
})}
</ScreenContainer>
);
}
EDIT3:
I can now log the data in the correct format with, but trying to render it on screen is still not working correctly.
const getWorkouts = async () => {
console.log('Firebase READ & WRITE');
const workoutsCollection = collection(db, 'Users', auth.currentUser.uid, 'Workouts');
const workoutsSnapshot = await getDocs(workoutsCollection);
let workoutsDocs = workoutsSnapshot.docs;
workoutsDocs.forEach((workout) => {
workoutsArray.push(workout.data());
});
setWorkouts(workoutsArray);
workouts.map((workout) => {
workout.exercises.forEach((exercise) => {
getDoc(exercise).then((doc) => {
console.log(workout.name + ':');
console.log(doc.data());
});
});
});
};
I believe you should do all the data-fetching i.e. for all the workouts and every exercise in each workout in the useEffect itself. I have never seen code where data fetching is done inside the JSX part of the function. I am highly skeptical about it.
Also, one thing I noticed while looking through your code is that you are iterating over the snapshot instead of docs in the the snapshot inside your getWorkouts() function. It should be as follows:
const getWorkouts = async () => {
console.log('Firebase READ & WRITE');
const workoutsCollection = collection(db, 'Users', auth.currentUser.uid, 'Workouts');
const workoutsSnapshot = await getDocs(workoutsCollection);
workoutsDocs = workoutsSnapshot.docs; // This line must be added to your code
workoutsDocs.forEach((workout) => {
workoutsArray.push(workout.data());
});
setWorkouts(workoutsArray);
};

Real-Time graph not showing in React

I want to plot my data in React-Native. I am receiving data from BLE. But I can't see the real time graph on the screen. What could be the reason? My brain stopped, so I couldn't figure out what the problem was.
Note : My data is,
hr
This is BLE characteristic value.
This is my component.js,
const width = Dimensions.get('window').width;
const height = Math.floor((Dimensions.get('window').height - 150) / 3);
let counter = 0;
const slotsPerWidth = 100;
const initialState = {
flow: hr,
isLoading: false,
};
class ChartScreen extends Component {
state = {
chartData: { ...initialState },
};
static getDerivedStateFromProps(state) {
counter++;
return {
chartData: {
flow: [hr],
},
};
}
render() {
const { flow} = this.state.chartData;
return this.state.isLoading
? <ScreenContainer>
<View style={styles.main}>
<View>
<ActivityIndicator></ActivityIndicator>
</View>
<Chart
key="flow"
data={flow}
maxValue={1900}
minValue={1750}
slotsPerWidth={slotsPerWidth}
width={width}
height={height}
marginBottom={20}
lineColor="rgba(95, 92, 1, 1)"
lineThickness={2}
chartBackground="#17204d"
horizontalGridLinesCount={5}
gridColor="rgba(65, 95, 93, .4)"
gridThickness={1}
unit="ml"
axisTooClose={10}
labelsColor="rgba(255, 255, 255, 0.8)"
labelsFontSize={12}
marginLeft={60}
labelsMarginLeft={15}
/>
</View>
</ScreenContainer>:
null
}
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
export default ChartScreen;
This is my device screen.tsx,
const DeviceScreen = ({
route,
navigation,
}: StackScreenProps<RootStackParamList, 'Device'>) => {
// get the device object which was given through navigation params
const { device } = route.params;
const [isConnected, setIsConnected] = useState(false);
const [services, setServices] = useState<Service[]>([]);
// handle the device disconnection
const disconnectDevice = useCallback(async () => {
navigation.goBack();
const isDeviceConnected = await device.isConnected();
if (isDeviceConnected) {
await device.cancelConnection();
navigation.navigate('Home');
}
}, [device, navigation]);
useEffect(() => {
const getDeviceInformations = async () => {
// connect to the device
const connectedDevice = await device.connect();
setIsConnected(true);
// discover all device services and characteristics
const allServicesAndCharacteristics = await connectedDevice.discoverAllServicesAndCharacteristics();
// get the services only
const discoveredServices = await allServicesAndCharacteristics.services();
setServices(discoveredServices);
PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Permission Localisation Bluetooth',
message: 'Requirement for Bluetooth',
buttonNeutral: 'Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
};
getDeviceInformations();
device.onDisconnected(() => {
navigation.navigate('Home');
});
// give a callback to the useEffect to disconnect the device when we will leave the device screen
return () => {
disconnectDevice();
};
}, [device, disconnectDevice, navigation]);
return (
<ScrollView contentContainerStyle={styles.container}>
<TouchableOpacity style={styles.button} onPress={disconnectDevice}>
<Text style={{fontFamily:"SairaExtraCondensedThin",textAlign:"center",fontSize:15,color:"white"}}>Antrenmanı Sonlandır</Text>
</TouchableOpacity>
<View>
<View style={styles.header} >
<Text>{`Name : ${device.name}`}</Text>
<Text>{`Is connected : ${isConnected}`}</Text>
</View>
{services &&
services.map((service) => <ServiceCard service={service} />)}
</View>
<View>
<ChartScreen chartdata />
</View>
</ScrollView>
);
};
Well, at least, in component.js the isLoading state is never set to true and in screen.tsx you are non passing any data since <ChartScreen chartdata /> means <ChartScreen chartdata={true} />, so it is a boolean property. Then, in getDerivedStateFromProps you are increasing counter that is not part of the state, so, by itself, it will not trigger a new render. Check also that the value of hr is set in the proper place, looks strange to me to see it not as a part of the state too, or as a property. It needs some investigation. Also, the useEffect dependencies are objects so check this out, but consider to refactor the useEffect part, react useEffect comparing objects
Note
Seems to me that there are some misconceptions about React here cousing most of the issues.
This may not be the actual and full answer but I decided to provide my input as a starting point for further investigation and impovements.

How to render item when data is updated react native

Hi guys I am currently facing a problem where in I need to update the data inside of a pop up
here's my code
useEffect(() => {
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
accepted,id,name,rating,services,userDestinationLat,userDestinationLng,userOriginLat,userOriginLng
} = doc.data();
list.push({ accepted, id, name, rating, services, userDestinationLat, userDestinationLng, userOriginLat, userOriginLng });
});
setUserBookingData(list);
});
}, []);
useEffect(() => {
// use this just to get userbookingData
console.log(userBookingData);
},[userBookingData]);
const [newOrder, setNewOrder] = useState({
id: '1',
service: userBookingData.services,
originLatitude : originalPos.latitude,
originLongitude: originalPos.longitude,
destinationLatitude: 45.24953,
destinationLongitude: -76.360733,
user:{
rating: userBookingData.rating,
name: userBookingData.name,
}
});
now here on my return view
return(
{ !userBookingData.length ? (<NewOrderPopUp
newOrder={newOrder}
onDecline={onDecline}
duration={2}
distance={0.5}
onAccept={() => onAccept(newOrder)}
/>) : (
<View></View>
)
}
);
here's the popup component it's in another page
const NewOrderPopUp = ({newOrder, onAccept, onDecline, duration, distance}) => {
return (
<View style={styles.root}>
<Pressable onPress={onDecline} style={styles.declineButton}>
<Text style={styles.declineText}>Decline</Text>
</Pressable>
<Pressable onPress={onAccept} style={styles.popupContainer}>
<View style = {styles.row}>
<Text style = {styles.service}>{newOrder.service}</Text>
{/* <Image source={{}}/> */}
<View style={styles.userBg}>
<FontAwesome name={"user"} color={"white"} size={35}/>
</View>
<Text style = {styles.service}>
<AntDesign name={"star"} size={16}/>
{newOrder.user.rating}
</Text>
</View>
<Text style = {styles.minutes}>{duration} mins</Text>
<Text style = {styles.distance}>{distance} KM</Text>
</Pressable>
</View>
);
};
it won't update the data / show the pop up wen there's a data
The expectation output is it should be able to pop up the NewOrderPopUp screen when there's a data.
here's the data from the userBookingData
I need to display it here
The fact that you are filling userBookingData in first useEffect does not allow you to write an useState getting value from userBookingData itself.
React doesn't work like that.
Not only but if I understand correctly, userBookingData is an array so write userBookingData.services returns null.
Lets suppose that you want to have in newOrder the first element that comes from userBookingData. In this case, you should write something like:
const [newOrder, setNewOrder] = useState({});
useEffect(() => {
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
accepted,id,name,rating,services,userDestinationLat,userDestinationLng,userOriginLat,userOriginLng
} = doc.data();
list.push({ accepted, id, name, rating, services, userDestinationLat, userDestinationLng, userOriginLat, userOriginLng });
});
setUserBookingData(list);
// here set newOrder
let resultObj = {};
resultObj.id = 1;
resultObj.service = list[0].services;
...
setNewOrder(resultObj);
});
}, []);

Update state when returning to previous page

I'm getting a description from a query call which is showing on the page "profile". When the user wants to edit this description they go to another page called "editProfile". When they are done editing they can press submit which will change the description on the server. My problem is that when I return to the "profile" page the description is not updated in the UI. How do I best fix this? With a hook or some sort of state management like redux? Thanks in advance
function Profile({ navigation }: ProfileStackNavProps<"Profile">) {
const { loading, error, data } = useQuery(GET_DESCRIPTIONS_FROM_ID);
const [description, setDescription] = useState(data.profiles[0].description);
return (
<Text>{description}</Text>
//bunch of other code
<Button
onPress={() => {
navigation.navigate("EditProfile", { description });
}}
>
)
}
function EditProfile({ route }: ProfileStackNavProps<"EditProfile">) {
const [text, setText] = useState(route.params.description);
const [editDescription, { loading, error }] = useMutation(EDIT_DESCRIPTION);
return (
<Center>
<TextInput onChangeText={setText} value={text} editable={true} />
<MyButton
title={"Save"}
onPress={() => {
editDescription({
variables: { description: text },
});
}}
/>
</Center>
);
}
when navigate to EditProfile, you pass a callback function like this:
<Button
onPress={() => {
navigation.navigate("EditProfile", { description,cb:(newDes)=>{setDescription(newDes)} });
}}
>
then, when you finish edit, call the function you pass:
route.params.cb(newDes)

React Native Functional Component Updates State On Second Submit Only

When I console log my values, it updates only if I submit the form the second time. Why is this happening?
const Link = (props) => {
const { state, scrape } = useContext(ScrapeContext);
const [clipboard, setClipboard] = useState();
const [googleClip, setGoogleClip] = useState(false);
const [googleLink, setGoogleLink] = useState('');
const urlFromClipboard = () => {
Clipboard.getString().then((content) => {
if (content.includes('https://www.google.com')){
setGoogleClip(true);
console.log('googleLink', googleLink);
setClipboard(content);
setGoogleLink(`${content.split('?')[0]}?__a=1`);
} else {
setGoogleClip(false);
}
});
if (googleClip) {
console.log(googleLink);
scrape({ googleLink });
}
}
useEffect(() => {
urlFromClipboard();
console.log('useEffect googleLink', googleLink);
console.log('useEffect state', state);
}, [clipboard]);
return (
<View style={styles.container}>
<View style={styles.inputFieldContainer}>
<TextInput
style={styles.inputField}
placeholder='Enter Google url'
autoCapitalize='none'
autoCorrect={false}
value={googleClip ? clipboard : ''}
/>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity
onPress={() => {
urlFromClipboard();
}}
style={styles.touchSubmit}
>
<Text style={styles.touchText}>Submit</Text>
</TouchableOpacity>
</View>
{state.errorMessage ? (
<Text style={styles.errorMessage}>{state.errorMessage}</Text>
) : null}
</View>
);
}
What I have here is a component that will grab data from a url, and on submit it will scrape certain information the app needs, however, it will only update those console log values if I press the submit button twice.
The problem is here that you do not have access the state right after you set the state. So you actually set the state, and it is there, but you only see the updated state second time you run the console log. That does not mean that you have to submit twice but when you submit for second time you run the console.log for the second time and see the value. Because setting state functions are async.In this case, you need to use callback function to actually see the updated state on the console. Hope that helps.
Like Bora Sumer mentioned, you're trying to use the state values before they are actually updated. You can do something like this:
Clipboard.getString().then((content) => {
if (content.includes('https://www.google.com')){
let link = `${content.split('?')[0]}?__a=1`
setGoogleClip(true);
console.log('googleLink', googleLink);
setClipboard(content);
setGoogleLink(link);
console.log(link);
scrape({ googleLink: link });
} else {
setGoogleClip(false);
}
});
You can use async/await each time you setState like following:
const urlFromClipboard = () => {
Clipboard.getString().then( async (content) => {
if (content.includes('https://www.google.com')){
await setGoogleClip(true);
console.log('googleLink', googleLink);
await setClipboard(content);
await setGoogleLink(`${content.split('?')[0]}?__a=1`);
} else {
await setGoogleClip(false);
}
});
if (googleClip) {
console.log(googleLink);
scrape({ googleLink });
}
}

Resources