Updated August 24, 2022
Setting Max Width When Supporting Web and Mobile in React Native
React Native's Web support continually gets better. With that, we've got to consider different screen sizes and orientations.
While building a React Native version of the game "Match the Pairs" (100% open source) I found that when playing the game in a horizontal landscape on the web everything looked off. The cards were wider than they were tall - I want to maintain the traditional playing card shape on the game board, regardless of screen size.
The Reality of Supporting Multiple Platforms
Before diving into the code I want to make sure you have proper expectations around targeting multiple platforms with a single codebase.
Often when I talk to people about the universal nature of React Native there is an assumption that you get to support iOS, Android, and the Web from one code base with 100% code sharing.
That is unrealistic. You can absolutely support iOS, Android, and the Web from a single codebase that is MOSTLY shared... but there will be differences!
But each platform is going to have unique requirements to make it fit the platform the user is running it on.
This sometimes requires customizations to your layouts and code. That's not a bad thing - you're still able to share a TON of code!
This simple app is a great example - this article shows layout change requirements and a previous article outlines the differences between mobile and the web when handling sharing.
Now, with that out of the way lets dive into the code.
The Starting Code
First, lets look at the code that drives the current UI.
import { SafeAreaView, StyleSheet, View, Text } from "react-native"
import { StatusBar } from "expo-status-bar"
import { StatsCard, GameCard } from "components/cards"
import { Button } from "components/buttons"
import { Spacing, Colors } from "constants/index"
import { useMatchGame } from "hooks/useMatchGame"
import { shareGame } from "src/utils/shareGame"
const ROWS = [
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
]
const MatchThePairs = () => {
const {
emojis,
reset,
chooseCard,
activeCardIndex,
matchedCards,
comparisonCards,
totalMoves,
matchCount,
totalPairs,
} = useMatchGame()
const handlePress = (index: number) => {
chooseCard(index)
}
const handleShare = () => {
if (matchCount === totalPairs) {
shareGame({ emojis, moveCount: totalMoves })
} else {
alert("You haven't matched all the pairs yet!")
}
}
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<View style={[styles.row, styles.header]}>
<Text style={styles.headerText}>Match the pairs 🤔</Text>
</View>
<View style={[styles.row, styles.stats]}>
<StatsCard
title="Pairs matched"
numerator={matchCount}
denominator={totalPairs}
/>
<StatsCard title="Total moves" numerator={totalMoves} />
</View>
{ROWS.map((indices, rowIndex) => (
<View style={[styles.row, styles.gameRow]} key={rowIndex}>
{indices.map(emojiIndex => {
const inMatchedCard = matchedCards.includes(emojiIndex)
const cardIsVisible =
inMatchedCard || comparisonCards.includes(emojiIndex)
return (
<GameCard
key={emojiIndex}
index={emojiIndex}
emojis={emojis}
onPress={() => handlePress(emojiIndex)}
selected={activeCardIndex === emojiIndex}
visible={cardIsVisible}
disabled={inMatchedCard}
/>
)
})}
</View>
))}
<View style={[styles.row, styles.actions]}>
<Button type="primary" onPress={() => reset()}>
Reset game
</Button>
<Button onPress={handleShare}>Share game</Button>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: Spacing.lg,
},
headerText: {
color: Colors.greyDarkest,
fontSize: 20,
fontWeight: "600",
marginTop: Spacing.md,
marginBottom: Spacing.xl,
},
row: {
flexDirection: "row",
marginHorizontal: Spacing.sm,
},
gameRow: {
flex: 1,
},
header: {
marginHorizontal: Spacing.md,
},
stats: {
marginBottom: Spacing.lg,
},
actions: {
justifyContent: "center",
marginTop: Spacing.xl,
},
})
export default MatchThePairs
Layout for Web and Mobile in React Native
To fix the issue we'll do 3 things
- Wrap the game board in a
View
with thecontent
styles - Set the
content
to be full width but have a max width value - Center the
content
view within the container.
/* ... */
const MatchThePairs = () => {
const {
emojis,
reset,
chooseCard,
activeCardIndex,
matchedCards,
comparisonCards,
totalMoves,
matchCount,
totalPairs,
} = useMatchGame()
const handlePress = (index: number) => {
/* ... */
}
const handleShare = () => {
/* ... */
}
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<StatusBar style="dark" />
<View style={[styles.row, styles.header]}>
<Text style={styles.headerText}>Match the pairs 🤔</Text>
</View>
<View style={[styles.row, styles.stats]}>
<StatsCard
title="Pairs matched"
numerator={matchCount}
denominator={totalPairs}
/>
<StatsCard title="Total moves" numerator={totalMoves} />
</View>
{ROWS.map((indices, rowIndex) => (
<View style={[styles.row, styles.gameRow]} key={rowIndex}>
{indices.map(emojiIndex => {
const inMatchedCard = matchedCards.includes(emojiIndex)
const cardIsVisible =
inMatchedCard || comparisonCards.includes(emojiIndex)
return (
<GameCard
key={emojiIndex}
index={emojiIndex}
emojis={emojis}
onPress={() => handlePress(emojiIndex)}
selected={activeCardIndex === emojiIndex}
visible={cardIsVisible}
disabled={inMatchedCard}
/>
)
})}
</View>
))}
<View style={[styles.row, styles.actions]}>
<Button type="primary" onPress={() => reset()}>
Reset game
</Button>
<Button onPress={handleShare}>Share game</Button>
</View>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingVertical: Spacing.lg,
alignItems: "center",
},
content: {
flex: 1,
width: "100%",
maxWidth: 550,
},
// ...
})
export default MatchThePairs
Let's dive into why we do each of these things.
First, we wrap our board in a second View
so that the SafeAreaView
can still take up the full screen (allowing for any background changes or other full screen changes).
We then use styles.content
to fill the full vertical height, set the width to be 100% on portrait screens, and set a max width on it so that the playing cards remain the correct shape.
Finally we add alignItems: 'center'
to the styles.container
to center our game board. You can accomplish the same thing by adding marginHorizontal: 'auto'
to styles.content
but I prefer to use Flexbox whenever possible.
All of that results in the following game on the web:
You can find the full open source React Native game on Github.