David Ekanem In love with technology and discussions around technology.

Implementing split view and responsive layout in React Native

10 min read 2879

Implementing split view and responsive layout in React Native

In this article, we’ll see how to build responsive layouts in React Native, and build on that knowledge to build a split view mail application while relying on a library from Shopify called Restyle.

Getting started

To follow along with this tutorial, you’ll need:

  • React Native ≥ v0.63.0
  • Expo ≥ v41
  • TypeScript ≥ v4.1.0
  • Node.js ≥ v16.13.1

To get started with building our React Native application, install the Expo CLI globally.

yarn global add expo-cli

Run the following commands to create a new React Native project, which we’ll call ResponsiveProject.

expo init ResponsiveProject

After initializing the project, we have a few key choices to make here. The Restyle library that we’ll be using in this project provides a type-enforced system for building UI components in React Native with TypeScript. As such, my preference is to use TypeScript for this project for the overall type safety that it enforces as we build out the theme of our project.

For readers that aren’t familiar with TypeScript but are familiar with JavaScript, I encourage you to read on, as we will not be diving too deeply into TypeScript in this project.

Choose the TypeScript React Native template
Choose the TypeScript React Native template
cd ResponsiveProject
yarn start

Run the React Native application

Install the Expo client app on your iOS or Android phone and make sure your phone and computer are connected to the same wireless network.

On Android, use the Expo app to scan the QR code from the terminal and on iOS, use the inbuilt QR code scanner in the Camera app.
Scan the QR code using your mobile device to begin the project

What is Restyle?

The Restyle library draws upon Styled System by offering theming (such as light and dark modes) and works off React Native’s default styling.

With Restyle, we can create UI libraries in React Native. The theme we create is connected to a component’s styles by their props, allowing consumers of the UI library to alter the styles of their components easily using these “utility-style props”. The props are easy to make responsive according to breakpoints set in the theme.

We made a custom demo for .
No really. Click here to check it out.

Working with Restyle

Setting up Restyle is quite simple. First, install the library into our project, ResponsiveProject.

yarn add @shopify/restyle

Copy and run the commands below to create an src folder in our root directory. Within the src folder, create a components folder to hold our index.ts and Theme.ts files in the components directory.

mkdir src
mkdir src/components
touch src/components/index.ts
touch src/components/Theme.ts

Open up the Theme.ts file in our components folder, and here, create our custom theme. The Restyle library comes with predefined functions to create both Box and Text components.

code src/components/Theme.ts

In Theme.ts, we are going to define our global theme object. It specifies set values for spacing, colors, and breakpoints.

TypeScript enforces the marginTop property to only accept margins that have been defined in our theme. When using our Restyle components, we can write marginTop="xl" to use the named spacing specified in our theme. If we want to customize our spacing, we can do it as I’ve outlined in the below code block.

import {
  BaseTheme,
  createBox,
  createText,
  createTheme,
  useTheme as useReTheme,
} from "@shopify/restyle";

export const theme: BaseTheme = createTheme({
  colors: {
    secondary: "#2CB9B0",
    text: "rgba(12,12,52,0.5)",
    white: "#F0F2F3",
  },
  spacing: {
    s: 8,
    m: 16,
    l: 24,
    xl: 75,
  },
  borderRadii: {
    s: 4,
    m: 10,
    l: 25,
    xl: 75,
  },
 breakpoints: {
    phone: 0,
    tablet: 768
   },
 textVariants: {
    hero: {
      fontSize: 80,
      lineHeight: 80,
      color: "white",
      textAlign: "center",
    },
     title1: {
      fontSize: 24,
      lineHeight: 30,
      color: "text",
    },
    body: {
      fontSize: 16,
      lineHeight: 24,
      color: "text",
    },
  },
});

export type Theme = typeof theme;
export const Text = createText<Theme>();
export const Box = createBox<Theme>();
export const useTheme = () => useReTheme<Theme>();

Open up the index.ts file in our components folder and export our theme.

export { Text, Box, useTheme} from "./Theme";

