Below is my code. How do I get my potion count to update in real time while also avoiding the "More hooks than in previous render" error.
I know there is a way to do this, but I'm struggling to understand how it works. If someone could explain it well, that would be great because I will need this to happen alot in what I'm building.
import React, { useState } from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import CustomButton from './CustomButton';
import { useFonts } from 'expo-font';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import player from './Player';
import usePotion from './UsePotion';
import { useNavigation } from '@react-navigation/native';
function addPotion() {
player.potions ;
}
function BattleScreen() {
const [fontsLoaded, error] = useFonts({
'Valorax': require('./Fonts/Valorax.otf'),
});
const navigation = useNavigation()
const [potionMessage, setPotionMessage] = useState('');
if (!fontsLoaded) {
return <Text>Loading...</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.text}>{player.potions}</Text>
{potionMessage && <Text style={styles.text}>{potionMessage}</Text>}
<View style={styles.topHalf}>
</View>
<View style={styles.bottomHalf}>
<View style={styles.buttonRow}>
<CustomButton onPress={() => handleAttack()} title='Attack'></CustomButton>
<CustomButton onPress={() => handleMagic()} title='Magic'></CustomButton>
</View>
<View style={styles.buttonRow}>
<CustomButton onPress={() => usePotion(setPotionMessage)} title='Use Potion'></CustomButton>
<CustomButton onPress={() => handleRun()} title='Run'></CustomButton>
<CustomButton onPress={() => navigation.navigate('Home')} title="Home"></CustomButton>
<CustomButton onPress={() => addPotion()} title="Add Potion"></CustomButton>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000000',
},
topHalf: {
flex: 1,
color: 'white',
},
bottomHalf: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap'
},
buttonRow: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-evenly',
flexWrap: 'wrap'
},
text: {
fontSize: 40,
color: 'white',
fontFamily: "Valorax",
}
});
export default BattleScreen;
CodePudding user response:
I think that the errors you are seeing is caused by calling the usePotion
hook in your JSX. Hooks can only be called in the top level of other other hooks, or the top level of other components. I'm not sure what usePotion
does with setPotionMessage
, but it needs to be called at the top level of the component. If there's some event you want to be triggerable from usePotion
you need to return it from usePotion
:
// usePotion
export default function usePotion(onChange){
// I assume usePotion would be structured like this
const [potion,setPotion] = useState({
hp:20,
message:''
})
// im assuming usePotion have some internal data
// that you would like to setPotionMessage to
const getPotionMessage = ()=>{
// because you past setPotionMessage I assume that you
// you want to subscribe to potion message changes?
onChange?.(potion.message)
return potion.message
}
return { potion,getPotionMessage}
}
Now you have a hook that returns some state about potions, and allows you trigger other state to update:
// top level of component
const {potion,getPotionMessage} = usePotion(setPotionMessage)
.
.
.
<CustomButton onPress={getPotionMessage} title='Use Potion'/>
Finally you need to get player into react lifecycle. You could either convert Player.js into a hook, or you could you could put player into state:
// at the top level of the component
const [playerState,setPlayerState] = useState(player);
// maybe wrap addPotions in a useCallback?
const addPotion = ()=>{
setPlayerState(prev=>{
return {...prev,potions:prev.potions 1}
})
}
CodePudding user response:
In order solve the potion count not updating, the short answer is that you will have to utilize a useState
hook. The short reasoning for this is because react will only rerender when state is changed and it is smart enough to only update the components that have been impacted by the state change.
An example of this in your code is when the setPotionMessage
method is called and the potionMessage
value is updated, the only update react makes is to the JSX {potionMessage && <Text style={styles.text}>{potionMessage}</Text>}
.
The long reason for why this data needs to be stored in state is the following:
When react initially renders, it compiles a DOM tree of all of your returned components. This DOM tree will look pretty similar to the JSX you have written, with additional parent tags and other stuff. When and only when there has been a change in state, React will compile a new DOM tree, called the Virtual DOM
with the newly changed data. Then it will compare the existing DOM tree to the new Virtual DOM tree to find the components that have changed. Finally, react will update the original DOM tree but it will only update those components that have changed.
Continuing with the potionMessage
example I used above. When BattleScreen
initially renders, the Virtual DOM will render the JSX like this (pulled from the return
statement):
<View>
<Text>3</Text>
<View></View>
...
</View>
However, once the setPotionMessage
is called and the potionMessage
value changes from ''
to a message, react recompiles the Virtual DOM to determine what has changed. It will recompile to something like this:
<View>
<Text>3</Text>
<View>
<Text>You really need a potion!</Text>
</View>
...
</View>
Then it compares the original DOM tree with the Virtual DOM tree to figure out what is different. When it finds what is different, it rerenders only the part of the tree that has changed on the actual DOM tree.
<View>
<Text>3</Text>
<View>
// ONLY this block is rerendered on the DOM
<Text>You really need a potion!</Text>
// ---
</View>
...
</View>
In terms of the code modifications needed, PhantomSpooks outlined the required changes. As they mentioned, getting the player into the react lifecycle will be key.