How do I reset nextProps.arbitraryValue once I have used it? - react-native

I am working on a react-native project.
I have Component A that calls Component B.
When screen B is finished doing work, it calls:
NavigationActions.pop({refresh: {workComplete: true}})
And on Screen A, I have the following code:
componentWillReceiveProps(nextProps) {
if (nextProps.workComplete) {
window.alert('work was completed');
}
}
However, the props.workComplete stays set, and I'm not sure how to unset it, so I keep getting an alert when props are changed in this component.
How can I reset that property value?

Not sure what you mean by unset, but this will be called everytime props change and since workComplete is set to true, it will constantly alert you. You can have workComplete as a state value and do something like this:
componentWillReceiveProps(nextProps) {
if (nextProps.workComplete !== this.state.workComplete) {
this.setState({ workComplete: true }, () => window.alert('work was completed'));
}
}

Related

ReactNative UI freezing for a second before rendering a component with a fetch in useEffect()

TL;DR: My UI freezes for .5-1s when I try to render a component that does a API fetch within a useEffect().
I have ComponentX which is a component that fetches data from an API in a useEffect() via a redux dispatch. I'm using RTK to build my redux store.
function ComponentX() {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchListData()); // fetch list data is a redux thunk.
}, [dispatch]);
...
return <FlatList data={data} /> // pseudo code
}
as you can see the fetch will happen everytime the component is rendered.
Now I have ComponentX in App along with another component called ComponentY.
Here's a rudamentary implementation on how my app determines which component to show. Pretend each component has a button that executes the onClick
function App() {
const [componentToRender, setComponentToRender] = useState("x");
if (componentToRender === "x") {
return <ComponentX onClick={() => setComponentToRender("y")}/>
} else {
return <ComponentY onClick={() => setComponentToRender("x")}/>
}
}
Now the issue happens when I try to move from ComponentY to ComponentX. When I click the "back" button on ComponentY the UI will freeze for .5-1s then show ComponentX. Removing the dispatch(fetchListData()); from the useEffect fixes the issue but obviously I can't do that since I need the data from the API.
Another fascinating thing is that I tried wrapping the dispatch in an if statement assuming that it would prevent a data fetch thus resolving the "lag" when shouldReload is false. The UI still froze before rendering ComponentX.
useEffect(() => {
if (shouldReload) { // assume this is false
console.log("reloading");
dispatch(fetchListData());
}
}, [dispatch, shouldReload]);
Any idea what's going on here?
EDIT:
I've done a little more pruning of code trying to simplify things. What I found that removing redux from the equation fixes the issue. By simply doing below, the lag disappears. This leads me to believe it has something to do with Redux/RTK.
const [listData, setListData] = useState([]);
useEffect(() => {
getListData().then(setListData)
}, []);
Sometimes running the code after interactions/animations completed solves the issue.
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
dispatch(fetchListData());
});
}, [dispatch]);

How to stop Vue.js 3 watch() API triggering on exit

I have implemented a watch within a Vue component that displays product information. The watch watches the route object of vue-router for a ProductID param to change. When it changes, I want to go get the product details from the back-end API.
To watch the route, I do this in Product.vue:
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute();
async function getProduct(ProductID) {
await axios.get(`/api/product/${ProductID}`).then(..do something here)
}
// fetch the product information when params change
watch(() => route.params.ProductID, async (newID, oldID) => {
await getProduct(newId)
},
//watch options
{
deep: true,
immediate: true
}
)
},
}
The above code works, except that if a user navigates away from Product.vue, for example using the back button to go back to the homepage, the watch is triggered again and tries to make a call to the API using undefined as the ProductID (becaues ProductID param does not exist on the homepage route) e.g. http://localhost:8080/api/product/undefined. This causes an error to be thrown in the app.
Why does the watch trigger when a user has navigated away from Product.vue?
How can this be prevented properly? I can do it using if(newID) { await getProduct(newId) } but it seems counterintuitive to what the watch should be doing anyway.
UPDATE & SOLUTION
Place the following at the top replacing the name for whatever your route is called:
if (route.name !== "YourRouteName") {
return;
}
That will ensure nothing happens if you are not on the route you want to watch.
I ran into the same problem. Instead of watching the current route, use vue-router onBeforeRouteUpdate, which only gets called if the route changed and the same component is reused.
From https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// same as beforeRouteLeave option with no access to `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// cancel the navigation and stay on the same page
if (!answer) return false
})
const userData = ref()
// same as beforeRouteUpdate option with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
// only fetch the user if the id changed as maybe only the query or the hash changed
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
watch registers the watcher inside an vue-internal, but component-independent object. I think it's a Map. So destroying the component has no effect on the reactivity system.
Just ignore the case where newID is undefined, like you already did. But to prevent wrapping your code in a big if block just use if(newID === undefined)return; at the beginning of your callback. If your ids are always truthy (0 and "" are invalid ids) you can even use if(!newID)return;.
well, in your use case the best approach would be to have a method or function which makes the api call to the server, having watch is not a really good use of it, because it will trigger whenever route changes and you do not want that to happen, what you want is simply get the productID from route and make the api call,
so it can be done with getting the productID in the created or mounted and make the api call!

An element that has v-if isn't updated as expected