If you decide not to use TypeScript, you can remove the export type and the theme should work in vanilla JavaScript. But as we mentioned earlier, it’s highly recommended to use TypeScript with the Restyle library.

Install the following packages:

yarn add @react-navigation/native @react-navigation/stack @react-navigation/drawer react-native-screens react-native-safe-area-context react-native-reanimated

In our App.tsx file, we wrap the application in the ThemeProvider component. The theme we create will be passed to a ThemeProvider at the top of our React tree:

import * as React from "react";
import { ThemeProvider } from "@shopify/restyle";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";
import {
  HomeNavigator,
} from "./src/Home";
import { theme } from "./src/components/Theme";
const AppStack = createStackNavigator();

export default function App() {
  return (
      <ThemeProvider theme={theme}>
            <SafeAreaProvider>
              <NavigationContainer>
                    <AppStack.Navigator
                screenOptions={{
                    headerMode: "screen",
                    headerShown: false,
                 }}
                    >
                <AppStack.Screen name="Home" component={HomeNavigator} />
                    </AppStack.Navigator>
            </NavigationContainer>
          </SafeAreaProvider>
 </ThemeProvider>
  );
}

Defining breakpoints to build responsive components

In this section, we’ll rely on breakpoints to build our responsive components. Breakpoints in React Native and Restyle are minimum widths for different target screen sizes where we desire to apply differing styles.

Using breakpoints, we can specify different styles for different screen sizes. To demonstrate this, we are going to create an example card that maintains styling on both widescreen devices and mobile screens.

In the src directory, create a folder called Home.

mkdir src/Home

In the Home folder, create an index.tsx file and input the code block below.

touch src/Home/index.tsx
code src/Home/index.tsx
import * as React from "react";
import { createDrawerNavigator } from "@react-navigation/drawer";
import HomeScreen from "./HomeScreen";

const Drawer = createDrawerNavigator();
export const HomeNavigator = () => (
  <Drawer.Navigator
    screenOptions={{
      headerShown: true,
    }}
    initialRouteName="Home"
  >
    <Drawer.Screen name="Home" component={HomeScreen} />
  </Drawer.Navigator>
);

In our Home folder, we’ll create the following:

  • A subfolder called HomeScreen
  • An index.ts file
  • A HomeScreen.tsx file
mkdir src/Home/HomeScreen
touch src/Home/HomeScreen/index.ts
touch src/Home/HomeScreen/HomeScreen.tsx

Open the index.ts file in our subfolder HomeScreen and export the HomeScreen component.

code src/Home/HomeScreen/index.ts
export { default } from "./HomeScreen";

Now, open up the HomeScreen.tsx file.

code src/Home/HomeScreen/HomeScreen.tsx

In the HomeScreen.tsx, we create a card example that will fit on widescreen devices and mobile devices.

We’ll rely on the defined breakpoints to determine whether the detected screen is a phone or tablet size. If it’s a phone, we use the justifyContent property to center the box. If the detected screen is a tablet, we select the space-around property.

import React from "react";
import { Box, Header, Text } from "../../components";

const HomeScreen = () => {
  return (
 <Box
    flex={1}
    backgroundColor="secondary"
    justifyContent={{ phone: "center", tablet: "space-around" }}
    padding="xl"
    elevation={4}
    flexDirection={{ phone: "column", tablet: "row" }}
    borderRadius="s"
 >
       <Box flex={1} alignItems="flex-start" flexWrap="wrap" margin="s">
         <Text variant="title1">
           Open up the application to test the responsiveness. 
         </Text>
        <Text variant="body">While working with mobile devices, there are two kinds of dimensions that have to be focused on. This is a sample component which shows how easy it is to create responsive components for different screen sizes with Restyle. 
        </Text>
      </Box>
   </Box>
)
}
export default HomeScreen;

Testing responsiveness in portrait and landscape modes

In order to test out the responsiveness in landscape and portrait mode, we have to go into our app.json file and make some changes.

code app.json

We add a change to the orientation property in app.json.

    "orientation": "portrait || landscape",

