What is the best way to initialize a state in ReactNative with custom values on a Modal? - react-native

So, I have a specific problem where i need to initialize a state with custom values that comes from the parent component props.theValue. But the problem is that the props.theValue isn't ready when we set the state. So this line should never work:
const [grams, setGrams] = useState(props.theValue)
The scenario:
We have a list of food on the Parent Screen, when the users Clicks on it, it opens a modal (Like a PopUp) with some options to choose before the selected item gets manipulated
What I tried?
Set initial flag useState(-1) to indicate it should set the value from props but again, the props isn't ready in this state
Save the state on the parent and pass the state with the set Function to the modal. But still it doesn't seems to be right
useEffect but whenever i call setGrams it get a Loop Rendering error, also the props.theValue is still undefined at this point
Example:
In this case, the both TextInput must have a initial value that comes
from the Parent Component and must be manipulated.

You could use Redux for manipulating your data between Components or you can try something like:
set a Global variable:
let previousObj = null
and then set a timeout to update your data only once, even if you set Modal invisible and Visible again
setTimeout(() => {
if(food.grams != undefined && previousObj != food){
previousObj = food
setGrams(""+food.grams)
}
}, 100)

You can either handle it in the child with default value or don't render the component in parent until the value is available:
{theValue && <Component theValue={theValue} />}
You can use useEffect to update the default value as well:
const [grams, setGrams] = useState(0)
useEffect(() => {
if (theValue) {
setGrams(theValue);
}
}, [theValue])

Related

Struggling with useEffect and flatlist

I am rendering a component for every item in a flatList. Each component has a label, and when the component is rendered, I have a useEffect that fetches the updated label name for that specific label.
For some reason, it seems to only be running for the last item in the flatList. The last item is the only item with the updated name, while all other still contain the outdated information.
Assuming there is an updated name for each label, why could my useEffect only be running on the last item?
<FlatList
data={labels}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
Label.js - I would think this would run for every label component rendered. Could there be a possible issue with what I have here? Or must it be somewhere else in my code?
let name = label.name;
useEffect(() => {
updateLabel()
name = label.name
}, [label]);
return (
<>
{name}
</>
)
I see several possible issues. Some important code is missing, so I'll answer what I can.
You're not using state to hold your label name in the Label component (name = label.name), so React will never know to re-render the component when it changes. It's rare to need to use a let variable in React. To hold properties that the component needs to change, use the useState hook.
However, you shouldn't do that here, because of the next point.
It looks like you are updating the label somewhere else, and also locally (name = label.name). Don't do this, it's too easy for the two to get out of sync and cause bugs. If the name is coming from somewhere else, show it and set it from props.
I'm not sure what updateLabel() does or where it comes from (how does the function know what to update the label to?), but if you need it, it should come from props.
If label.name is a string, you can't render it in a fragment. You must render it in a Text component. <Text>{label.name}</Text>
The object that FlatList passes in to the renderItem callback does not have a property called label, you are looking for item - this is the object from the data prop.
function renderLabel({ item }) { // item, not label
return <Label label={item} onPress={() => onPressLead(item)}/>;
}
const Label = ({ label, updateLabel }) => {
// no local label variable
useEffect(() => {
updateLabel(); // what is this supposed to do?
}, []); // no dependencies, if you only want to update the label once on mount
return <Text>{label.name}</Text>; // if label.name is a string
};
// your FlatList is fine as written
Your use effect probably needs the label as a dependency.
useEffect(() => {
updateLabelName()
}, [label]);

React Native console.log old value useState