Lately I've been trying to use VueJS to create a small project and I have encountered this problem. So I have this navigation bar that includes a Log Out Button element that'd only shown if someone has already logged in. This is my Navbar before I logged in.
Before Login Navbar. No Log Out Button
After I logged in, my navbar'd be like this.
after logged in Navbar. Log Out Button exists
This is my code for this navbar component.
<div id="logout-button" v-if="isLoggedIn">
<button v-on:click="logOut">Log Out</button>
</div>
....
<script>
export default {
name: "NavigationBar",
data: function () {
return {
isLoggedIn: false,
};
},
mounted() {
if (localStorage.isLoggedIn !== undefined) {
this.isLoggedIn = localStorage.isLoggedIn;
}
},
methods: {
...
logOut: function () {
localStorage.isLoggedIn = false;
localStorage.access = null;
localStorage.refresh = null;
location.reload();
},
},
};
</script>
What I've been trying is whenever the log out button clicked, I'd change the value of a key in my local storage, named "isLoggedIn" from "true" to "false". After that, I'd reload the page and when the page reached mounted, the component's data named "isLoggedIn"'d be changed with the new value of my local storage which is now "false". Hence, my expectation is the button wouldnt' be there.
The problem is, whenever I clicked log out, the Log Out Button'd be always there as if I didn't change the value that hopefully doesn't evaluated as "true" in the v-if. I don't know which or what is the problem since I'm new to Vue and I really hope you guys could tell me what it is. Thank you very much!
In your logOut() method, you're only changing the value in the localStorage, however the v-if="isLoggedin" is binded to the component's data so you also need to update that.
logOut: function () {
localStorage.isLoggedIn = false;
localStorage.access = null;
localStorage.refresh = null;
this.isLoggedIn = false
location.reload();
},
Additionally, you can only store strings in the localStorage, so you need to evaluate your string to return a boolean for your component's data.
this.isLoggedIn = (localStorage.isLoggedIn === 'true')
Here is a small jsfiddle for you to play with: https://jsfiddle.net/6rv7j5bg/
Try using:
localStorage.setItem("isLoggedIn", false)
and:
localStorage.getItem("isLoggedIn")
and see if it makes any difference!

How do I call functions inside render() method and still keep it pure?

I want to use a modal in my React Native app that ask the user to confirm his action.
The state looks like this:
state = {
dialogVisible: false,
confirmed: null
}
If the user confirms his delete action (turning confirmed to true), I want to execute my delete() method.
Delete method:
delete = () => {
const { deckName } = this.props.navigation.state.params
console.log('WAS CONFIRMED')
this.setState({
dialogVisible: false
})
this.props.navigation.navigate('Decks')
removeDeckFromStorage(deckName)
this.props.dispatch(removeDeck(deckName))
this.setState({
confirmed: null
})
}
noDelete = () => {
this.setState({
dialogVisible: false
})
this.setState({
confirmed: null
})
}
When the user confirmed his action, the modal closes, and the delete is done. Afterwards, I want to set confirmed back to null re-use it later.
On the other hand, if the user does not confirm the modal by clicking No, the noDelete() method should be called, which just closes the modal and sets confirmed back to null.
My problem is now that I get a warning saying:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
That is because I check for the state of confirmed inside the render() method:
const { confirmed } = this.state
if (confirmed){
this.delete()
}
else if (confirmed === false){
this.noDelete()
}
I did this because when I checked the state of confirmed inside the delete method right after setting the confirmed state to true, it always said null.
I put the check inside render because after the confirmed state is changed through the user input, the component is re-rendered and thus giving me the right state for the query.
The dialog buttons each change the current confirmed state when clicked:
positiveButton={{
title: "YES",
onPress: () => this.setState({confirmed: true})
}}
So, how can I check for confirmed after it was set but still outside of the render method to keep it pure?
You should never update the state in the render method. You should move your logic to the delete and noDelete functions instead.
I'm not sure how your modal is, however let's suppose it's something like this:
<View>
<TouchableOpacity onPress={this.delete}>
<Text>Delete</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.noDelete}>
<Text>No Delete</Text>
</TouchableOpacity>
</View>
And in the delete and noDelete you simply remove the setState({ confirmed }) since you're already calling the deletion from there.

React native setState using object

I have an object that when logged prints the following:
Object {
"Air Conditioning": false,
"Attic": false,
"Basement": false,
"Bathrooms": false,
"Bedrooms / Living Areas": false,
"Crawl Space": false,
}
I would like to setState using the above. I attempted the following:
componentDidMount() {
this.setAreaNamesInState(this.props.areas)
}
setAreaNamesInState(areaNames) {
let areaNamesList = {}
for (let area of areaNames) {
areaNamesList[area] = false
}
console.log('areaNamesList', areaNamesList)
this.setState(areaNamesList)
console.log('Attic', this.state['Attic'])
}
It doesn't seem to be working, as when I log Attic above it returns undefined.
The answers of other users are correct, you could do the following
this.setState({ areas: areaNamesList }, () => {
console.log('Attic', this.state.areas['Attic'])
})
The difference is you are trying to set the whole state object with your newly created object, which is a bad practice, while this answer updates a property called areas inside your state object with the provided data.
The console log will execute synchronously after the state is updated with the property areas, and log false
As a side note, maybe using componentDidMount is a bad idea if the prop is not provided the first time the component is created and rendered, since it's only executed the first time and never again, the property might be undefined by the time you need it.
Consider switching to other lifecycle methods.
Try with
this.setState({ areaNamesList }, () => {
console.log('Attic', this.state.areaNamesList['Attic'])
})
You're missing curly braces in your setState as this.state is an object.
this.setState({ areaNamesList })
Also worth mentioning that setState may not be completed before console.log('Attic', this.state.areaNamesList['Attic']). setState can take a callback that will be executed when it is complete as such:
this.setState({ Attic: areaNamesList }, () => {
console.log('Attic', this.state.areaNamesList["Attic"])
})