Testing the responsiveness of our layout based on screen orientation

Improving our app’s responsive layout

In this next section, we are going to explore some other properties of React Native that enable us to improve the responsiveness of our application.

React Native provides the useWindowDimensions Hook, which automatically updates width and height of components when the screen size changes. We can also detect the platform on which the app is launched.

To achieve this, we’ll use the Platform module.

import React from "react";
import { useWindowDimensions, Platform } from "react-native";
import Constants from "expo-constants";
import { Text } from "../../components";

const { width, height: wHeight } = useWindowDimensions();

const Container = ({children}) => {
      return (
     <Box
        height={
          wheight + (Platform.OS === "android" ? Constants.statusBarHeight : 0)
        }
        backgroundColor="primary"
      >
        <Box
            borderRadius="xl"
            borderTopLeftRadius={0}
            backgroundColor="white"
            flex={1}
            justifyContent="center"
            padding="l"
          >
            {children}
          </Box>
     </Box>
   ):
};
export default Container;

Implementing split views in our applications

Per Apple’s Human Interface Guidelines, a split view:

manages the presentation of hierarchical content at the top level of our application. A split view can consist of a two- or three-column interface showing a primary column, and a secondary pane of content. Changes in the primary column drive changes in the secondary pane of content.

Let’s implement one. First, create a folder called navigation in our src folder and create the following three files:

  • An index.ts file
  • A sideNav.ts file
  • A Navigator.ts file
mkdir.exe src/navigation
touch src/navigation/index.ts
touch src/navigation/sideNav.ts
touch src/navigation/Navigator.ts
code src/navigation/Navigator.ts

In the Navigator.ts file, we import the CommonActions and StackOptions objects from the @react-navigation/native library.

We use StackOptions and CommonActions to generate actions specific to stack-based navigators; StackOptions expands upon the actions available in the CommonActions reference and creates a Navigator class.

import { StackActions, CommonActions } from "@react-navigation/native";

class Navigator {
  navigator = null;
  setNavigator = (navigatorInstance: any) => {
    this.navigator = navigatorInstance;
  };
  isMounted = () => Boolean(this.navigator);
  dispatch = (action: any) => {
    this.navigator.dispatch(action);
  };
  navigate = (screenName: string, params: object) =>
    this.dispatch(CommonActions.navigate({ name: screenName, params }));
  push = (screenName: string, params: object) =>
    this.dispatch(StackActions.push(screenName, params));
}
export default Navigator;

Open up the sideNav.ts file and initialize the functions in our Navigator class.

import Navigator from "./Navigator";

export const {
  isMounted: isSideNavigatorMounted,
  push: sidePush,
  navigate: sideNavigate,
  setNavigator: setSideNavigator,
} = new Navigator();

To complete our Navigator component, we export the component.

code src/navigation/index.ts
import Navigator from "./Navigator";

export const { push, setNavigator: setIndexNavigator } = new Navigator();

In our app.tsx file, we pass our indexNavigator as a reference to our NavigationContainer from the React Native Navigation library.

import { setIndexNavigator } from "./src/navigation";
const AppStack = createStackNavigator();

export default function App() {
  return (
    <ThemeProvider theme={theme}>
      <SafeAreaProvider>
        <NavigationContainer ref={setIndexNavigator}>
          <AppStack.Navigator
            screenOptions={{
              headerMode: "screen",
              headerShown: false,
            }}
          >
            <AppStack.Screen name="Home"  component={HomeNavigator} />
          </AppStack.Navigator>
        </NavigationContainer>
      </SafeAreaProvider>
    </ThemeProvider>
  );
}

Now, we’ll move on to creating our Split.tsx component in our component folder.

We’ll do this in the src directory, which should contain a main component and a side component.

touch src/components/Split.tsx
code src/components/Split.tsx
import * as React, {ReactNode} from "react";
import { Box } from "./Theme";

interface SplitProps {
  main: ReactNode;
  side: ReactNode;
}

