Managing state in react-native when using react-navigation - react-native

I am trying to create a to-do app in react-native. It has some some extra functionality such as editing existing tasksand viewing task history. I have a two StackNavigators nested inside a TabNavigator - but I'm struggling to find out exactly how I should be passing state into my application, and then once the state/data is available with the app, how I should then update the application state.
Here is the relevant code for my App:
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
data: [
//Array of to-do objects here.
]
}
}
updateAppData(id, ){
//define function that wraps this.setState(). This will be passed into
application to update application state.
}
render(){
return(
<Root screenProps={this.state}/>
)
}
}
//make Todos Stack-Nav section to sit inside tab1
const TodosWrapper = StackNavigator({
CurrentTodos: { screen: CurrentTodos },
Details: { screen: Details},
EditTodo: { screen: EditTodo},
AddNewTodo: { screen: AddNewTodo}
})
//make History Stack-Nav section to sit inside tab2
const HistoryWrapper = StackNavigator({
CompletedTodos: { screen: CompletedTodos },
CompletedDetails: { screen: CompletedDetails }
})
//Make main tab navigation that wraps tabs 1 and 2
const Root = TabNavigator({
Todos : {
screen: TodosWrapper,
title: "Open Tasks"
},
History: { screen: HistoryWrapper }
})
I then want to pass the state of my App into my screens, such as the one below:
export default class CurrentTodos extends React.Component {
static navigationOptions = {
header: null
}
render(){
const { navigate } = this.props.navigation;
let { data } = this.props.screenProps;
data = data.map( (item, id) => (
<ListItem style={styles.listItem} key={"_"+id}>
<View style={styles.textWrapper}>
<Text style={styles.listItemTitle}>{item.title}</Text>
<Text note>{ item.isComplete ? "Complete" : "In Progress" }</Text>
<Text note>{item.description}</Text>
</View>
<View>
<Button
onPress={() => this.props.navigation.navigate('Details')}
title="Full Details"
style={styles.fullDetails}
/>
</View>
</ListItem>
)
);
return (
<ScrollView style={styles.scrollView}>
<View style={styles.viewHeader}>
<Text style={styles.title}>
Current Tasks
</Text>
<Text
style={styles.addNew}
onPress={() => navigate("AddNewTodo")}
>
+
</Text>
</View>
<List>
{data}
</List>
</ScrollView>
)
}
}
I am currently planning to pass the state into my screens using screenProps={this.state} and then also passing a function that wraps this.setState() so that screen components can update the app's state.
As I am a newbie to all this, I want to make sure I don't pick up poor practices.
Could anyone help me out as to whether or not this is the correct approach? The huge amount of documentation is overwhelming at times! Thanks in advance!

If you want a centralized state, you should definitely give redux a try. Redux is a global immutable state manager.
Summarizing, in redux all your state is stored into a "Store", which wraps your top navigator. This store is composed by many "Reducers" which are the ones holding the current state of your application. Your Components communicate with the store using actions, which are received by the reducers and those communicate the new state to your views and handle the required changes.
Tell me if you need me to add more details. And here you have some references for you to check:
Official Site: http://redux.js.org
Tutorials from the creator: https://egghead.io
Courses: https://www.udemy.com/the-complete-react-native-and-redux-course/
This should get you in the right direction!
Good luck!

Related

How to navigate to other screen using the headerRight on the navigator code?

The title is very confusing, but the explanation that I will give will be more clear.
I am creating one StackNavigator for my app, and I am defining one icon to be displayed on the header of one of my screens like so:
const navigator= createStackNavigator(
{
Initial: {
screen: Posts,
navigationOptions: {
title: "All Posts",
headerRight: () => {
return (
<TouchableOpacity style={{ padding: 5 }}>
<Feather name={"plus"} size={30} />
</TouchableOpacity>
);
},
},
},
Details: Details,
Create: Create,
},
{
initialRouteName: "Initial",
}
);
const App = createAppContainer(navigator);
export default () => {
return (
<Provider>
<App />
</Provider>
);
};
The problem is that I want to navigate the user to the Create screen when the user presses the icon that I am rendering on the right side of the header, but I do not know how to have access to the navigation.navigate() function that the navigator generates for all the screens. I know that I can define the header on the Posts' screen file, so I have access to the navigation.navigate() function, but I want to know if there is some way to do it on the App.js file.
EDIT
Reading the documentation I saw that the way that I am was creating the navigator is not what the official documentation recommends. I learned to make it like this by one old course, using React Navigation 4.x, and now with React Navigation 6.x I perceived the difference in the creation and changed the way that I was doing it on my app. You can check the documentation for the problem that I was having here
You can prepare your navigation option this way by sending props
options={(props) => ({
headerRight: () => <TouchableOpacity
onPress={()=>props.navigation.navigate('Screen')} style={{ padding: 5 }}>
<Feather name={"plus"} size={30} />
</TouchableOpacity>
})}

