I'm building an app using expo and react native. In my project, I have a stackNavigator containing three stackScreen and I also have a DatabaseManager class with static async function. In my components, I use useState and useEffect to load my data and display them.
When I move from the first Screen to the second one, I got an error :
Warning: Cannot update a component (%s) while rendering a different component (%s). To locate the bad setState() call inside %s, follow the stack trace as described in https://reactjs.org/link/setstate-in-render%s, StackNavigator, Screen, Screen.
This warning is, I suppose the reason why I need to tap 2 times on the screen to navigate between screens sometimes.
Here's a part of my code:
export default class DatabaseManager {
...
static async getSplits(){
if (!this.db)
await this.openDatabase();
const query = "select distinct day from exercises";
const resultQuery = await this.executeSql(query, []);
const result = [];
var rows = resultQuery.rows;
for (let i = 0; i < rows.length; i++) {
var item = rows.item(i);
result.push({split: item.DAY});
}
return result;
}
static async getExercises( split){
if (!this.db)
await this.openDatabase();
const query = "select distinct name from exercises where day = ? order by number";
const resultQuery = await this.executeSql(query, [split]);
const result = [];
var rows = resultQuery.rows;
for (let i = 0; i < rows.length; i++) {
var item = rows.item(i);
result.push({name: item.NAME});
}
return result
}
}
App.js
//...
function HomeStackScreen() {
return (
<HomeStack.Navigator initialRouteName='Home' >
<HomeStack.Screen name="Home" component={Home} options={{title:'Splits'} }/>
<HomeStack.Screen name="Screen" component={Screen} />
</HomeStack.Navigator>
);
}
...
export default function Home ({navigation}){
const [splits, setSplits] = useState([{ split:''}]);
const handlePress = async (split) => {
navigation.navigate('Screen', {split: split});
};
useEffect(() => {
const dataFetch = async () => {
const data = await DatabaseManager.getSplits();
setSplits(data);
}
dataFetch();
}, [])
return (
<View style={styleHome.container}>
<ScrollView style={styleHome.scrollViewContainer}>
{splits.map((split, index) => (
<View style={styleHome.touchableContainer} key={index}>
<Pressable
key={split.split}
style={styleHome.touchable}
activeOpacity={1}
onPress={() => handlePress(split.split)}
>
<Text style={styleHome.textTouchable}>{split.split}</Text>
</Pressable>
</View>
))}
</ScrollView>
</View>
);
The second screen is similar to this.
I tried to use useFocusEffect, enableFreeze and useFocus. I also tried to put a Loading Screen before navigating to a new screen, nothing solve the issue. By using some console.log, I found out that my code seems to be correct, the Home component doesn't re render after navigating to the other component.
I found out the solution: I just had a bad navigation.setOptions() in the second screen ...
I wrapped it in the useEffect() and it works fine.
I found the solution there: https://github.com/react-navigation/react-navigation/issues/9478#issuecomment-813031134
Related
I have a component in my React Native app that displays a list of pending friends. This component makes a GET request to an API to retrieve the list of pending friends and then uses a useEffect hook to map over the list and render each friend as a Pressable component. I'm also using the useFocusEffect hook to make the get request when the screen renders.
Here is the relevant code for the component:
const Pending = () => {
const [pendingFriends, setPendingFriends] = useState(null)
let pendingFriendsRender = []
useEffect(() => {
if (pendingFriends !== null) {
for(let i = 0; i < pendingFriends.length; i++) {
pendingFriendsRender.push(
<Pressable key={i} style={styles.friend}>
<Text style={styles.friendText}>{pendingFriends[i].username}</Text>
</Pressable>
)
}
}
}, [pendingFriends])
useFocusEffect(
useCallback(() => {
async function fetchData() {
const accessToken = await AsyncStorage.getItem('accessToken')
try {
const res = await instance.get('/pending_friends', {
headers: { authorization: `Bearer ${accessToken}`},
})
setPendingFriends(res.data)
} catch (error) {
console.log(error.response.status)
}
}
fetchData()
}, [])
)
return(
<View style={styles.friendsContainer}>
{pendingFriendsRender}
</View>
)
}
I have tried using an empty array as the second argument in the useEffect hook but that approach has not worked. I also tried removing the useEffect hook so the if statement with the for loop stands at the top of the component without the hook, that worked but I can't update it in this way after the component rendered. I checked the API and it is returning the correct data.
The first useEffect you have really isn't needed. You can map through your state inside of your JSX. Anytime the state changes, the component will be re-rendered:
// Need a default here, could also set some loading state when fetching your data
if(pendingFriends === null) {
return <>Loading...</>
}
return(
<View style={styles.friendsContainer}>
{pendingFriends.map((friend, i) => {
return (
<Pressable key={friend.id} style={styles.friend}>
<Text style={styles.friendText}>{friend.username}</Text>
</Pressable>
)
})}
</View>
)
Also keep in mind, it's not recommended to use the index as the key, it can lead to unexpected bugs and issues. Instead use a unique string key (as shown above).
React: using index as key for items in the list
pendingFriendsRender should be the state:
const [pendingFriendsRender, setPendingFriendsRender] = useState([])
Instead of
let pendingFriendsRender = []
Then just clone the array so you lose reference to the object and add the new element
const newPendingFriendsRender = [...pendingFriendsRender, newElement]
or you can use FlatList to make it easier.
I have a a Header there is a text input and I have a Main component there are the list of products of the searched text.
So If the input text is empty then I want to show him his last searched things. If he type anything then I want to show him the products with the same name as the input text. I make a condition like this:
const SearchRoot = ({ }: ISearchRoot) => {
const searchParam = useSelector((state: RootState) => state.Search.searchParam);
return (
<>
{ searchParam.length > 0 ?
<SearchList />
:
<LatestHistory />
}
</>
)
}
So if I type anything then my input are closing automatically. But if I press again the keyboard and typing then its not closing and working. So its only happend when the component changing from <LatestHistory to <SearchList only one time. So how can I make my keyboard always open when the component is chaning ?
Search
const Search = () => {
return (
<View style={s.container}>
<StatusBar backgroundColor='#fff' />
<SearchHeader />
<SearchRoot />
</View>
)
}
SearchHeader
const SearchHeader = () => {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParams>>();
const dispatch = useDispatch();
// filters
const [searchText, setSearchText] = useState<string>('');
const handleChangeText = (e: string) => {
dispatch(setSearch({searchParam: e}));
setSearchText(e)
};
const handleClearText = () => {
dispatch(setSearch({searchParam: ''}));
setSearchText('');
};
const handlePressSeach = () => {
searchText.length > 0 &&
navigation.navigate('Searched', {
searchText,
searchType: tabType
});
};
const handleGoBack = () => navigation.goBack();
return (
<View style={s.container}>
<View style={s.header}>
<View style={s.backContainer}>
<GoBackIcon
onPress={handleGoBack}
color='#555'
/>
</View>
<View style={s.inputContainer}>
<SearchInput
value={searchText}
onChangeText={handleChangeText}
onPressSearched={handlePressSeach}
onPressClearTextField={handleClearText}
autoFocus={true}
style={s.sInput}
/>
</View>
</View>
</View>
)
}
SearchRoot
const SearchRoot = ({ }: ISearchRoot) => {
const searchParam = useSelector((state: RootState) => state.Search.searchParam);
return (
<>
{ searchParam.length > 0 ?
<SearchMain />
:
<Text>Vorschläge</Text>
}
</>
)
}
I am very thankful for your help!!
I just read your problem statement and matched it with your Code.
✦First of all your sequence is wrong.
✦First Comes the Previous Searched Result.
✦Then when you press it then it should clear the previous search result.
✦Then On text change Property
✦Then OnPress Searched
Maybe that's Why it closes. If that's Not the case then!
(In my Opinion)
when then you click the search bar the previously searched result disappears and this is done by the function handleClearText. Now When the Function Completes it's Functionality the search bar closes. The Second time you open the search bar it works as their's no Previous Search result to clear.
So the Problem is with the handleClearText function.
My Proposed Solution in case 2 is
We can call the search method again (at the end) of handleClearText function.
then it will re open the search bar with the new state.
I hope it resolves your issue, Regards...
first of all const [searchText, setSearchText] = useState(''); this whole logic move to parent class search and from there pass it to siblings to avoid any confusion.whenever re-rendering happens values will be passed correctly to child.I think somewhere that is going wrong.Either use state and prop drilling or store, dont mix it up.
If I'm using React Navigation v5, what is the best way to pass the current state of a parent component (in my case, the main App) down through a Tab and Stack navigator to a screen that I'd like to use the current state in?
Following the documentation, I have created a stack navigator for each tab that holds the respective screens.
App.js contains a state that needs to be used for a few things. Most importantly, it will provide badge count on the Tab navigator, as well as be a source of Flatlist data on one of the tab screens.
What is the correct approach to getting the state from App all the way down to a child component in a stack navigator in a tab navigator?
App.js
const Tab = createBottomTabNavigator()
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
neededArray: []
}
}
const updateTheArray = (newArray) => {
this.setState({
neededArray: newArray
})
}
componentDidMount(){
//Listener that searches for nearby bluetooth beacons and updates the array with the passed function
startObserver(updateTheArray)
}
componentWillUnmount(){
stopObserver()
}
render(){
return(
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen
name = "Home"
component = { HomeStack }/>
<Tab.Screen
name = "About"
component = { AboutStack }/>
//The Stack that contains the screen that I need to use the App's state in
<Tab.Screen
name = "Nearby"
component = { NearbyStack }/>
</Tab.Navigator>
</NavigationContainer>
)
}
}
NearbyStack.js
//This stack holds the screen that I need to use the App's state in
const NearbyStackNav = createStackNav()
const NearbyStack = () => {
return(
<NearbyStackNav.Navigator>
<NearbyStackNav.Screen
name = "Nearby"
component = { NearbyScreen }
/>
</NearbyStackNav.Navigator>
)
}
NearbyScreen.js
//The screen that I want to use the App's state in
const NearbyScreen = () => {
return(
<View>
<FlatList
//Where I would like to use the App's state
/>
</View>
)
}
You can pass some initial params to a screen. If you didn't specify any params when navigating to this screen, the initial params will be used. They are also shallow merged with any params that you pass. Initial params can be specified with an initialParams prop:
Usage
<Tab.Screen
name = "Nearby"
component = { NearbyStack }
initialParams={{ arrayItem: this.state.neededArray }}
/>
NearbyScreen.js
React.useEffect(() => {
if (route.params?.arrayItem) {
// Post updated, do something with `route.params.arrayItem`
// For example, send the arrayItem to the server
}
}, [route.params?.arrayItem]);
My solution was to use React's Context API.
BeaconContext.js - New
import React from 'react'
const BeaconContext = React.createContext()
export default BeaconContext
App.js - Modified
import BeaconContext from './path/to/BeaconContext'
const Tab = createBottomTabNavigator()
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
neededArray: []
}
}
const updateTheArray = (newArray) => {
this.setState({
neededArray: newArray
})
}
componentDidMount(){
startObserver(updateTheArray)
}
componentWillUnmount(){
stopObserver()
}
render(){
return(
// Wrap the nav container in the newly created context!!!
<BeaconContext.Provider value = { this.state.neededArray }
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen
name = "Home"
component = { HomeStack }/>
<Tab.Screen
name = "About"
component = { AboutStack }/>
<Tab.Screen
name = "Nearby"
component = { NearbyStack }/>
</Tab.Navigator>
</NavigationContainer>
</BeaconContext.Provider>
)
}
}
NearbyStack.js - Unchanged
const NearbyStackNav = createStackNav()
const NearbyStack = () => {
return(
<NearbyStackNav.Navigator>
<NearbyStackNav.Screen
name = "Nearby"
component = { NearbyScreen }
/>
</NearbyStackNav.Navigator>
)
}
NearbyScreen.js - Modified
import BeaconContext from './path/to/BeaconContext'
const NearbyScreen = () => {
return(
<View>
//Wrap the component in the new context's consumer!!!
<BeaconContext.Consumer>
{
context => <Text>{ context }</Text>
}
</BeaconContext.Consumer>
</View>
)
}
I've been struggling with the exact same issue - when using the initialProps property to pass a state to a Tab.Screen the screen never receives any updates. It reads the intial state value once then nothing.
To make it work I skipped using the initialProps property and instead used the children property on Tab.Screen like so:
App containing <Tab.Navigator> and <Tab.Screen>:
const[myBool, setMyBool] = useState(false)
<Tab.Screen
name="MyTab"
children={() => (
<MySecondScreen passedStateParam={ myBool } />
)}
.
.
.
</Tab.Screen>
MySecondScreen consuming updates on passed myBool state:
export function MySecondScreen ({ passedStateParam }) {
const myPassedBoolState = passedStateParam
React.useEffect(() => {
if(myPassedBoolState) {
//Act upon App.tsx updating the state
}
}, [myPassedBoolState])
}
Not sure if I'm missing something when trying to perform this with the initialParams property but this way (using children property) I got it to work at least.
I'm writing a React Native app and I'm using React Navigation (V2) with it. I want to update the navigationOptions and add a new button, after my component has updated. Here is the code with which I tried it:
static navigationOptions = ({ navigation }) => {
const options = {
headerTitle: SCREEN_TEXT_MENU_HEADER,
headerStyle: {
borderBottomWidth: 0,
marginBottom: -5
}
};
if (navigation.getParam("drawer", true)) {
options["headerLeft"] = (
<HeaderIconButton
onClick={() => {
navigation.openDrawer();
}}
icon={require("../../assets/icons/burgerMenu.png")}
/>
);
}
if (navigation.getParam("renderBillButton", false)) {
options["headerRight"] = (
<HeaderIconButton
onClick={() => {
navigation.navigate("BillScreen");
}}
type="primary"
icon={require("../../assets/icons/euro.png")}
/>
);
}
return options;
};
componentDidUpdate = prevProps => {
const { navigation, orders } = this.props;
if (prevProps.orders.length !== orders.length) {
navigation.setParams({
renderBillButton: orders.length > 0
});
}
};
The problem with this approach is, that the navigationOptions do not get reset after componentDidUpdate(). How can I dynamically adjust the header with React Navigation?
You can use this.props.navigation.setParams() function to update the navigation state params.
Reference: https://reactnavigation.org/docs/en/headers.html#updating-navigationoptions-with-setparams
Okay here is what went wrong: I also had to call the same code within componentDidMount(), otherwise it would not affect the page upon loading. So in addition to the code of my question I added:
componentDidMount = () => {
const { navigation, order } = this.props;
navigation.setParams({
renderBillButton: orders.length > 0
});
}
I am working with react native.
I have component listing by using
And, when the state to give data to update the list change. It won't update immediately. It take few seconds to re-render.
so, how can I update the component immeidately
//Listcomponent
const ListGlossary = ({glossaries, onPressGlossary, navigation, searchField}) => {
return (
<FlatList
data={glossaries}
keyExtractor={(item) => item.key}
renderItem={({item}) =>
<TouchableHighlight
onPress = {() => navigation.navigate('DetailGlossaryScreen', { searchField: searchField, word: item.word, translate: item.translate})}>
<ListItem
key={`${item.key}`}
title={`${item.word}`}
/>
</TouchableHighlight>
}
/>
}
//And you can find here the home screen component
class HomeScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
glossaries: [],
searchField: '',
}
}
componentDidMount () {
Promise.resolve().then(() => {this.setState({glossaries: JSONDataFromFile, isLoading: false})})
}
onSearchChange = (inputText) => {
this.setState({searchField: inputText});
}
render(){
return(
let filteredWords = []
if(this.state.searchField != null) {
let searchField = this.state.searchField.toLowerCase(),
glossaries = this.state.glossaries;
for(let i = 0, l = glossaries.length; i < l; ++i) {
if(glossaries[i].word.toLowerCase().indexOf(searchField) === 0){
filteredWords.push(glossaries[i]);
}
}
}
{this.state.isLoading ?
<View style={{flex: 1, paddingTop: 20}}>
<ActivityIndicator />
</View>
:
<ListGlossary
navigation = {this.props.navigation}
glossaries = {filteredWords}
onPressGlossary={this.onPressGlossary}
searchField = {this.state.searchField}
/>
}
)
}
Please show the whole component, and give the length of the list.
--- Edit
I suspect you're doing too much work in the render function. You're filtering every time it gets called, and since you're passing in the navigation prop (I assume you're using React-Navigation), it'll get called frequently. If you're using a stack navigator, all the other screens are also getting re-rendered every time you navigate to a new screen. Avoid passing navigation as much as possible, or use a HOC composition to ignore it.
You probably don't need to be filtering glossaries every time the user changes the search value. Use the shouldComponentUpdate lifecycle method.