Last active
December 25, 2018 20:41
-
-
Save ericallam/51ae9fbd654d8b692bff to your computer and use it in GitHub Desktop.
Getting React Native's Navigator and Relay to work together
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AppComponent from './src/components/AppComponent'; | |
import Relay, { | |
DefaultNetworkLayer, | |
} from 'react-relay'; | |
import React, { | |
Component | |
} from 'react-native'; | |
Relay.injectNetworkLayer(new DefaultNetworkLayer('http://localhost:3000/graphql')); | |
export default class App extends Component { | |
render(): void { | |
return ( | |
<AppComponent /> | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Relay, { | |
RootContainer, | |
Route | |
} from 'react-relay' | |
class SeasonRoute extends Route { | |
static paramDefinitions = {}; | |
static queries = { | |
currentSeason: () => Relay.QL`query { currentSeason }`, | |
}; | |
static routeName = 'MatchdayRoute'; | |
} | |
class NodeRoute extends Route { | |
static paramDefinitions = { | |
nodeID: { required: true } | |
}; | |
static queries = { | |
node: () => Relay.QL`query { node(id: $nodeID) }`, | |
}; | |
static routeName = 'NodeRoute'; | |
} | |
const MatchdayList = require('./MatchdayList'); | |
const MatchList = require('./MatchList'); | |
const ROUTES = { | |
MatchdayList, | |
MatchList | |
}; | |
import React, { | |
View, | |
Text, | |
StyleSheet, | |
Navigator, | |
Component | |
} from 'react-native'; | |
export default class AppComponent extends Component { | |
renderScene(route, navigator){ | |
const props = { route, navigator }; | |
if (route.name == "MatchdayList") { | |
return <RootContainer | |
Component={MatchdayList} | |
route={new SeasonRoute()} | |
renderFetched={(data) => <MatchdayList {...props} {...data} />} | |
/> | |
}else if (route.name == "MatchList") { | |
return <RootContainer | |
Component={MatchList} | |
route={new NodeRoute({nodeID: route.matchday.id})} | |
renderFetched={(data) => <MatchList {...props} {...data} />} | |
/> | |
} | |
} | |
render() { | |
return ( | |
<Navigator | |
style = { styles.container } | |
initialRoute = { { name: 'MatchdayList' } } | |
renderScene = { this.renderScene.bind(this) } | |
configureScene = { () => { return Navigator.SceneConfigs.FloatFromRight; } } /> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
justifyContent: 'center' | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
AppRegistry, | |
} from 'react-native'; | |
import App from './app'; | |
AppRegistry.registerComponent('App', () => App); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Relay from 'react-relay'; | |
import React, { | |
View, | |
Text, | |
StyleSheet, | |
Component, | |
TouchableHighlight | |
} from 'react-native'; | |
class MatchdayItem extends Component { | |
constructor(props, context) { | |
super(props, context); | |
this._handleItemPress = this._handleItemPress.bind(this); | |
} | |
_handleItemPress() { | |
this.props.handleItemPress(this.props.matchday) | |
} | |
render() { | |
var { matchday } = this.props; | |
return ( | |
<TouchableHighlight onPress={this._handleItemPress}> | |
<View> | |
<View> | |
<Text> | |
Matchday {matchday.number} | |
</Text> | |
<Text> | |
{matchday.start_date} | |
</Text> | |
<Text> | |
{matchday.status} | |
</Text> | |
</View> | |
<View> | |
<Text>{matchday.matches_count} matches</Text> | |
</View> | |
</View> | |
</TouchableHighlight> | |
) | |
} | |
} | |
module.exports = Relay.createContainer(MatchdayItem, { | |
fragments: { | |
matchday: () => Relay.QL` | |
fragment on Matchday { | |
id, | |
number, | |
start_date, | |
matches_count, | |
status | |
} | |
` | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Relay from 'react-relay'; | |
import MatchdayItem from './MatchdayItem'; | |
import MatchList from './MatchList' | |
import React, { | |
View, | |
Text, | |
StyleSheet, | |
ListView, | |
Component | |
} from 'react-native'; | |
const _matchdaysDataSource = new ListView.DataSource({ | |
rowHasChanged: (r1, r2) => r1.__dataID__ !== r2.__dataID__, | |
}); | |
class MatchdayList extends Component { | |
constructor(props, context) { | |
super(props, context); | |
const { edges } = props.currentSeason.matchdays; | |
this.state = { | |
initialListSize: edges.length, | |
listScrollEnabled: true, | |
dataSource: _matchdaysDataSource.cloneWithRows(edges), | |
}; | |
this.renderMatchdayEdge = this.renderMatchdayEdge.bind(this); | |
this.handleItemPress = this.handleItemPress.bind(this); | |
} | |
handleItemPress(matchday) { | |
this.props.navigator.push({name: 'MatchList', matchday: matchday}); | |
} | |
componentWillReceiveProps(nextProps) { | |
if (this.props.currentSeason.matchdays.edges !== nextProps.currentSeason.matchdays.edges) { | |
const { | |
dataSource, | |
} = this.state; | |
this.setState({ | |
dataSource: | |
dataSource.cloneWithRows(nextProps.currentSeason.matchdays.edges), | |
}); | |
} | |
} | |
renderSeparator(sectionId, rowId) { | |
return <View key={`sep_${sectionId}_${rowId}`} style={styles.separator} />; | |
} | |
renderMatchdayEdge(matchdayEdge, sectionID, rowID) { | |
return ( | |
<MatchdayItem | |
key={matchdayEdge.node.id} | |
matchday={matchdayEdge.node} | |
handleItemPress={this.handleItemPress} | |
/> | |
); | |
} | |
render() { | |
return ( | |
<View> | |
<View> | |
<Text>{currentSeason.name}</Text> | |
</View> | |
<ListView | |
dataSource={this.state.dataSource} | |
initialListSize={this.state.initialListSize} | |
renderRow={this.renderMatchdayEdge} | |
renderSeparator={this.renderSeparator} | |
/> | |
</View> | |
); | |
} | |
} | |
module.exports = Relay.createContainer(MatchdayList, { | |
fragments: { | |
currentSeason: () => Relay.QL` | |
fragment on Season { | |
name, | |
matchdays(first: 100) { | |
edges { | |
node { | |
id, | |
number | |
${MatchdayItem.getFragment('matchday')} | |
}, | |
}, | |
}, | |
} | |
` | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Relay from 'react-relay'; | |
import React, { | |
View, | |
Text, | |
StyleSheet, | |
Component, | |
ListView, | |
TouchableHighlight | |
} from 'react-native'; | |
const MatchResults = ({ | |
home_team_score, | |
away_team_score | |
}) => ( | |
<Text style={styles.score}> | |
{home_team_score} - {away_team_score} | |
</Text> | |
) | |
import moment from 'moment' | |
class MatchItem extends Component { | |
_renderResults() { | |
var { match } = this.props; | |
if (match.status === 'FINISHED') { | |
return <MatchResults | |
home_team_score={match.home_team_score} | |
away_team_score={match.away_team_score} /> | |
} | |
} | |
render() { | |
const { match } = this.props; | |
const scheduledDate = moment(match.scheduled_date); | |
return ( | |
<TouchableHighlight> | |
<View style={styles.row}> | |
<View style={styles.info}> | |
<Text style={styles.teams}> | |
{match.home_team.name} vs {match.away_team.name} | |
</Text> | |
<Text style={styles.date}> | |
{scheduledDate.format('DD/MM/YYYY h:mm:ss a')} | |
</Text> | |
{this._renderResults()} | |
</View> | |
</View> | |
</TouchableHighlight> | |
) | |
} | |
} | |
MatchItem = Relay.createContainer(MatchItem, { | |
fragments: { | |
match: () => Relay.QL` | |
fragment on Match { | |
id, | |
scheduled_date, | |
status, | |
home_team_score, | |
away_team_score, | |
home_team { name } | |
away_team { name } | |
} | |
` | |
}, | |
}); | |
const _matchesDataSource = new ListView.DataSource({ | |
rowHasChanged: (r1, r2) => r1.__dataID__ !== r2.__dataID__, | |
}); | |
class MatchList extends Component { | |
constructor(props, context) { | |
super(props, context); | |
const { edges } = props.node.matches; | |
this.state = { | |
initialListSize: edges.length, | |
listScrollEnabled: true, | |
dataSource: _matchesDataSource.cloneWithRows(edges), | |
}; | |
this.renderMatchEdge = this.renderMatchEdge.bind(this); | |
} | |
componentWillReceiveProps(nextProps) { | |
if (this.props.node.matches.edges !== nextProps.node.matches.edges) { | |
const { | |
dataSource, | |
} = this.state; | |
this.setState({ | |
dataSource: | |
dataSource.cloneWithRows(nextProps.node.matches.edges), | |
}); | |
} | |
} | |
renderSeparator(sectionId, rowId) { | |
return <View key={`sep_${sectionId}_${rowId}`} style={styles.separator} />; | |
} | |
renderMatchEdge(matchEdge, sectionID, rowID) { | |
return ( | |
<MatchItem | |
key={matchEdge.node.id} | |
match={matchEdge.node} | |
/> | |
); | |
} | |
render() { | |
var { matchday } = this.props.route; | |
return ( | |
<View style={styles.container}> | |
<View style={styles.header}> | |
<Text style={styles.headerText}>Matchday {matchday.number}</Text> | |
</View> | |
<ListView | |
dataSource={this.state.dataSource} | |
initialListSize={this.state.initialListSize} | |
renderRow={this.renderMatchEdge} | |
renderSeparator={this.renderSeparator} | |
/> | |
</View> | |
); | |
} | |
} | |
module.exports = Relay.createContainer(MatchList, { | |
fragments: { | |
node: () => Relay.QL` | |
fragment on Matchday { | |
number, | |
matches(first: 10) { | |
edges { | |
node { | |
id | |
${MatchItem.getFragment("match")} | |
}, | |
}, | |
}, | |
} | |
` | |
}, | |
}); | |
var styles = StyleSheet.create({ | |
container: { | |
flex: 1 | |
}, | |
separator: { | |
height: 1, | |
backgroundColor: '#e0e0e0', | |
marginLeft: 14 | |
}, | |
header: { | |
height: 60, | |
justifyContent: 'center', | |
alignItems: 'center', | |
backgroundColor: 'lightgrey', | |
flexDirection: 'column', | |
paddingTop: 25 | |
}, | |
headerText: { | |
fontWeight: 'normal', | |
fontSize: 18, | |
color: 'black' | |
}, | |
teams: { | |
fontWeight: 'bold' | |
}, | |
date: { | |
color: '#949494' | |
}, | |
score: { | |
color: '#585858', | |
fontSize: 12, | |
fontWeight: '200' | |
}, | |
row: { | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
alignItems: 'center', | |
padding: 12, | |
backgroundColor: 'white' | |
}, | |
info: { | |
flexDirection: 'column', | |
justifyContent: 'space-around' | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
scalar DateTime | |
type Match implements Node { | |
id: ID! | |
scheduled_date: DateTime | |
status: String | |
home_team_score: Int | |
away_team_score: Int | |
home_team: Team | |
away_team: Team | |
} | |
type MatchConnection { | |
pageInfo: PageInfo! | |
edges: [MatchEdge] | |
} | |
type Matchday implements Node { | |
id: ID! | |
number: Int! | |
status: Status! | |
matches_count: Int | |
start_date: DateTime | |
end_date: DateTime | |
matches(after: String, first: Int, before: String, last: Int): MatchConnection | |
} | |
type MatchdayConnection { | |
pageInfo: PageInfo! | |
edges: [MatchdayEdge] | |
} | |
type MatchdayEdge { | |
node: Matchday | |
cursor: String! | |
} | |
type MatchEdge { | |
node: Match | |
cursor: String! | |
} | |
interface Node { | |
id: ID! | |
} | |
type PageInfo { | |
hasNextPage: Boolean! | |
hasPreviousPage: Boolean! | |
startCursor: String | |
endCursor: String | |
} | |
type Root { | |
currentSeason: Season | |
node(id: ID!): Node | |
} | |
type Season implements Node { | |
id: ID! | |
name: String! | |
leagueCode: String! | |
matchdays(after: String, first: Int, before: String, last: Int): MatchdayConnection | |
} | |
enum Status { | |
UPCOMING | |
ACCEPTING_UPDATES | |
IN_PROGRESS | |
COMPLETED | |
} | |
type Team implements Node { | |
id: ID! | |
name: String | |
} |
I've updated this Gist with the recommendation in this relay issue. Now I use a new Route/RootContainer for each Navigator child.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm having trouble figuring out how to get Relay + React Native's navigator component to work together. This uses navigator to show a list of "Matchdays" related to the current season of Premier League football. When the user taps on a matchday item, I want to navigate them to a list of Matches related to that single matchday. As you can see in
MatchdayList.handleItemPress
I'm usingnavigator.push
to navigate to the MatchList, and passing in the single matchday in the route as is the practice with React Native's navigator.The problem is when the MatchList is rendered, Relay displays the warning:
If I update the
App.renderScene
method to pass in the matchday as a prop to the component, I get a similar but different warning:The problem seems to be that in Relay's model, MatchList should be a child to MatchdayItem? That way it would be able to resolve the GraphQL query to next the MatchList fragment inside the single Matchday record?
But with Navigator, when MatchList is pushed into the stack, it replaces the existing MatchdayList component, leaving Relay confused as to the hierarchy of the components and thus the GraphQL structure it needs to build.