I'm having trouble with React Native showing wrong value for me. I wan't to show the value after an useState update. My goal is to pass the value to the parent component but right now it passes the opposite value (true when switch is off). What do I have to do to console.log the right value after a useState update?
Watch image for example here
The useState hook is somewhat asynchronous (although you cannot wait for it).
Try using a useEffect:
useEffect(() => {
console.log(isEnabled)
}, [isEnabled]) // Array of dependencies: when any of these value changes, the function in the useEffect will re-run
More information here:
https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia
https://javascript.plainenglish.io/why-you-shouldnt-always-use-usestate-658994693018
The Change function will always "see" the state value that existed at the time of running the function. This is not because of asynchronicity per se (state updates are actually sync) but because of how closures work. It does feel like it is async though.
The state value will properly update in the background, but it won't be available in the "already-running" function. You can find more info here.
The way I see your handler implemented though:
const handleChange = () => {
setIsEnabled(!isEnabled) // you do not need updater function, you can directly reference the state
triggerParentMethod(!isEnabled); // then you can also directly call the parent function here
}
I recommend this as this way you will notify the parent immediately on user click instead of waiting for the state to be set and then notifying the parent in the next render cycle (in the effect), which should be unnecessary.
State updates in React are asynchronous, meaning that React does not wait for the state to be updated before executing the next line of code. In your case, the state update setIsEnabled(...) is not finished before console.log(isEnabled) is run, and therefore it returns the old value.
Just put the console.log(isEnabled) outside the function for it to print the update correctly. The component SetupSwitch is re-rendered when the state isEnabled is updated, which means it prints the console.log of the updated variable again.
...
console.log(isEnabled);
const Change = () => {
...
You will have to implement useEffect to view the changes.
useState is an asynchronous function it will go to the callback queue, meanwhile, the value will be consumed, so you need to trigger the action whenever the count changes. (for this example)
const [count, setCount] = useState(0);
useEffect(() => console.log(count), [count]);
setCurrPos(preevCount => prevCount + 1);

track UI elements states with one object, but the states are not reserved once leaving the screen and coming back

In my react-native project, I have three checkboxes, I need to track the state of those checkboxes so I use an object with key-value (value is boolean) to represent the states of all three checkboxes and use useState hook to manage them. Here is my code:
import React, { useState, useEffect } from 'react';
...
const MyScreen = ({ navigation }) => {
// initially, all checkboxes are checked
const initialCheckBoxState = {
0: true,
1: true,
2: true,
};
const [checkBoxesState, setCheckBoxesState] = useState(initialCheckBoxState);
useEffect(() => {
return () => {
console.log('Screen did unmount');
};
}, [checkBoxesState]);
return (
<View>
...
<SectionList
sections={options}
renderItem={({ index, item }) => (
<CheckBox
onPress={() => {
const checkBoxesStateCopy = { ...checkBoxesState };
checkBoxesStateCopy[index] = !checkBoxesStateCopy[index];
setCheckBoxesState(checkBoxesStateCopy);
}}
/>
)}
/>
...
</View>
);
};
I omitted code that is not the concern of my problem. As you can see, for each item I draw one CheckBox component.
In practice, there are always three items (i.e. three check boxes to show). At the beginning I declared initialCheckBoxState, each key-pair represents the state of the checkbox of each. In the onPress callback of Checkbox I toggle each check box state & update the checkBoxesState by hook method setCheckBoxesState as a whole.
Everything works fine at runtime, my screen is re-rendered when toggling checkbox state, UI shows the status of checkboxes correctly. But issue comes when I navigate back to the previous screen and navigate back to this screen, all checkboxes states are back to the initial states.
So, why the checkboxes states are not reserved?
P.S. previous screen and MyScreen are under the same stack navigator. User press a button of previous screen to navigate to MyScreen. From MyScreen user can go to previous screen by pressing the "headerLeft" button
First lets answer the question:
why the checkboxes states are not reserved?
This component is handling its state completely independent, the state is created & handled inside and no values are passed-in from outside. what does it mean? this component has its initial state value inside of itself, it doesn't use any prop or anything else to initialize the state. everytime this component gets created, state is again initialized with that value. so that's the reason you lose all changes done to checkboxes, because when you leave this screen(component) , it gets unmounted(we'll talk about this in next question) and because all values are just handled inside, every data (containing checkboxes state) will be lost.
So now lets talk about this:
is react-native supposed to reserve the state when come back to the screen?
short answer is No. Every component is destroyed when unmounted including their state and data.
Now lets answer why
screens are still on the stack in memory, not destroyed?
Usually developers use a package like react-navigation or RNRF(which is built on top of react-navigation) for react navigation, most of times we don't care about how they handle this navigation logic, we just use the interface the provided us. each of these packages may have their own way to handle navigation. providing full answer to determine why exactly the screen in still in memory needs full code review and sure lots of debugging but i guess there are 2 possibilities. first as i said maybe the package you are using keeps the unmounted screens in memory at least for a while for some reason. the 2nd is a common react community issue which is Unmounted component still in memory which you can check at: https://github.com/facebook/react/issues/16138
And at last lets answer the question:
how do i keep checkboxes state even with navigating back and losing component containing their state?
This doesn't have just one way to that but simple and short answer is move your state out of the that component, e.g move it out to the parent component or a global variable.
to make it more clear lets explain like this: imagine screen A is always mounted, then you go in B and there you can see some checkboxes and you can modify the states. if the state is handled completely inside B, if you navigate back from screen B to A you lose all changes because B is now unmounted. so what you should do it to put checkboxes states in A screen then pass the values down to B. and when modifying the values, you modify A state. so when B gets unmounted all changes are persistant because you have them in A.
other approached exists as well, you can create a global singleton object named globalState. then put values needed to share between multiple screens there. if you prefer redux or mobx you can use them. one of their usages is when you have some data that you need to share between mutiple screens, these data are independent from where you are at and will persist.
This explanation is from official react-navigation documentation:
Consider a stack navigator with screens A and B. After navigating to
A, its componentDidMount is called. When pushing B, its
componentDidMount is also called, but A remains mounted on the stack
and its componentWillUnmount is therefore not called.
When going back from B to A, componentWillUnmount of B is called, but
componentDidMount of A is not because A remained mounted the whole
time.
https://reactnavigation.org/docs/navigation-lifecycle/#example-scenario
Your MyScreen screen is equivalent to screen B from the example, which means you can expect your screen to stay mounted if you navigate forward, but not backwards.
Its simple, just add a keyExtractor to your SectionList component, which would uniquely identify each checkbox, so that react knows which one to re-render on update.
You'll want to use AsyncStorage to persist data to the device. State variables will be cleared any time the component unmounts.
AsyncStorage docs:
https://react-native-community.github.io/asaync-storage/
import AsyncStorage from '#react-native-community/async-storage';
//You can only store string values so convert objects to strings:
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('#storage_Key', jsonValue)
} catch (e) {
// saving error
}
}
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('#storage_Key')
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch(e) {
// error reading value
}
}
UPDATE -
State is not being persisted due to the nature of React Component lifecycles. Specifically, when you navigate away from a screen the lifecycle method componentWillUnmount is called.
Here's an excerpt from the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().
...Once a component instance is unmounted, it will never be mounted again.
This means any values stored in state will be destroyed as well and upon navigating back to the screen ComponentDidMount will be called which is where you may want to assign persisted values back to state.
Two possible approaches aside from AsyncStorage that may work for some use cases to persist data across screens is using Context or a singleton.