const Split = ({ main, side }: SplitProps) => {
  return (
    <Box style={{ flex: 1, flexDirection: "row" }}>
      <Box
        style={{
          flex: 1,
          maxWidth: 400,
          borderWidth: 1,
          borderColor: "secondary",
        }}
      >
        {main}
      </Box>
      <Box
        style={{
          flex: 1,
          overflow: "hidden",
          borderWidth: 1,
          borderColor: "text",
        }}
      >
        {side}
      </Box>
    </Box>
  );
};

export default Split;

Export the Split.tsx component from the index file of the components folder.

code src/components/index.ts
export { Text, Box, useTheme } from "./Theme";
export { default as Split } from "./Split";

Adding mock data for display

The next step is the creation of our mail screens and adding some mock data. To generate mock data, we’ll use the Faker library and Lorem Ipsum Generator.

Install the Faker library and its type declarations to enable it in our TypeScript application.

yarn add faker
yarn add --dev @types/faker

Create the folder that contains our Mail components and the mock data file.

mkdir src/Mail
mkdir src/Mail/Screens
touch src/Mail/Screens/Mails.tsx
touch src/Mail/Screens/Mail.tsx
touch src/Mail/data.ts
code src/Mail/data.ts

Copy the mock data from this GitHub Gist I created, and paste it into the data.ts file.

We’ll create an enum to strongly type our navigation between the different column screens.

touch src/Mail/enums.ts
code src/Mail/enums.ts

Define our mail screen like so:

export enum MailStatus {
  MAILS_SCREEN = "MAILS",
  MAIL_SCREEN = "MAIL",
}

In our Mail folder, create a navigation.ts file and create the pushMailScreen function to control the mounting of the selected screen on the main column interface.

touch src/Mail/mailNavigation.ts
code src/Mail/mailNavigation.ts
import { isSideNavigatorMounted, sideNavigate } from "../navigation/detailsNav";
import { push } from "../navigation";
import { MailStatus } from "./enums";
interface MailNavigationProps {
  userId: string;
}
export const pushMailScreen = ({ userId }: MailNavigationProps) => {
  const args = [MailStatus.MAIL_SCREEN, { userId }];
  if (isSideNavigatorMounted()) {
    return sideNavigate(...args);
  }
  return push(...args);
};

Creating the first column screen component

Next, we move on to creating the screen component for our first column, which will hold the names of the email senders.

code src/Mail/Screens/Mails.tsx
import * as React from "react";
import { FlatList, Image } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { pushMailScren } from "../mailNavigation";
import { usersById, userIds } from "../data";
import { Box, Text } from "../../components";

interface MailsProps {
  userId: string;
  item: any;
}

const renderItem = ({ item: userId }: MailsProps) => {
  const { name } = usersById[userId];
  const onPress = () => pushMailScreen({ userId });

  return (
    <TouchableOpacity onPress={onPress}>
      <Box
        style={{
          flexDirection: "row",
          alignItems: "center",
          borderTopWidth: 1,
          marginBottom: 10,
          paddingTop: 10,
        }}
      >
        <Text style={{ marginLeft: 10, fontSize: 20 }}>{name}</Text>
      </Box>
    </TouchableOpacity>
  );
};
const keyExtractor = (id: string) => id;
const Mails = () => (
  <FlatList
    data={userIds}
    keyExtractor={keyExtractor}
    renderItem={renderItem}
  />
);

export default Mails;

The next step is creating the side column, which we’ll call Mail.tsx.

import * as React from "react";
import { FlatList, Image } from "react-native";
import { Box, Text } from "../../components";
import { usersById } from "../data";

interface MailProps {
  body: string;
  item: any;
  id: string;
  route: any;
}

const renderItem = ({ item: { body } }: MailProps) => (
  <Text
    style={{
      borderBottomWidth: 1,
      padding: 10,
    }}
  >
    {body}
  </Text>
);

const keyExtractor = ({ id }: MailProps) => id;

