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)
Related
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
The code below is a form in my react app which shows up when a user edits a record from a data table - datatable and edit part is not shown here as it's not relevant.
Explanation of Tab Mismatch issue :
As soon as the below page loads, I see the following tabs (image below) on the UI with Tab1 highlighted:
The network tab of my browser shows the following web service call:
First webservice call for Tab1:
https://myserver.com/MyServices/api/assets/search/getAssetCategories?requestId=100&assetCategoryId=0
Similarly, when I click Tab 2, the web service call is :
Second webservice call for Tab2:
https://myserver.com/MyServices/api/assets/search/getAssetCategories?requestId=100&assetCategoryId=1
And with Tab 3, it's :
Third webservice call for Tab3:
https://myserver.com/MyServices/api/assets/search/getAssetCategories?requestId=100&assetCategoryId=2
As you can see, since I am sending the value of selectedTabIndex for the parameter assetCategoryId in my webservice call in the useEffect function and value of event.index for the parameter assetCategoryId inside onTabSelected function. , the Tab1 is highlighted because of 0 value of selectedTabIndex inside useEffect function and hence the
//Inside useEffect function
params: { assetCategoryId: selectedTabIndex }
//Inside onTabSelected function
params: {assetCategoryId: event.index}
However, since I have the following available:
const assetCategories = JSON.parse('[{"id":1,"value":"Tab 1"},{"id":2,"value":"Tab 2"},{"id":3,"value":"Tab 3"}]');
Question 1.
Inside useEffect function, I want to send first value of the id field to the assetCategoryId parameter instead of the selectedTabIndex while calling the webservice call. Is it possible to do this? I mean I did like this assetCategores[0].id and it worked but is this a good way to achieve this?
Question 2.
I want to keep the activeIndex value based on the first id parameter, which is 1 in my case and not based on selectedTabIndex like it is in my code now:
<TabView id='AssetsTabView' activeIndex={selectedTabIndex}
One another issue which could arise is when the values of id could be like this and I would still want to display Tab1 , Tab2 and Tab3 properly:
const assetCategories = JSON.parse('[{"id":34,"value":"Tab 1"},{"id":66,"value":"Tab 2"},{"id":999,"value":"Tab 3"}]');
My complete code below:
import React, { useState, useEffect } from 'react';
import { Form, withFormik} from 'formik';
import {Button} from '#material-ui/core'
import axios from 'axios'
import {TabPanel, TabView} from "primereact/tabview";
const RequestForm = (props) => {
const {values, setFieldValue, touched, errors, isSubmitting, handleReset, handleChange} = props;
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
useEffect(() => {
if(props.dataRequest){
let editeddataRequestId = props.dataRequest.requestId;
axios
.get('api/assets/search/getAssetCategories?requestId='+editeddataRequestId, {
params: {
assetCategoryId: selectedTabIndex
}
}).then(response => {
}).catch(err => console.log(err));
}//end if of props.dataRequest
}, []);
var onTabSelected = (event) => {
(event.index) ? setSelectedTabIndex(event.index) : null
if(props.dataRequest){
let editeddataRequestId = props.dataRequest.requestId;
axios
.get('api/assets/search/getAssetCategories?requestId='+editeddataRequestId, {
params: {
assetCategoryId: event.index
}
}).then(response => {
}).catch(err => console.log(err));
}
};
const assetCategories = JSON.parse('[{"id":1,"value":"Tab 1"},{"id":2,"value":"Tab 2"},{"id":3,"value":"Tab 3"}]');
const DynamicTabView = ({ activeIndex, onTabChange }) => (
<div style={{width: 'max-content', whiteSpace: 'nowrap', marginLeft: 'auto', marginRight: 'auto'}}>
<TabView id='AssetsTabView' activeIndex={selectedTabIndex}
onTabChange={(e) => setSelectedTabIndex(e.index), (e) => onTabSelected(e)}>
{assetCategories.map((item, i) =>
<TabPanel key={i} header={item.value}></TabPanel>
)}
</TabView>
</div>
);
return (
<div>
<Form className="form-column-3">
<DynamicTabView activeIndex={selectedTabIndex} onTabChange={(e) => {setSelectedTabIndex(e.index), () => { this.onTabSelected(e) }}}/>
<Button size="large" variant="contained" color="primary" onClick={props.onCancel} style={{marginLeft: '5px'}} type="button">Cancel</Button>
</Form>
</div>
)
};
export const DataRequestEnhancedFormEdw = withFormik({
mapPropsToValues: props => {
return {}
},
})(RequestForm)
I wanna implement a live Search function on my Redux State which I use in my home page via useSelector. and when user delete the search content original data show up as well. I use filter but the data doesn't affect. how can I achieve that? any help would be appreciated:
const Home = (props) => {
const companies = useSelector(state => state.companies.availableCompanies); //this is my data
const handleSearch = (e) => {
companies.filter(el => el.name.includes(e));
console.log(companies) // here I see my data changes but doesn't affect on UI
}
return (
<SearchBar onChangeText={handleSearch} />
<View style={styles.cardContainer}> // here I show data.
{companies.map((el, index) => {
return (
<Card
key={el.id}
companyId={el.id}
companyName={el.name}
companyImage={el.image}
companyMainAddress={el.mainAddress}
companyPhoneNumber={el.telephoneNumber}
companyDetails={el.details}
onPress={() => {
navigation.navigate('CardDetails', {
id: el.id,
companyName: el.name,
});
}}
/>
)
})}
</View>
Have a try with the below changes
Hope it will work for you.
const Home = (props) => {
const [searchQuery, setSearchQuery] = useState();
const [filteredData, setFilteredData] = useState();
const companies = useSelector(state => state.companies.availableCompanies); //this is my data
const handleSearch = (e) => {
setSearchQuery(e);
}
useEffect(() => {
if (searchQuery && typeof searchQuery === "string" && searchQuery.length > 0) {
const searchData = companies.filter(el => el.name.includes(searchQuery));
setFilteredData([...searchData]);
} else {
setFilteredData();
}
}, [searchQuery, companies])
return (
<SearchBar onChangeText={handleSearch} />
<View style={styles.cardContainer}>
{(filteredData && Array.isArray(filteredData) ? filteredData : companies).map((el, index) => {
return (
<Card
key={el.id}
companyId={el.id}
companyName={el.name}
companyImage={el.image}
companyMainAddress={el.mainAddress}
companyPhoneNumber={el.telephoneNumber}
companyDetails={el.details}
onPress={() => {
navigation.navigate('CardDetails', {
id: el.id,
companyName: el.name,
});
}}
/>
)
})}
</View>
This variable below is a copy of state.companies.availableCompanies that is replaced on every render with the original value from state.companies.availableCompanies.
const companies = useSelector(state => state.companies.availableCompanies); //this is my data
Since you're assigning the result of filter to the copy, and not to the original variable inside the redux store. The results are not reflected there, and every time rerender happens, the Functional Component is called again, making all the code inside this function execute again. So, there is a new variable companies that is not related to the old one.
To actually update the original variable inside redux. You need to create a redux action, and dispatch it.
You need to go back and learn the fundamental concepts of redux before proceeding with this.
Here is the link to the documentation explaining how the data flow works in redux.
https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
You need to use useDispatch() to get dispatcher and dispatch an action to your reducer with state to update in your handleSearch ()
Something like:
const dispatch = useDispatch();
const handleSearch = (e) => {
dispatch({type:"YOUR_ACTION",payload:companies.filter(el => el.name.includes(e))})
console.log(companies) ;
}
Refer: https://medium.com/#mendes.develop/introduction-on-react-redux-using-hooks-useselector-usedispatch-ef843f1c2561
export default function App({ navigation, navigation: { goBack } }) {
const [hasPermission, setHasPermission] = useState(null);
const [scanned, setScanned] = useState(false);
const [data, setData] = useState("");
const handleBarCodeScanned = ({ data }) => {
setScanned(true);
axios
.get(
`https://api.edamam.com/api/food-database/v2/parser?upc=${data}&app_id=2626c70d&app_key=0c0f87ae4e5437621363ecf8e7ea80ae&page=20`
)
.then((res, data) => {
setData(res.data.hints);
navigate("Food", {
title: data.label, <--------------------------
});
})
.catch((error) => {
console.log(error.response.data);
});
};
return (
<View style={styles.container}>
<BarCodeScanner
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
style={StyleSheet.absoluteFillObject}
>
<TouchableOpacity onPress={() => goBack()}>
<Icon
name="angle-left"
color="white"
size={40}
style={{ top: hp("10%"), left: wp("5%") }}
/>
</TouchableOpacity>
</BarCodeScanner>
</View>
);
}
Hello, I'm trying to send title to a page using React Navigation, so when the user scan an item, and the item it's found in the database it'll take him/her on the Food page with that constant. Currently the data is obtained from the database correctly, but when I run the code it says Possible Unhadled Promise Rejection: TypeError: undefined is not an object
you are using the same name data for a state, inside a function and again inside .then of the API try changing the names and then check it, I didn't figure out which data.label is you passing to the title please use different names for the variable inside the function ({dataFromBarCode}) and then (res, dataApi) it will work
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 });
}
}