I am working on a react native app that uses Composite Experimental Navigation (CardStack + Tabs) and redux for state management. I was able to create Tab based Navigation but the issue that i am facing now is when i switch between tabs the component is unmounted and it re-render every time.
Problems
Lets say i have scrolled down several posts and when i change Tab it will start from top. (Workaround could be to store scroll position in redux state).
Here is the sample code i am using for Navigation Tabbed Experimental Navigation
You must change approach for TabBar.
Basically you want Navigator per Tab, so you can have route stack for each Tab and one Navigator to contain Tabs (TabBar). You also want to set initial route stack for TabBar and jump between those routes.
It is important to understand difference between Navigator methods.
When you pop route, it's unmounted, and active index is moved to
last one. As last one is kept in state it will be restored as it was
previously rendered (most likely, it can happen props changed). Going again to popped route will rerender scene entirely (this happens to you).
When you push route, nothing is unmounted, new route is mounted.
When you jumpToIndex route, again nothing in unmounted, thus jumping
between routes restores scenes as they were (again, if props changed
scene will be rerendered).
So, I don't think this is correct:
I was able to create Tab based Navigation but the issue that i am facing now is when i switch between tabs the component is unmounted and it re-render every time.
... you unmount routes with wrong navigation actions.
Also what is different now, NavigationCardStack doesn't actual create it's own state, it is passed from outside, which gives you great flexibility. And also good thing is that you can use reducers provided by Facebook for common actions (such as push, pop, jumpToIndex; they are part of Navigation Utils).
You have full example on how to create navigationState and it reducers here, so I am not going to explain that, just going to give idea how to solve your problem.
[UPDATE] Example works now!
import React from 'react';
import { NavigationExperimental, View, Text, StyleSheet } from 'react-native';
const {
CardStack: NavigationCardStack,
StateUtils: NavigationStateUtils,
} = NavigationExperimental;
const style = StyleSheet.create({
screen: {
flex: 1,
},
screenTitle: {
marginTop: 50,
fontSize: 18,
},
pushNewScreenLabel: {
marginVertical: 10,
fontSize: 15,
fontWeight: "bold",
},
goBackLabel: {
fontSize: 15,
},
tabBarWrapper: {
position: 'absolute',
height: 50,
bottom: 0,
left: 0,
right: 0,
top: null,
backgroundColor: 'grey',
flexDirection: 'row',
flex: 0,
alignItems: 'stretch',
},
tabBarItem: {
flex: 1,
justifyContent: 'center',
backgroundColor: 'red',
},
});
export class TabBar extends React.Component {
constructor(props, context) {
super(props, context);
this.jumpToTab = this.jumpToTab.bind(this);
// Create state
this.state = {
navigationState: {
// Active route, will be rendered as default
index: 0,
// "tab-s" represents route objects
routes: [
{ name: 'Tab1', key: '1' },
{ name: 'Tab2', key: '2' },
{ name: 'Tab3', key: '3' },
{ name: 'Tab4', key: '4' }],
},
};
}
jumpToTab(tabIndex) {
const navigationState = NavigationStateUtils.jumpToIndex(this.state.navigationState, tabIndex);
this.setState({ navigationState });
}
renderScene({ scene }) {
return <Tab tab={scene.route} />;
}
render() {
const { navigationState } = this.state;
return (
<View style={style.screen}>
<NavigationCardStack
onNavigate={() => {}}
navigationState={navigationState}
renderScene={this.renderScene}
/>
<View style={style.tabBarWrapper}>
{navigationState.routes.map((route, index) => (
<TabBarItem
key={index}
onPress={this.jumpToTab}
title={route.name}
index={index}
/>
))}
</View>
</View>
);
}
}
class TabBarItem extends React.Component {
static propTypes = {
title: React.PropTypes.string,
onPress: React.PropTypes.func,
index: React.PropTypes.number,
}
constructor(props, context) {
super(props, context);
this.onPress = this.onPress.bind(this);
}
onPress() {
this.props.onPress(this.props.index);
}
render() {
return (
<Text style={style.tabBarItem} onPress={this.onPress}>
{this.props.title}
</Text>);
}
}
class Tab extends React.Component {
static propTypes = {
tab: React.PropTypes.object,
}
constructor(props, context) {
super(props, context);
this.goBack = this.goBack.bind(this);
this.pushRoute = this.pushRoute.bind(this);
this.renderScene = this.renderScene.bind(this);
this.state = {
navigationState: {
index: 0,
routes: [{ key: '0' }],
},
};
}
// As in TabBar use NavigationUtils for this 2 methods
goBack() {
const navigationState = NavigationStateUtils.pop(this.state.navigationState);
this.setState({ navigationState });
}
pushRoute(route) {
const navigationState = NavigationStateUtils.push(this.state.navigationState, route);
this.setState({ navigationState });
}
renderScene({ scene }) {
return (
<Screen
goBack={this.goBack}
goTo={this.pushRoute}
tab={this.props.tab}
screenKey={scene.route.key}
/>
);
}
render() {
return (
<NavigationCardStack
onNavigate={() => {}}
navigationState={this.state.navigationState}
renderScene={this.renderScene}
/>
);
}
}
class Screen extends React.Component {
static propTypes = {
goTo: React.PropTypes.func,
goBack: React.PropTypes.func,
screenKey: React.PropTypes.string,
tab: React.PropTypes.object,
}
constructor(props, context) {
super(props, context);
this.nextScreen = this.nextScreen.bind(this);
}
nextScreen() {
const { goTo, screenKey } = this.props;
goTo({ key: `${parseInt(screenKey) + 1}` });
}
render() {
const { tab, goBack, screenKey } = this.props;
return (
<View style={style.screen}>
<Text style={style.screenTitle}>
{`Tab ${tab.key} - Screen ${screenKey}`}
</Text>
<Text style={style.pushNewScreenLabel} onPress={this.nextScreen}>
Push Screen into this Tab
</Text>
<Text style={style.goBackLabel} onPress={goBack}>
Go back
</Text>
</View>
);
}
}
## TBD little more clean up..
Related
I have two react components. the first Lobby uses react-native-navigation to push Gameroom to the stack. It passes props such as the socket object and other data to the Gameroom component
when the back button of the navigation bar is pressed inside Gameroom, a socket.io leave event is emitted, and I have verified it is heard by the server, so the socket passed through props works. the server then emits an event left back to the socket.io room (Gameroom component).
the left event listener, if placed inside Gameroom's componentDidMount() does not execute. However, if the same socket.io event listener is placed in Lobby component (previous screen) componentDidMount() the event is heard
I've tried adding the event listener to multiple componentDidMount functions, I also thought about using the Context API, but I'm not working with nested components. I'm passing the socket object in react-native-navigation's {passProps} from screen to screen
Lobby:
imports ...
const socket = io("http://192.xxx.xxx.xx:3000");
export default class Lobby extends React.Component {
static options(passProps) {
return {
topBar: {
background: {
color: "transparent"
},
drawBehind: true,
visible: true,
animate: true,
leftButtons: [
{
id: "leave",
icon: require("../assets/img/Chevron.png")
}
]
}
};
}
constructor(props) {
super(props);
this.state = {
username: "Initializing...",
queue: []
};
}
componentDidMount() {
Navigation.events().bindComponent(this);
socket.emit("lobbyEntry");
socket.on("lobbyEntry", entry => {
this.setState({ queue: entry.lobby, username: socket.id });
});
socket.on("userJoined", lobby => {
this.setState({ queue: lobby });
});
// socket.on("left", () => {
// alert("Opponent Left...Oh well");
// Navigation.pop(this.props.componentId);
// });
}
navigationButtonPressed({ buttonId }) {
switch (buttonId) {
case "leave":
socket.emit("leave");
Navigation.popToRoot(this.props.componentId);
break;
}
}
createMatch = () => {
if (this.state.username != "Initializing...") {
socket.emit("findMatch");
socket.on("alreadyCreated", () => {
alert("You already created a match!");
});
socket.on("listUsers", lobby => {
this.setState({ queue: lobby });
});
socket.on("matchFound", data => {
Navigation.push(this.props.componentId, {
component: {
name: "Gameroom",
passProps: {
room: data.id,
socket: socket,
firstMove: data.firstMove,
p1: data.p1,
p2: data.p2
}
}
});
});
} else {
alert("Wait for Username to be initialized...");
}
};
render() {
const bg = getBackground();
return (
<ImageBackground source={bg} style={{ height: "100%", width: "100%" }}>
<View style={styles.title_container}>
<Text style={styles.title_sm}>Matchmaking Lobby</Text>
</View>
<View style={styles.alt_text_container}>
<Text style={styles.alt_text_md}>Username:</Text>
<Text style={styles.alt_text_md}>{this.state.username}</Text>
</View>
<View
style={{
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}
>
<XplatformButton onPress={this.createMatch} text={"Create a Match"} />
</View>
<View style={styles.alt_text_container}>
<Text style={styles.alt_text_sm}>Players actively searching...</Text>
<FlatList
style={styles.alt_text_container}
data={this.state.queue}
renderItem={({ item, index }) => (
<Text style={styles.alt_text_md} key={index}>
{item}
</Text>
)}
/>
</View>
</ImageBackground>
);
}
}
Gameroom:
import ...
export default class Gameroom extends React.Component {
static options(passProps) {
return {
topBar: {
title: {
fontFamily: "BungeeInline-Regular",
fontSize: styles.$navbarFont,
text: "Gameroom - " + passProps.room,
color: "#333"
},
background: {
color: "transparent"
},
drawBehind: true,
visible: true,
animate: true,
leftButtons: [
{
id: "leave",
icon: require("../assets/img/Chevron.png")
}
]
}
};
}
constructor(props) {
super(props);
Navigation.events().bindComponent(this);
}
navigationButtonPressed({ buttonId }) {
switch (buttonId) {
case "leave":
this.props.socket.emit("leave");
Navigation.pop(this.props.componentId);
break;
}
}
componentDidMount() {
// socket.on("left", () => {
// alert("Opponent Left...Oh well");
// Navigation.pop(this.props.componentId);
// });
}
render() {
const bg = getBackground();
return this.props.p2 != null ? (
<Gameboard
room={this.props.room}
you={
this.props.socket.id == this.props.p1.username
? this.props.p1.marker
: this.props.p2.marker
}
opponent={
this.props.socket.id != this.props.p1.username
? this.props.p2.marker
: this.props.p1.marker
}
move={this.props.firstMove}
socket={this.props.socket}
/>
) : (
<ImageBackground style={styles.container} source={bg}>
<View style={{ marginTop: 75 }}>
<Text style={styles.alt_text_md}>
Waiting for Opponent to Join...
</Text>
</View>
</ImageBackground>
);
}
}
I expect the event listener to execute from the current screen's componentDidMount() function, but it only executes if it's inside the previous screen's componentDidMount()
When you create a component,
the constructor -> componentWillMount -> render -> componentDidMount is
followed.
In your Lobby class, the event listener is run because it is in ComponentDidmont.
However, the event listener of the Gameroom class is inside the constructor. If executed within the constructor, the event cannot be heard because it is not yet rendered.
Event listeners are called when they appear on the screen
Usage
componentDidMount() {
this.navigationEventListener = Navigation.events().bindComponent(this);
}
componentWillUnmount() {
// Not mandatory
if (this.navigationEventListener) {
this.navigationEventListener.remove();
}
}
I am having trouble rendering my state. The initial value of my state is empty but when I press the button state change.
I want to see this change immediately but the state only renders when start to type something in my Textinput
This is my State:
constructor(props) {
super(props)
this.state = {
noteText: '',
userName:'',
}
}
I change state using onPress button
changeState(){
this.setState({userName:'Kevin'})
}
This is where I render my State
<View>
//I want to immediately render this.state.userName
<Text>{this.state.userName}</Text>
<TextInput
onChangeText={(noteText) => this.setState({noteText:noteText})}
value={this.state.noteText}
/>
</View>
My state won't render until I start to type something in my TextInput. Any idea how I can render immediately?
You have two ways here.
Set default state,
constructor(props) {
super(props)
this.state = {
noteText: '',
userName:'Kevin',
}
this.changeState = this.changeState.bind(this);
}
Or call changeState in componentDidMount,
componentDidMount(){
this.changeState(); //bind this to changeState in constructor
}
You can rather use inside bind your function inside your constructor, or use the arrow function.
constructor(props) {
this.changeState = this.changeState.bind(this);
}
or
changeState = () => {
this.setState({ userName: "Kevin" });
};
Check a simple snack: https://snack.expo.io/#abranhe/change-state
import React, { Component } from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
export default class App extends Component {
state = { userName: '' };
changeState = userName => {
this.setState({ userName });
};
render() {
return (
<View style={styles.container}>
<Text style={styles.username}>{this.state.userName}</Text>
<Button title="Abraham" onPress={() => this.changeState('Abraham')} />
<Button title="James" onPress={() => this.changeState('James')} />
<Button title="Mark" onPress={() => this.changeState('Mark')} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
username: {
alignSelf: 'center',
marginBottom: 20,
},
});
Im using the react navigation library, specifically createBottomTabNavigator
The docs at https://reactnavigation.org/docs/en/params.html explain how to pass parameters between routes using a stack navigator but Im using a tab navigator and I cant find anything that jumps out to me explaining how to do it in a tab navigation setup
My tab navigator in App.js is
const BottomTabMenu = createBottomTabNavigator (
{
WatchList: { screen: WatchListScreen },
Alerts: { screen: AlertsScreen },
Analytics: { screen: AnalyticsScreen },
Settings: { screen: SettingsScreen },
},
{
initialRouteName: 'WatchList',
defaultNavigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused, horizontal, tintColor }) => {
const { routeName } = navigation.state;
let IconComponent = Ionicons;
let iconName;
if (routeName === 'WatchList') { iconName = 'md-list'; }
if (routeName === 'Alerts') { iconName = 'md-alert'; }
if (routeName === 'Analytics') { iconName = 'md-analytics'; }
if (routeName === 'Settings') { iconName = 'md-settings'; }
return <IconComponent name={iconName} size={25} color={tintColor} />;
},
}),
tabBarOptions: {
activeTintColor: 'tomato',
inactiveTintColor: 'gray',
},
}
);
const AppContainer = createAppContainer(BottomTabMenu);
The watchList route holds a jsonfile called symbols that I want to pass to the alerts route which is in alertsScreen.js
export default class WatchListScreen extends React.Component {
constructor(props) {
super(props)
this.state = {
symbols: [],
}
}
LOTS OF OTHER STUFF
ALERTSSCREEN.JS
export default class AlertsScreen extends React.Component {
render() {
//according to the docs, this is how to receive in the props
const { navigation } = this.props;
const jsonWatchList = navigation.getParam('jsonWatchList', '[]');
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>blah blah blah</Text>
</View>
);
}
}
According to the docs the line I need to pass the data would be something like this
this.props.navigation.navigate('Alerts', {
jsonWatchList: [lots of data here],
});
Only problem is I dont know where it should go.
It is well documented in your link (https://reactnavigation.org/docs/en/params.html).
It explains how pass params between route. It doesn't matter if you are using a stackNavigator or a createBottomTabNavigator
Pass your parameters like you wanted to do :
this.props.navigation.navigate('jsonWatchList', {
jsonWatchList: [lots of data here],
});
And in your other screen, get them like :
const data = this.props.navigation.getParam('jsonWatchList');
I have an issue that I don't quite understand.
I would like to display messages contained in an array using several flatlists. Then I will have to group them by date.
The messages actually correspond to a series of questions and answers from a chat where each message is recorded in an offline database (PouchDB is used). So I would like to display in an interface the questions that the user has answered, in short, I want to view the logs.
Here is the code:
const screen = Dimensions.get('screen');
const styles = StyleSheet.create({
logsView: {
backgroundColor: '#dddddd',
paddingLeft: 15,
paddingRight: 2,
height: '100%',
},
dateContainer: {
width: '75%',
padding: 1,
marginTop: 5,
},
dateContent: {
textAlign: 'center',
},
});
export default class ComponentPlanDetailsScreen
extends ComeoMeAbstractScreen<PropsType, StateType> {
static navigationOptions = {
title: µte('MyPlans'),
headerRight: (<View />),
};
constructor(props: PropsType) {
super(props);
this.IfeelMessagesBusiness = new IfeelMessagesBusiness();
this.state = {
currentDate: new Date(),
markedDate: moment(new Date()).format('YYYY-MM-DD'),
messages: [],
};
}
componentDidMount = () => {
// Get all messages from chat history
this.IfeelMessagesBusiness.getAllIfeelMessages().then((result: Object) => {
this.setState({ messages: result });
});
};
// Render each item of Flatlist
renderLogItem = ({ item }: Object) => {
console.log(`je passe renderlogitem ${JSON.stringify(item)}`);
return <LogItem message={item} />;
}
// Key for data in FlatList component
keyExtractor = (item: Object, index: number): string => index.toString();
render() {
const test = [
{
stringValue: 'Did you take some drugs ?',
isImage: false,
isQuestion: true,
questionNumber: 6,
author: {
id: 1,
avatar: 'http://image.noelshack.com/fichiers/2016/47/1480031586-1474755093-risitas721.png',
username: 'Dr Risitas',
},
loggedDateTime: '1552033946989',
},
];
const today = this.state.currentDate;
const day = moment(today).format('x');
return (
<View>
<Carousel
animate={false}
indicatorSize={10}
height={screen.height * 0.7
}
>
<View>
<ScrollView
style={styles.logsView}
>
<View>
{this.state.messages.map((ItemListByDate: Object): Array<Object> => {
console.log(`je passe array ${JSON.stringify([ItemListByDate])}`);
return (
<View key={ItemListByDate.loggedDateTime.toString()}>
<View style={styles.dateContainer}>
{ (parseInt(ItemListByDate.loggedDateTime, 10) + 172800000) <= parseInt(day.toString(), 10) ?
<Text style={styles.dateContent}>{moment(parseInt(ItemListByDate.loggedDateTime, 10)).format('DD-MM-YYYY')}</Text>
:
<Text style={styles.dateContent}>{moment(parseInt(ItemListByDate.loggedDateTime, 10)).fromNow()}</Text>
}
</View>
<View>
<FlatList
data={[ItemListByDate]}
renderItem={this.renderLogItem}
keyExtractor={this.keyExtractor}
ref={(ref: any) => { this.flatList = ref; }}
onContentSizeChange={(): any => this.flatList.scrollToEnd({ animated: true })}
onLayout={(): any => this.flatList.scrollToEnd({ animated: true })}
/>
</View>
</View>);
})
}
</View>
</ScrollView>
</View>
</Carousel>
</View>
);
}
}
The problem I don't understand is that the renderLogItem function to call the LogItem component is never called while the ItemListByDate array is displayed in the logs. No messages are displayed, I just have the grey background of the ScrollView component.
On the other hand, if I use the test array instead of this.state.messages with the map function, the message is displayed correctly in the interface and renderLogItem is called correctly.
I understand that when my component is called for the first time, the state is empty and the switch to the componentDidMount function will in my case trigger the update of the state and cause a re-render. This also causes the map function to call up and normally displays each message
Maybe it is due to a display problem, where the messages would be hidden because the initial state of the messages is empty ?
Thank you in advance for your help !
My first thought is that it has to do with the fact that this.IfeelMessagesBusiness.getAllIfeelMessages() is asynchronous. So the first time the render method is called, it tries to map òver undefined and it never updates.
Could you try doing a flatlist of Flatlist maybe ?
I updated the code to reflect Denis' solution. However, react is now no longer responding to commands sent from the node server
export default class Display extends Component {
constructor(props){
super(props);
this.state = {
ren: "",
action: "background",
media: "white",
backgroundColor: 'white',
uri: "https://www.tomswallpapers.com/pic/201503/720x1280/tomswallpapers.com-17932.jpg"
};
}
componentDidMount(){
this.startSocketIO();
}
componentWillUnmount(){
socket.off(Socket.EVENT_CONNECT);
}
startSocketIO(){
socket.on('function_received', func_var => {
let command = func_var.split(" ");
this.setState({action: command[0]});
this.setState({media: command[1]});
console.log(this.state.action);
console.log(this.state.media);
switch(this.state.action){
case 'image':
this.setState({ uri: this.state.media});
console.log(this.state.uri);
case 'background':
this.setState({ backgroundColor: this.state.media});
console.log(this.state.backgroundColor);
default:
console.log(this.state.backgroundColor);
// return (<View style={{backgroundColor: this.state.backgroundColor, flex: 1}} />);
}
});
}
render(){
return (
null
);
}
}
I'm currently working on a basic react native app that displays images received in uri format from a node server and changes the background color. Separately, both of my implementations work. (See the BackGround and ImageServer components) I'm now attempting to combine the functionality of both components into one component named display. So far my current code looks like it should work without issue however after sending a command to the device via socket.io it appears that the render doesn't go any further since my console logs stop populating after a test. I'm not sure if there is an issue with the setup of the switch statement or if I'm somehow causing a race condition. Any insight would be greatly appreciated!
import React, { Component } from 'react';
import {Image, Text, StyleSheet, Button, View, Dimensions, Vibration} from 'react-native';
const io = require('socket.io-client');
//change this to your public ip "google what is my ip address"
//This will be modified in the future to grab the ip address being used by
//the node server
let server = 'redacted';
let socket = io(server, {
transports: ['websocket']
});
export default class Display extends Component {
constructor(props){
super(props);
this.state = {
action: "background",
media: "white",
backgroundColor: 'white',
uri: "https://www.tomswallpapers.com/pic/201503/720x1280/tomswallpapers.com-17932.jpg"
};
}
render(){
socket.on('function_received', func_var => {
var command = func_var.split(" ");
this.setState({action: command[0]});
this.setState({media: command[1]});
});
console.log(this.state.action);
console.log(this.state.media);
switch(this.state.action){
case 'image':
this.setState({ uri: this.state.media});
return (
<Image
source={{uri: this.state.uri}}
style={styles.fullscreen} />
);
case 'background':
this.setState({ backgroundColor: this.state.media});
return (<View style={{backgroundColor: this.state.backgroundColor, flex: 1}} />);
default:
return (<View style={{backgroundColor: this.state.backgroundColor, flex: 1}} />);
}
}
}
export class BackGround extends Component {
constructor(props){
super(props);
this.state = {
backgroundColor: 'black'
};
}
render(){
socket.on('function_received', func_var => {
this.setState({ backgroundColor: func_var});
});
return (
<View style={{backgroundColor: this.state.backgroundColor, flex: 1}} />
);
}
}
export class ImageServer extends Component {
constructor(props){
super(props);
this.state = {
uri: "https://www.tomswallpapers.com/pic/201503/720x1280/tomswallpapers.com-17932.jpg"
};
}
render() {
socket.on('function_received', func_var => {
//Vibration.vibrate([0, 500, 200, 500,200,500,200,500,200,500,200,500,200], false);
this.setState({ uri: func_var});
});
return(
<Image
source={{uri: this.state.uri}}
style={styles.fullscreen}
/>
);
}
}
You code should like this
componentDidMount() {
this.startSocketIO();
}
startSocketIO() {
socket.on('some_method', (response) => {
this.setState({ key: response.some_value })
});
}
componentWillUnmount() {
// unsubscribe socket.io here !important
}
render() {
return (
// JSX
);
}