How to use Vue v-model binding in Vuetify components together with computed properties and Vuex?

I'm trying to use v-radio-group in conjunction with computed values from Vuex as described similarly here.
Example codepen of the issue I'm facing is here
Whenever a radio button is clicked, a Vuex mutation is called to save the selected value in the state.
However it can be the case that some validation fails inside the mutation and that therefore the value is not changed in the state as expected.
Regardless of what value ends up in the Vuex state, the radio buttons do not truly reflect the current state.
E.g. in the codepen snippet I'd expect the second option (Option 1) never to show as chosen, as the corresponding state is always 0.
As far as I can see this behavior is not only happening when using v-radio-groups.
It happens with all Vuetify components using v-model and computed getters/setters.
So e.g. Vuetifys v-text-input/v-text-field and v-select also show the same behavior.
To sum it up, my questions are the following:
Why is the second option in my codepen example getting selected even as the corresponding state is different?
How can I achieve the expected result (Having Option 1 never shown as selected, even when it is clicked)?
As far as I know Vuetify keeps its own state in their components like v-radio-group.
To change it you need to send updated props. Then it will react and update its own state.
The trouble is that you are performing validation in a mutation. Which is a bad practice in my opinion.
I will show you how to "block" changing state and update v-radio-group so its own state corresponds to what is actually in your $store.state.radioState.
And I will spend some more time to figure out how to performe it in on mutation ;-)
This is not a perfect solution >> my codepen
Your mutation just updates the state.
// store.js
mutations: {
setRadioState (state, data) {
state.radioState = data;
},
},
Your set method do the validation.
// component
computed: {
chosenOption: {
get () {
return this.$store.state.radioState;
},
set (value) {
if (value !== 1) {
this.$store.commit('setRadioState', value)
} else {
const oldValue = this.$store.state.radioState
this.$store.commit('setRadioState', value)
this.$nextTick(() => {
this.$store.commit('setRadioState', oldValue)
})
}
}
}
}
What happens in the set when it fails validation? You save current state to oldValue, you update state so it corresponds to v-radio-group component. And in the $nextTick you change it right back to oldValue. That way v-radio-group gets updated props and change its state to yours.

How can I work with just values in react-select onChange event and value prop?

I try to use react-select in my reactjs app, but I have a problem with the onChange event. onChange is supposed to send two arguments. The first is supposed to be the selected value, but instead of the selected value, the whole option item is passed as the selected value.
For instance
I have an array of option items like options=[{ id: '1', name: 'A'},{ id: '2', name:'B'}]
I set getOptionValue = (i) => i.id; and getOptionLabel = (i)=>i.name;
When select the second item onChange(value) is passed the second option as the value argument ({id:'2',name:'B'}) instead of the value of the second option ('2').
This behavior is inconsistent with most input components out there. I would expect onChange to be passed the value of the item, and for the item itself I would expect another event like onItemSelected or something like that.
Also, when I set value={'2'} (controlled component), the component doesn't show the selected item.
I must say that I use AsyncSelect with loadOptions.
How can I make it work with just simple values, instead of option objects?
If this can't happen I have to abandon react-select for another similar component.
AFAIK currently there's no way to make React-Select work internally with just the value. What I'm doing in my application is implementing a layer to retrieve the object going down, and extract the value going up. Something like this (this is simplified, you may need more validation or handling depending on your application):
const Select extends Component {
handleChange(newSelected) {
// this.props.handleChange is your own function that handle changes
// in the upper scope and receives only the value prop of the object
// (to store in state, call a service, etc)
this.props.handleChange(newSelected.value);
}
render() {
// Assuming you get the simple value as a prop from your store/upper
// scope, so we need to retrieve the option object. You can do this in
// getDerivedStateFromProps or wherever it suits you best
const { options, value } = this.props;
const selectedOption = options.find(option => option.value === value)
<ReactSelect
options={options}
value={selectedOption}
onChange={this.handleChange}
{...props}
/>
}