Skip to content

Instantly share code, notes, and snippets.

@ericallam
Last active December 25, 2018 20:41
Show Gist options
  • Save ericallam/51ae9fbd654d8b692bff to your computer and use it in GitHub Desktop.
Save ericallam/51ae9fbd654d8b692bff to your computer and use it in GitHub Desktop.
Getting React Native's Navigator and Relay to work together
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 />
);
}
}
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'
}
});
import React, {
AppRegistry,
} from 'react-native';
import App from './app';
AppRegistry.registerComponent('App', () => App);
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
}
`
},
});
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')}
},
},
},
}
`
},
});
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'
}
});
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
}
@ericallam
Copy link
Author

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 using navigator.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:

RelayContainer: Expected prop `matchday` to be supplied to `MatchList`, but got `undefined`. Pass an explicit `null` if this is intentional.

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:

Expected prop `matchday` supplied to `MatchList` to be data fetched by Relay. This is likely an error unless you are purposely passing in mock data that conforms to the shape of this component's fragment.

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.

@ericallam
Copy link
Author

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