Cannot display firestore info in react native view

I am trying ti display basic profile data from firestore on a profile page, but the information will not show up in the view. My code is below: I create a constructor with the state ethnicity
var db = firebase.firestore();
export default class Dashboard extends Component {
constructor() {
super();
this.state = {
displayName: firebase.auth().currentUser.displayName,
uid: firebase.auth().currentUser.uid,
ethnicity: ''
}
}
And I then update the state with componentDidMount() as shown below and then try to display it in view
componentDidMount() {
db.collection("profiles").doc(this.state.uid).get()
.then(doc => {
this.state.ethnicity= doc.data().ethnicity.toString();
console.log(this.state.ethnicity)
})
.catch(function(error) {
console.log("Error getting document:", error);
})
}
render() {
return (
<View style={styles.container}>
<Text style = {styles.textStyle}>
Mixee!
</Text>
<Text style = {styles.textStyle}>
Hello, {this.state.displayName}
</Text>
<Text style = {styles.textStyle}>
Profile:
</Text>
<Text style = {styles.textStyle}>
Ethnicity: {this.state.ethnicity}
</Text>
<Button
color="#3740FE"
title="Logout"
onPress={() => this.signOut()}
/>
</View>
);
}
The console.log(this.state.ethnicity) correctly prints out the ethnicity so I do not know why it isn't showing up. Any help appreciated thanks!
It looks like you are modifying state directly, with this.state.ethnicity = doc.data().ethnicity.toString(); This might not cause a re-render, so your component does not update with the new state. Instead, try:
this.setState({
ethnicity: doc.data().ethnicity.toString()
});
Calling setState will let React Native know that the state has changed, and that it should call render again, as explained here.

Null is not an object while creating navigation between screens in expo project

I need to open new screen after clicking on the button. For it I made the following steps:
1) Installed this library
2) Created a new screen and added it to the folder with other screens (DetailInfoScreen is a new screen, which should be opened and HomeScreen is a screen, where a button, after clicking on which new screen should be opened):
3) Added the following lines of code:
import { Navigation } from 'react-native-navigation';
import DetailInfoScreen from './DetailInfoScreen';
class HomeScreen extends Component {
constructor(props) {
super(props);
this.onPressSearch = this.onPressSearch.bind(this);
Navigation.registerComponent('DetailInfoScreen', () => DetailInfoScreen);
}
goToScreen = (screenName) => {
Navigation.push(this.props.componentId, {
component: {
name: screenName
}
});
}
render() {
const { list, text } = this.props;
return (
<View style={styles.container}>
<View style={styles.searchContainer}>
<TouchableOpacity
onPress={this.goToScreen('DetailInfoScreen')}
>
<View>
<Text>Search</Text>
</View>
</TouchableOpacity>
</View>
);
}
But when I run the project I have the following error:
And one more moment which disturbs me is that autocorrection in vscode doesn't see my new screen while importing:
Maybe it doesn't play any role, but still. So, what's the reason of the problem and how can I solve it?
You can simply navigate to another screen by using this:
<TouchableOpacity
onPress={() => this.props.navigation.navigate('DetailInfoScreen')>
<Text>Search</Text>
</TouchableOpacity>
I would personally advice for you to use react-navigation instead of react-native-navigation, you can read more on this link

React native updates state "on its own"

I have two screens, one list (Flatlist) and one filter screen where I want to be able to set some filters for the list. the list screen has the states "data" and "usedFilters". When I am switching to the filters screen, the states are set as navigation parameters for react navigation and then passed via navigation.navigate, together with the onChange function, as props to the filter screen. There they are read, and the filters screen class' state is set (usually with passed filters from the list screen, if no valid filters has been passed, some are initialized).
After that the filters can be changed. If that happens, the state of the filter screen gets updated.
If then the apply button is clicked the filter screens' state is passed to the onChange function and via that back to the list screen, the onChange function updates the state "usedFilters" state of the list screen. If the cancel button is pressed null is passed to the onChange function and there is no setState call.
Setting new states for the list screen works perfectly fine. the problem is, that when i press the cancel button (or the back button automatically rendered by react navigation) the changes are kept nevertheless. That only happens if the state has been changed before. So if there has never been applied a change and hence the "usedFitlers" state of the list screen is null, this behavior does not occur. Only if I already made some changes and hence the "usedFitlers" state of the list screen has a valid value which is passed to the filters screen the cancel or go back buttons won't work as expected.
I am using expo-cli 3 and tried on my android smartphone as well as the iOS simulator. Same behavior. I looked into it with chrome dev tools as well but i simply couldn't figure out where the "usedFitlers" state was updated.
I am using react native 0.60 and react navigation 3.11.0
My best guess is that for some reason the two states share the same memory or one is pointer to the other or sth like that. (Had problems like that with python some time ago, not knowing the it uses pointers when assigning variables).
Anyone got an idea?
List Screen:
export default class ListScreen extends React.Component {
state = { data: [], usedFilters: null };
static navigationOptions = ({ navigation }) => {
let data = navigation.getParam('data')
let changefilter = navigation.getParam('changeFilter')
let currfilter = navigation.getParam('currFilter')
return {
headerTitle:
<Text style={Styles.headerTitle}>{strings('List')}</Text>,
headerRight: (
<TouchableOpacity
onPress={() => navigation.navigate('FilterScreen', {
dataset: data, onChange: changefilter, activeFilters:
currfilter })} >
<View paddingRight={16}>
<Icon name="settings" size={24} color=
{Colors.headerTintColor} />
</View>
</TouchableOpacity>
),
};
};
_onChangeFilter = (newFilter) => {
if (newFilter) {
this.setState({ usedFilters: newFilter })
this.props.navigation.setParams({ currFilter: newFilter });
} // added for debugging reasons
else {
this.forceUpdate();
let a = this.state.usedFilters;
}
}
_fetchData() {
this.setState({ data: fakedata.results },
() => this.props.navigation.setParams({ data: fakedata.results,
changeFilter: this._onChangeFilter }));
}
componentDidMount() {
this._fetchData();
}
render() {
return (
<ScrollView>
<FlatList/>
// Just data rendering, no problems here
</ScrollView>
);
}
}
Filter Screen:
export default class FilterScreen extends React.Component {
static navigationOptions = () => {
return {
headerTitle: <Text style={Styles.headerTitle}> {strings('filter')}
</Text>
};
};
state = { currentFilters: null }
_onChange = (filter, idx) => {
let tmp = this.state.currentFilters;
tmp[idx] = filter;
this.setState({ currentFilters: tmp })
}
_initFilterElems() {
const filters = this.props.navigation.getParam('activeFilters');
const dataset = this.props.navigation.getParam('dataset');
let filterA = [];
let filterB = [];
let filterC = [];
if (filters) {
// so some checks
} else {
// init filters
}
const filterElements = [filterA, filterB, filterC];
this.setState({ currentFilters: filterElements })
}
componentDidMount() {
this._initFilterElems()
}
render() {
const onChange = this.props.navigation.getParam('onChange');
return (
<ScrollView style={Styles.screenView}>
<FlatList
data={this.state.currentFilters} // Listeneinträge
keyExtractor={(item, index) => 'key' + index}
renderItem={({ item, index }) => (
<FilterCategory filter={item} name={filterNames[index]}
idx={index} onChange={this._onChange} />
)}
ItemSeparatorComponent={() => <View style=
{Styles.listSeperator} />}
/>
<View style={Layout.twoHorizontalButtons}>
<TouchableOpacity onPress={() => {
onChange(this.state.currentFilters);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('apply')} </Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
onChange(null);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('cancel')}
</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView >
);
}
}
So when I press the cancel button, null is returned to the _onChangeFilter function of the list screen. This part works, and according to console.log and the debugger, the setState is not called. But if i set a breakpoint within the else part, i can see that this.state.usedFilters has changed.
Ok after a while i figured it out. The problem was that the whole filters list was always just referenced since react native (js) seems to always use references, even when changing sub-parts of the lists.
fixed that by using lodash cloneDeep.

Setting NavigationBar's title asynchronously

I am trying to set a NavigationBar for my Navigator. When I display a static title, it is OK. However, when I try to set title asynchronously (for example when I go to user's profile route, I get user's display name from API and set this.props.navigation.title when API call promise resolves) the title gets jumpy.
What would be the proper approach for this issue?
Here is my component (which is connected to redux store) that handles NavigationBar:
import React from 'react-native';
let {
Component,
Navigator,
View
} = React;
import {connect} from 'react-redux';
let NavigationBarRouteMapper = {
Title: (route, navigator, index, navState) => {
return (
<Text style={{marginTop: 15, fontSize: 18, color: colors.white}}>{this.props.navigation.title}</Text>
);
}
};
class App extends Component {
render() {
return (
<View style={{flex: 1}}>
<Navigator
ref={'navigator'}
configureScene={...}
navigationBar={
<Navigator.NavigationBar routeMapper={ NavigationBarRouteMapper } />
}
renderScene={(route, navigator) => {...}}
/>
</View>
);
}
}
export default connect(state => state)(App);
Since routeMapper is a stateless object and route seems to be the only way (best practice) to keep the state of NavigationBar.
route can be set with this.props.navigator.replace(newRoute); in child components. However, this will re-renderScene and will sometimes cause dead loop of reRendering child components. What we want is to change the NavigationBar itself without side effects.
Luckily, there's a way to keep the context of current route. Like this:
var route = this.props.navigator.navigationContext.currentRoute;
route.headerItems = {title: "Message"};
this.props.navigator.replace(route);
Refs:
How to change the title of the NavigatorIOS without changing the route in React Native
[Navigator] Binding the navigation bar with the underlying scene