I'm building an app with React Native and Firebase Realtime Database, when I start the function addItem
the only parameter in the object that I get from the Database is the id
, the other paramters return as ''
, I've tried using console.log()
and the TextInputs work fine also I've used this configuration a lot of times and this is the first time it happens.
export default function NewItem() {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const [description, setDescription] = useState('');
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={addItem}>
<Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
</TouchableOpacity>
)
})
}, [])
const addItem = () => {
const changes = ref(db, 'path')
get(changes).then( async (snapshot) => {
if (snapshot.val().data !== undefined) {
let array = []
let object = {
"id": `${id}`,
"name": name,
"price": price,
"description": description,
}
array.push(object)
update(changes, {
data: array
})
}
})
}
return (
<TouchableWithoutFeedback>
<TextInput
onChangeText={(e) => setName(e)}
/>
<TextInput
onChangeText={(e) => setShippingPrice(e)}
/>
<TextInput
onChangeText={(e) => setPrice(e)}
/>
<TextInput
onChangeText={(e) => setDescription(e)}
/>
</TouchableWithoutFeedback>
)
}
CodePudding user response:
addItem
is a stale enclosure over the state it references since the useEffect
hook runs only once and closes over the initial state. You can likely resolve by re-enclosing the updated state by adding addItem
function to the dependency array.
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={addItem}>
<Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
</TouchableOpacity>
)
})
}, [addItem]);
You should memoize the addItem
callback handler to further reduce the number of times the useEffect
hook above re-encloses the callback/state.
const addItem = React.useCallback(() => {
const changes = ref(db, 'path');
get(changes)
.then(async (snapshot) => {
if (snapshot.val().data !== undefined) {
const data = [{
"id": `${id}`,
name,
price,
description,
}];
update(changes, { data });
}
})
}, [name, price, description]);
Alternatively you could cache all the state values in a React ref and access them via the ref in the callback.
Example:
export default function NewItem() {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const [description, setDescription] = useState('');
const stateRef = React.useRef({
name,
price,
description,
});
useEffect(() => {
stateRef.current = { name, price, description };
}, [name, price, description]);
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={addItem}>
<Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
</TouchableOpacity>
)
})
}, []);
const addItem = () => {
const changes = ref(db, 'path');
get(changes)
.then(async (snapshot) => {
if (snapshot.val().data !== undefined) {
const { name, price, description } = stateRef.current;
const data = [{
"id": `${id}`,
name,
price,
description,
}];
update(changes, { data });
}
})
}
return (...);
}
CodePudding user response:
As Drew Reese pointed out, this is the typical stale state issue in React, when you use JavaScript closures. useEffect takes a function as parameter, in that function you use another function addItem that is declared inside a React Component and uses React State. What happens in this case is that addItem gets memoized in the condition it is when useEffect is executed, it's like a static picture of it. Unfortunately addItem is not a pure function and it relies on React state, so it's gonna change during React lifecycle, while useEffect callback version of it is going to remain a static picture. This happens because you initialized useEffect hook with an empty array as a second parameter, [].
That's why React hooks introduce the dependencies array, the values inside that array determine when the callback inside the hook has to be re-evaluated. There's a very important Linting rule that forces you to put all the dependencies of your callback inside the deps array: @react-hooks/exhaustive-deps to avoid a lot of subtle bugs and misbehaviours.
This should be the correct way to use React hooks in your case, to improve performance and avoid bugs and unnecessary calculations on rerenders:
export default function NewItem() {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const [description, setDescription] = useState('');
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={addItem}>
<Text style={{fontWeight:'bold'}}>ADD ITEM</Text>
</TouchableOpacity>
)
})
}, [addItem])
const addItem = useCallback(() => {
const changes = ref(db, 'path')
get(changes).then( async (snapshot) => {
if (snapshot.val().data !== undefined) {
const array = []
const object = {
"id": `${id}`,
"name": name,
"price": price,
"description": description,
}
array.push(object)
update(changes, {
data: array
})
}
})
},[name, price, description ])
return (
<TouchableWithoutFeedback>
<TextInput
onChangeText={(e) => setName(e)}
/>
<TextInput
onChangeText={(e) => setShippingPrice(e)}
/>
<TextInput
onChangeText={(e) => setPrice(e)}
/>
<TextInput
onChangeText={(e) => setDescription(e)}
/>
</TouchableWithoutFeedback>
)
}