I am using drawer navigation with react navigation v5, and i have a screen called profile, that takes in a route prop, that i pass user id to. Problem is, when I visit a profile, and then logout, and log back in, i get an error saying that route.params.id is not an object, undefined. In my profile.tsx I checked where i use the route params, and its as in the shown code below:
useEffect(() => {
if (!route) {
return navigation.navigate("Søg Brugere");
}
getUser();
}, [route]);
and getUser function should not be executed, however I include it for clarlity.
const getUser = async () => {
if (!route) return;
try {
console.log(route.params.id);
const id = route.params.id;
setRefreshing(true);
const { data } = await (await HttpClient()).get(
config.SERVER_URL + "/api/user/get-user-by-id/" + id
);
setRefreshing(false);
setProfile(data.user);
setMatch(data.match);
setInitiated(true);
if (socket && user) {
const notificationData = {
url: `/profile/${user._id}`,
type: "new-visit",
text: `Nyt besøg fra ${user.displayName}`,
user: data.user,
resourceId: user._id,
};
socket.emit("notification", notificationData);
}
} catch (e) {
navigation.navigate("Søg Brugere");
}
};
and also a snippet of my logout function, used in drawer navigator:
<View style={{ flex: 1, justifyContent: "flex-end" }}>
<Button
onPress={async () => {
props.navigation.replace("Søg Brugere");
props.navigation.closeDrawer();
await AsyncStorage.removeItem("token");
setUser(null);
}}
title="Log Ud"
color="#F44336"
/>
</View>
i solved this by adding at the top of my rfc:
const id = route?.params?.id
Related
There is a React component which contains list of users and form to invite a new user.
"inviteNewUser" is a *POST* request in backend
"getUsers" is a *GET* request to get all users
The problem is that after clicking on button "Invite User" I would like to see the invited user in the list of users ("currentUsers" in code below) without refreshing the page. But right now it happens only after I refresh the whole page.
when I'm trying to make a GET request to get all users right after inviteNewUser(data) (POST request) I'm getting the "old" user list without user which I just invited. So the "currentUsers" list is not immediately updated
Could someone help me to fix this issue ?
export function MyForm({
getUsers,
inviteNewUser,
userId,
currentUsers
}) {
useEffect(() => {
getUsers(userId);
}, [userId]);
function handleSendInvite(data) {
inviteNewUser(data);
getUsers(data.userId);
}
return (
<>
{currentUsers.map((user) => (
<UserItem
key={user.userId}
user={user}
/>
))}
<Button
text="Invite User"
onClick={() => {
handleSendInvite({userId});
}}
/>
</>);
}
MyForm.propTypes = {
getUsers: PropTypes.func.isRequired,
inviteNewUser: PropTypes.func.isRequired,
userId: PropTypes.number.isRequired,
currentUsers: PropTypes.arrayOf(UserInfo),
};
const mapStateToProps = (state) => {
const { id } = routerParamsSelector(state);
const currentUsers = selectCurrentUsers(state);
return {
userId: parseInt(id, 10),
currentUsers,
};
};
const mapDispatchToProps = {
getUsers,
inviteNewUser
};
export default connect(mapStateToProps, mapDispatchToProps)(MyForm);
Try async and await, it works.
const handleSendInvite = async (data) {
await inviteNewUser(data);
getUsers(data.userId);
}
It should be user instead of userId as you access the property of the object in the function.
<Button text="Invite User" onClick={() => { handleSendInvite(user); }} />
------------------------------------------------------------------------
const handleSendInvite = async (data) {
await inviteNewUser(data);
getUsers(data.userId);
}
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
Building a site where users upload images/video, in the component that handles that, I have the images load into the page after being uploaded, so that the user can make sure they want to post them, and have the option to remove them if they like. I originally had this as a Class-based view, and everything worked as expected, but now, after changing to a functional component, after uploading images, React doesn't seem to notice the changes to imageLinks (even though the console shows that imageLinks is getting added to), until I update something else like the post title, then they all load in as expected. Once loaded in, if I click the delete button, React instantly updates and the photos/videos no longer shows up, as expected.
Any ideas as to why this is behaving so oddly?
The fact that the deleteMedia function works just fine is what really weirds me out.
I set up my variables like this
export default function NewPost(props) {
const [postCategories, setPostCategories] = useState([]);
const [postTitle, setPostTitle] = useState();
const [postDescription, setPostDescription] = useState();
const [postHashtags, setPostHashtags] = useState();
const [imageLinks, setImageLinks] = useState([]);
...}
In my component, I have this to handle uploading and deleting files.
const uploadMedia = async (file) => {
var csrftoken = getCookie("csrftoken");
var media_id = makeMediaID();
let formData = new FormData();
formData.append("content_id", contentID);
formData.append("creator_id", USER_ADDRESS);
formData.append("content_media", file);
formData.append("media_id", media_id);
const response = await fetch("/api/newpost/media/", {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
},
body: formData,
});
console.log(response);
const r_json = await response.json();
if (r_json.success) {
let tempLinks = imageLinks;
tempLinks.push({
img: r_json.image_url,
id: r_json.m_id,
type: r_json.m_type,
});
setImageLinks(tempLinks);
console.log(imageLinks);
} else {
console.log(r_json.message);
}
};
const deleteMedia = async (media) => {
var csrftoken = getCookie("csrftoken");
let formData = new FormData();
formData.append("media_id", media);
formData.append("creator_id", USER_ADDRESS);
const response = await fetch("/api/newpost/media/delete/", {
method: "DELETE",
headers: {
"X-CSRFToken": csrftoken,
},
body: formData,
});
console.log(response);
const r_json = await response.json();
if (r_json.success) {
let tempLinks = imageLinks.filter((item) => item.id !== media);
setImageLinks(tempLinks);
} else {
console.log("Media deletion error");
}
};
And in my render, I have this, which worked just fine when it was a class-based component.
{imageLinks.map((item) => (
<Grid item xs={9} align="center" key={item.img}>
<Card style={{ maxWidth: 550, margin: 15 }}>
<div
style={{
display: "flex",
alignItem: "center",
justifyContent: "center",
}}
>
<CardMedia
style={{
width: "100%",
maxHeight: "550px",
}}
component={item.type}
image={item.img}
controls
title={String(item.img)}
/>
</div>
<div align="center">
<CardActions>
<Button
endIcon={<DeleteForeverIcon />}
label="DELETE"
color="secondary"
variant="contained"
onClick={() => deleteMedia(item.id)}
>
REMOVE
</Button>
</CardActions>
</div>
</Card>
</Grid>
))}
Issue
The issue is a state mutation.
const [imageLinks, setImageLinks] = useState([]);
tempLinks is a reference to the imageLinks state. You are pushing directly into this tempLinks array and saving the same array reference back into state. The code is also incorrectly attempting to log the state immediately after enqueueing the state update. This doesn't work as React state updates are asynchronously processed.
if (r_json.success) {
let tempLinks = imageLinks; // <-- reference to state
tempLinks.push({ // <-- state mutation!
img: r_json.image_url,
id: r_json.m_id,
type: r_json.m_type,
});
setImageLinks(tempLinks); // <-- same reference back into state
console.log(imageLinks); // <-- only logs current state
}
The imageLinks state array reference never changes so React doesn't see this as state actually being updated.
Solution
Create and return a new array reference for React.
if (r_json.success) {
setImageLinks(imageLinks => [ // <-- new array reference
...imageLinks, // <-- shallow copy previous state
{ // <-- append new element object
img: r_json.image_url,
id: r_json.m_id,
type: r_json.m_type,
}
]);
}
Use a separate useEffect hook to console log the state updates.
useEffect(() => {
console.log(imageLinks);
}, [imageLinks]);
I'm doing the notification page of my react native app. It has infinite scroll and "pull to refresh" options. Entering to the page it works, and it works also pulling to refresh.
The problem occurs when I scroll down because it seems it calls server to fetch new notifications but it doesn't concatenate to the array.
import React, { useState, useEffect, useCallback, Component } from "react";
import {
View,
Text,
FlatList,
Button,
Platform,
ActivityIndicator,
StyleSheet,
ScrollView,
RefreshControl,
SafeAreaView,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import i18n from "i18n-js";
import Colors from "../../constants/Colors";
import { getNotificationList } from "../../utils/NotificationsUtils";
import Card from "../../components/UI/Card";
const NotificationsScreen = (props) => {
const [refreshing, setRefreshing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(0);
const [notifications, setNotifications] = useState([]);
const [error, setError] = useState();
const dispatch = useDispatch();
const onRefresh = useCallback(async () => {
setRefreshing(true);
setNotifications([]);
setPage(0);
console.log("-- Refreshing --");
getNotifications().then(() => {
setRefreshing(false);
});
}, [dispatch, setRefreshing]);
const fetchMoreNotifications = useCallback(async () => {
const newPage = page + 7;
setPage(newPage);
console.log(
"FETCH MORE from page " + newPage + " on array of " + notifications.length
);
getNotifications().then(() => {
setIsLoading(false);
});
}, [dispatch, getNotifications]);
const getNotifications = useCallback(async () => {
setError(null);
setIsLoading(true);
try {
console.log("Get from page " + page);
// let fromRecord = (page - 1) * 7;
const retrievedNotifications = await getNotificationList(
page,
7,
true,
false
);
console.log(
"Setting " +
retrievedNotifications.response.notifications.length +
" new notifications on an already existing array of " +
notifications.length +
" elements"
);
let updatedNews = notifications.concat(
retrievedNotifications &&
retrievedNotifications.response &&
retrievedNotifications.response.notifications
);
setNotifications(updatedNews);
} catch (err) {
setError(err.message);
}
setIsLoading(false);
}, [dispatch, setIsLoading, setNotifications, setError]);
useEffect(() => {
setIsLoading(true);
getNotifications(page).then(() => {
setIsLoading(false);
});
}, [dispatch, getNotifications]);
return (
<View>
{error ? (
<View style={styles.centered}>
<Text>Error</Text>
</View>
) : refreshing ? (
<View style={styles.centered}>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
) : !notifications || !notifications.length ? (
<View style={styles.centered}>
<Text>No data found</Text>
</View>
) : (
<FlatList
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
data={notifications}
keyExtractor={(notification) => notification.notificationQueueId}
onEndReached={fetchMoreNotifications}
onEndReachedThreshold={0.5}
initialNumToRender={4}
renderItem={(itemData) => (
<View
style={{
marginTop: 10,
height: 150,
width: "100%",
}}
>
<Card style={{ height: 150, backgroundColor: "white" }}>
<Text style={{ fontSize: 16, color: Colors.black }}>
{itemData.item.text}
</Text>
</Card>
</View>
)}
/>
)}
</View>
);
};
const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});
export default NotificationsScreen;
If I scroll to end it triggers 'fetchMoreNotifications' function and I get this in the console:
FETCH MORE from page 7 on an array of 0
Get from page 0
Setting 7 new notifications on an already existing array of 0 elements
FETCH MORE from page 7 on an array of 0
Get from page 0
Setting 7 new notifications on an already existing array of 0 elements
FETCH MORE from page 7 on an array of 0
Get from page 0
Setting 7 new notifications on an already existing array of 0 elements
...and so on
As you can see it says 'existing array of 0 elements' even if previously I saved notifications. Maybe it has some issue with useCallback's dependency?
Issue :
There are 2 main issues, one with page and second with notifications, due to useCallback and dependencies, useCallback function will always point to the old values which are not in dependencies until one of the dependencies for updated.
1) The solution to page issue :
Pass newPage as param to getNotifications, due to async behavior of setPage it will not get updated directly
And on the second time, to get the updated value of page you can pass page as a dependency.
2) The solution to the notification issue :
Update the notification directly from its prev state value with setState(prevState => newState).
Solution :
const fetchMoreNotifications = useCallback(async () => {
const newPage = page + 7;
setPage(newPage);
console.log(
"FETCH MORE from page " + newPage + " on array of " + notifications.length
);
getNotifications(newPage).then(() => { // <---- Pass as param
setIsLoading(false);
});
}, [page]); // <---- Add page as dependency
const getNotifications = useCallback(
async page => { // <---- Get page as a param
setError(null);
setIsLoading(true);
try {
console.log("Get from page " + page);
// let fromRecord = (page - 1) * 7;
const retrievedNotifications = await getNotificationList(
page,
7,
true,
false
);
setNotifications(prevNotification => prevNotification.concat(
retrievedNotifications &&
retrievedNotifications.response &&
retrievedNotifications.response.notifications
)); // <---- Setting up state directly from previous value, instead of getting it from clone version of use callback
} catch (err) {
console.log(err);
setError(err.message);
}
setIsLoading(false);
},
[setIsLoading, setNotifications, setError]
);
WORKING DEMO :
Check the console log for updated page value and notification will be rendered on Html it self
NOTE : Removed some of your code just to improve code readability and
debug the issue
The problem is really simple. The getNotifications function is created using useCallback and hasn't used notifications as a dependency. Now when notifications updates, the getNotications function is still referring to the old notifications values due to closure.
Also note that you call getNotifications on fetchMoreNotifications immediately after setting page state but page state too is bound by closure and will not update in the same re-render
The solution here is to use the function approach to setNotifications and use useEffect to trigge4r getNotification on page change
const fetchMoreNotifications = useCallback(async () => {
const newPage = page + 7;
setPage(newPage);
}, [dispatch, getNotifications]);
useEffect(() => {
setIsLoading(true);
getNotifications(page).then(() => {
setIsLoading(false);
});
}, [dispatch, page, getNotifications]);
const getNotifications = useCallback(async () => {
setError(null);
setIsLoading(true);
try {
console.log("Get from page " + page);
// let fromRecord = (page - 1) * 7;
const retrievedNotifications = await getNotificationList(
page,
7,
true,
false
);
setNotifications(prevNotification => prevNotification.concat(
retrievedNotifications &&
retrievedNotifications.response &&
retrievedNotifications.response.notifications
));
} catch (err) {
setError(err.message);
}
setIsLoading(false);
}, [dispatch, setIsLoading, setNotifications, setError]);
Am I doing something wrong here? I'm trying to get some data, but its returning errors in my console log, saying something about rerendering
mycompontent:
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')){
console.log('inside includes');
setGoogleClip(true);
setClipboard(content);
setGoogleLink(`${content.split('?')[0]}?somedata`);
} else {
setGoogleClip(false);
}
});
if (googleClip) {
scrape({ googleLink });
}
}
useEffect(() => {
urlFromClipboard();
}, [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>
);
}
scrape context file:
const scrape = (dispatch) => {
console.log('dispatch scrape', dispatch)
return async ({googleLink}) => {
console.log('scrape googleLink',googleLink);
try {
const response = await googleApi.post('/googleLink', {googleLink});
dispatch({ type: 'googleLink', payload: response });
navigate('NewPage');
} catch (error) {
dispatch({
type: 'googleLink_error',
payload: 'Please submit correct Google link.'
})
}
}
}
I got the data in my backend to respond correctly, but it's failing to finish what its intended, on the frontend side. The console.log('dispatch scrape', dispatch) is giving me an error in my console:
dispatch scrape function dispatchAction(fiber, queue, action) {
(function () {
if (!(numberOfReRenders < RE_RENDER_LIMIT)) {
throw ReactError(Error("Too many re-renders. React limits the number …
It doesn't produce the entire error until I hover over it... saying more inline about "to prevent infinite loop..." Here's the screenshot:
it looks like every render is setting clipboard to a new value, triggering useEffect in each new render, thus causing an infinite loop. I'm new to react native, and have been suffering a lot from infinite loop myself. Not sure it helps to change the useEffect's dependency to the function urlFromClipboard, and wrap urlFromClipboard in a callback function, of course set clipboard as dependency of the callback function:
const urlFromClipboard = useCallback(() => {
Clipboard.getString().then((content) => {
if (content.includes('https://www.google.com')) {
console.log('inside includes');
setGoogleClip(true);
setClipboard(content);
setGoogleLink(`${content.split('?')[0]}?somedata`);
} else {
setGoogleClip(false);
}
});
if (googleClip) {
scrape({ googleLink });
}
}, [clipboard])
useEffect(() => {
urlFromClipboard();
}, [urlFromClipBoard]);