const Mail = ({ route }: MailProps) => {
   const userId = route.params === undefined ? 1 : route?.params?.userId;
  const { messages = [], name, email } = usersById[userId | 1];

  const ListHeaderComponent = () => (
    <>
      <Box
        style={{
          alignItems: "center",
          borderBottomWidth: 1,
          marginBottom: 20,
          paddingTop: 20,
        }}
      >
        <Text style={{ fontSize: 20, marginBottom: 10 }}>{name}</Text>
        <Text style={{ fontSize: 16 }}>E-mail: {email}</Text>
      </Box>
      <Text style={{ fontSize: 20, paddingLeft: 10 }}>Messages: </Text>
    </>
  );
  return (
    <FlatList
      keyExtractor={keyExtractor}
      data={messages}
      renderItem={renderItem}
      ListHeaderComponent={ListHeaderComponent}
    />
  );
};

export default Mail;

Using the createStackNavigation function

The next step is the creation of two stack navigators — a main stack and a side stack.

mkdir src/Mail
mkdir src/Mail/stack
touch src/Mail/stack/MainStack.tsx
touch src/Mail/stack/SideStack.tsx
code src/Mail/stack/MainStack.tsx

We’ll use @react-navigation/stack to enable us to transition between screens.

import * as React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { MailStatus } from "../enums";
import Mail from "../Screens/Mail";
import Mails from "../Screens/Mails";

const MainStack = createStackNavigator();

const screens = [
  {
    name: MailStatus.MAILS_SCREEN,
    component: Mails,
  },
  {
    name: MailStatus.MAIL_SCREEN,
    component: Mail,
  },
];

const MainMailStack = () => (
  <MainStack.Navigator initialRouteName={MailStatus.MAIL_SCREEN}>
    {screens.map((props) => (
      <MainStack.Screen key={props.name} {...props} />
    ))}
  </MainStack.Navigator>
);
export default MainMailStack;

To complete our side-column interface, we create a SideStack.tsx file.

code src/Mail/stack/SideStack.tsx
import * as React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";

import { setSideNavigator } from "../../navigation/sideNav";

import Mail from "../Screens/Mail";
import { MailStatus } from "../enums";

const SideStack = createStackNavigator();

const screenProps = [
  {
    name: MailStatus.MAIL_SCREEN,
    component: Mail,
  },
];

const SideMailStack = () => (
  <NavigationContainer independent ref={setSideNavigator}>
    <SideStack.Navigator initialRouteName={MailStatus.MAIL_SCREEN}>
      {screenProps.map((props) => (
        <SideStack.Screen key={props.name} {...props} />
      ))}
    </SideStack.Navigator>
  </NavigationContainer>
);

export default SideMailStack;

The last piece of the puzzle is the MailScreen. Let’s create an index.tsx file in our Mail folder.

code src/Mail/index.tsx
import * as React from "react";
import { Split } from "../components";
import MainMailStack from "./stack/MainStack";
import SideMailStack from "./stack/SideStack";
const MailScreen = () => {
  return <Split main={<MainMailStack />} side={<SideMailStack />} />;
};
export default MailScreen;

To complete our split view email screen, open up the root file of our Home folder, import the MailScreen, and add it to our Drawer navigator.

code src/Home/index.tsx
import * as React from "react";
import { createDrawerNavigator } from "@react-navigation/drawer";
import HomeScreen from "./HomeScreen";
import MailScreen from "../Mail";
 
const Drawer = createDrawerNavigator();
export const HomeNavigator = () => (
  <Drawer.Navigator
    screenOptions={{
      headerShown: true,
    }}
    initialRouteName="HomeScreen"
  >
    <Drawer.Screen name="Home" component={HomeScreen} />
    <Drawer.Screen name="Mail" component={MailScreen} />
  </Drawer.Navigator>
);

Here is the final gif of our split view mail screen.
Our final split view implementation

 

Conclusion

In this tutorial, we implemented a simple component that adapted its content to any screen size, showed a technique for adjusting screen width and height using the useWindowsDimensions Hook, and created a simple split view application.

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — .

David Ekanem In love with technology and discussions around technology.

Leave a Reply