Playing with user interface in React Native

Playing with user interface in React Native

So, you’re here to understand how the user interface is designed in React Native? Yes, you’re at the right place because we talk about user interface only in this article. Let’s start with understanding what user interface is.

Let’s continue with the Counter Project and take it a step further by learning how to enhance the design of the counter, making it both more aesthetically pleasing and straightforward. To get started, clone the project using this command:

git clone https://github.com/devnur-org/rn-expo-counter

The project has multiple checkpoints set up as branches, so let’s switch to the specific checkpoint by using the following command.

git checkout reusable-components

Install the dependencies

yarn

Run the Metro Server and run the application on any platform

yarn start --reset-cache

You’re now prepared to continue with the project. If you want to understand how we reached this stage in the Counter Project, check out the following article.

In the current scenario, we have two counters that are working independently as shown below

We need to enhance the user interface and add more details to the screen to improve the application’s appearance. Initially, we only had two counters on the screen without any additional elements. Now, we plan to implement the following changes:

  1. Add a header featuring the application’s name and logo.
  2. Enclose the counters in a card with a header and description.
  3. Replace the Reset icon with text to clearly convey its functionality.

Adding the Header

Let’s start with the Header component. Let’s start with creating two new files

  • src/components/Header/index.tsx
  • src/components/Header/styles.ts

Let’s implement the code for the Header file. The header structure will consist of a view with a height of 60dp, featuring an icon and text arranged in a horizontal row.

index.tsx – src/components/Header/index.tsx
import {Text, View} from "react-native";
import {styles} from "./styles";
import React from "react";
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'

const Header: React.FC = () => {
  return (
    <View style={styles.container}>
      <MaterialCommunityIcons name="horizontal-rotate-counterclockwise" size={24} color="black"/>
      <Text style={styles.text}>My Counters</Text>
    </View>
  )
}

export default Header;

Update the necessary styles in the src/components/Header/styles.ts

styles.ts – src/components/Header/styles.ts
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  container: {
    height: 60,
    width: '100%',
    paddingHorizontal: 24,
    justifyContent: 'flex-start',
    alignItems: 'center',
    alignContent: 'center',
    borderBottomColor: 'rgba(0, 0, 0, 0.1)',
    borderBottomWidth: 1,
    flexDirection: 'row',
    columnGap: 12
  },
  text: {
    fontSize: 16,
    fontWeight: 'bold'
  }
})

Great! We’ve developed the Header component. Now let’s add it straight to the App.tsx file so we can preview it.

App.tsx
import {StyleSheet, View} from "react-native";
import {StatusBar} from "expo-status-bar";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';

export default function App() {
  return (
    <View style={styles.container}>
      <Header />
      <Counter/>
      <Counter/>
      <StatusBar style="auto"/>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

Oops! It seems we’ve messed up the user interface. What should we do next? First, let’s identify the problem: our header is hidden behind the Status Bar. We need to find a solution for this issue. There are several approaches, such as adding top padding to the Header, but is that the best option?

Approaching the problem the right away

To address this issue, we first identified the problem and implemented a temporary fix by adding padding. However, this isn’t the best approach because solving a problem shouldn’t just involve a quick fix. It’s important to consider solutions that are scalable and maintain the existing conventions.

The proper solution is to use the <SafeAreaView> component from React Native. This component is aware of the safe areas within a mobile application, such as the Status Bar and bottom navigation panel, and it ensures that components don’t overlap with these areas. Therefore, we should replace the View with <SafeAreaView>.

App.tsx
import {SafeAreaView, StyleSheet} from "react-native";
import {StatusBar} from "expo-status-bar";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <Header/>
      <Counter/>
      <Counter/>
      <StatusBar style="auto"/>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

It seems we’ve partially resolved the issue, successfully fixing it on iOS but not on Android. Platform-specific issues are common in React Native. When faced with these, the first step should be to consult the documentation. If the solution is still unclear, StackOverflow is an excellent resource.

Now, to fully address the problem, we can use the StatusBar component from the react-native library, as it is currently being imported from expo-status-bar. We can fix the issue by updating its properties as follows:

  • barStyle={‘dark-content’} – This sets the StatusBar content to use a dark theme.
  • backgroundColor={‘white’} – This applies a white background color to the StatusBar.
App.tsx
import {SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <Header/>
      <Counter/>
      <Counter/>
      <StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

It looks like the issue is resolved! Congratulations on creating your first header.

The Counter Component

Let’s go through the user interface of the new counter we are going to design

Let’s approach the user interface design step by step. Let’s remove all the components and styles from the Counter file.

index.tsx – src/components/Counter
import React, {useState} from "react";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return <></>;
}

export default Counter;
styles.ts – src/components/Header
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({});

Our header has ended up in the middle of the screen, which isn’t the intended behavior. This happened because of the styles in the App.tsx file. Let’s update those styles to make the header appear as expected.

App.tsx
import {SafeAreaView, StatusBar, StyleSheet} from "react-native";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <Header/>
      <Counter/>
      <Counter/>
      <StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
  },
});

Card Container

The card container will be the view that holds the content of the Counter.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {View} from "react-native";
import {styles} from "./styles";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return <View style={styles.cardContainer}></View>;
}

export default Counter;

Let’s update the styles accordingly

styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  }
});

Header Text

The header text will serve as the label for the counter, making it easily identifiable. We’ll use the Text component and place it inside the card container view.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, View} from "react-native";
import {styles} from "./styles";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <Text style={styles.headerText}>Header Text</Text>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  }
});

In the preview above, you can see the Header Text appearing twice, which is expected because the Counter component has been called twice in App.tsx.

Description Text

The description text will provide an explanation of the counter, defining its purpose. We’ll use the Text component once more and place it inside the card container view.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, View} from "react-native";
import {styles} from "./styles";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <Text style={styles.headerText}>Header Text</Text>
      <Text style={styles.descriptionText}>Description Text</Text>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  }
});

Reset Button

The reset button will set the counter back to 0. For the reset button, we’ll use two views: TouchableOpacity to create a custom button instead of using the default Button component, and a Text component for the button label. Let’s start by creating the button and applying the necessary styles.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <Text style={styles.headerText}>Header Text</Text>
      <Text style={styles.descriptionText}>Description Text</Text>
      <TouchableOpacity style={styles.actionButton}></TouchableOpacity>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  },
  actionButton: {
    backgroundColor: "#FFFFFF",
    borderRadius: 8,
    width: 70,
    alignItems: 'center',
    padding: 12
  }
});

Reset Button Text

Let’s add the reset button’s text

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <Text style={styles.headerText}>Header Text</Text>
      <Text style={styles.descriptionText}>Description Text</Text>
      <TouchableOpacity style={styles.actionButton}>
        <Text style={styles.actionText}>Reset</Text>
      </TouchableOpacity>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  },
  actionButton: {
    backgroundColor: "#FFFFFF",
    borderRadius: 8,
    width: 70,
    alignItems: 'center',
    padding: 12
  },
  actionText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center'
  }
});

Action Buttons

The action buttons consist of two buttons: a plus button to increase the counter and a minus button to decrease it. Since both buttons share the same specifications, we can create a reusable component and use it within the Counter component.

To create a reusable component, we can add a new folder named CounterActionButtons inside the src/components directory. In this folder, we’ll follow the same convention used for the Counter component by creating two files: index.tsx for the component logic and styles.ts for the styles.

mkdir src/components/CounterActionButton

Now, let’s create the necessary files

touch src/components/CounterActionButton/index.tsx src/components/CounterActionButton/styles.ts

Add the code in index.tsx for CounterActionButton component

index.tsx – src/components/CounterActionButton
import React from "react";
import {StyleSheet, TouchableOpacity, TouchableOpacityProps} from "react-native";
import {styles} from "./styles";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";

interface CounterActionButtonProps extends TouchableOpacityProps {
  icon: "plus" | "minus"
}

const CounterActionButton: React.FC<CounterActionButtonProps> = ({icon, ...props}) => {
  return (
    <TouchableOpacity
      {...props}
      style={StyleSheet.flatten([
        styles.actionWrapper,
        props.disabled && styles.actionWrapperDisabled
      ])}>
      <MaterialCommunityIcons name={icon} size={24} color={props.disabled ? "gray" : "black"}/>
    </TouchableOpacity>
  )
}

export default CounterActionButton
  • Lines 6 to 8: We created an interface named CounterActionButtonProps that defines the required props for the CounterActionButton component. This interface is also extended with TouchableOpacityProps, which includes the properties of the TouchableOpacity component.
  • Line 10: The CounterActionButton is defined as a functional component that takes CounterActionButtonProps as its props, making it the type for this component. After the equal sign, we destructured the prop object to extract the icon and other properties related to TouchableOpacity.
  • Line 13: We use the spread operator to pass all the props to TouchableOpacity.
  • Line 14: We used StyleSheet.flatten, which merges an array of style objects into a single aggregated style object.
  • Line 16: A condition is added to override the background style when TouchableOpacity is disabled.
  • Line 18: We used the @expo/vector-icons library for icons, utilizing the icon prop to display either a “plus” icon for incrementing or a “minus” icon for decrementing the counter. Additionally, the disabled prop from TouchableOpacity is used, so when it is set to true, the icon color changes, and the button is disabled from performing any action.

Let’s update the styles

styles.ts – src/components/CounterActionButton
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  actionWrapper: {
    width: 30,
    height: 30,
    backgroundColor: 'white',
    borderRadius: 6,
    alignItems: 'center',
    justifyContent: 'center'
  },
  actionWrapperDisabled: {
    backgroundColor: 'lightgray'
  }
})

To render the Reset Button, we need to call it within the Counter component

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <Text style={styles.headerText}>Header Text</Text>
      <Text style={styles.descriptionText}>Description Text</Text>
      <TouchableOpacity style={styles.actionButton}>
        <Text style={styles.actionText}>Reset</Text>
      </TouchableOpacity>
      <CounterActionButton icon={"plus"}/>
      <CounterActionButton icon={"minus"}/>
    </View>
  );
}

export default Counter;

The components are not yet positioned as needed, but we’ll address the layout after all the components are completed.

Counter Text

One of the final components to implement is the Counter Text, which will display the count value. It will increase when incremented, decrease when decremented, and reset when the reset button is pressed. This component will be placed between the Counter Action Buttons, and we’ll use the Text component to display the count value.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <Text style={styles.headerText}>Header Text</Text>
      <Text style={styles.descriptionText}>Description Text</Text>
      <TouchableOpacity style={styles.actionButton}>
        <Text style={styles.actionText}>Reset</Text>
      </TouchableOpacity>
      <CounterActionButton icon={"plus"}/>
      <Text style={styles.counterText}>0</Text>
      <CounterActionButton icon={"minus"}/>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  },
  actionButton: {
    backgroundColor: "#FFFFFF",
    borderRadius: 8,
    width: 70,
    alignItems: 'center',
    padding: 12
  },
  actionText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center'
  },
  counterText: {
    width: 50,
    fontWeight: '600',
    fontSize: 16,
    textAlign: 'center'
  }
});

Working with Placement

Now that we have all the components, the issue is their placement on the screen. To solve this and achieve the desired layout, let’s nest the Header Text and Description Text inside a View component and add some space between them. This will help us organize the components according to the design requirements.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <View style={styles.textWrapper}>
        <Text style={styles.headerText}>Header Text</Text>
        <Text style={styles.descriptionText}>Description Text</Text>
      </View>
      <TouchableOpacity style={styles.actionButton}>
        <Text style={styles.actionText}>Reset</Text>
      </TouchableOpacity>
      <CounterActionButton icon={"plus"}/>
      <Text style={styles.counterText}>0</Text>
      <CounterActionButton icon={"minus"}/>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  textWrapper: {
    rowGap: 6
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  },
  actionButton: {
    backgroundColor: "#FFFFFF",
    borderRadius: 8,
    width: 70,
    alignItems: 'center',
    padding: 12
  },
  actionText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center'
  },
  counterText: {
    width: 50,
    fontWeight: '600',
    fontSize: 16,
    textAlign: 'center'
  }
});

Let’s nest the Reset Button, Counter Action Buttons, and Counter Text inside a View component. We’ll set the flex direction to row to stack these components horizontally, center-align them, and swap the order of the Counter Action Buttons so that the Minus button appears first, followed by the Plus button.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <View style={styles.textWrapper}>
        <Text style={styles.headerText}>Header Text</Text>
        <Text style={styles.descriptionText}>Description Text</Text>
      </View>
      <View style={styles.actionWrapper}>
        <TouchableOpacity style={styles.actionButton}>
          <Text style={styles.actionText}>Reset</Text>
        </TouchableOpacity>
        <CounterActionButton icon={"minus"}/>
        <Text style={styles.counterText}>0</Text>
        <CounterActionButton icon={"plus"}/>
      </View>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16
  },
  textWrapper: {
    rowGap: 6
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  },
  actionWrapper: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center'
  },
  actionButton: {
    backgroundColor: "#FFFFFF",
    borderRadius: 8,
    width: 70,
    alignItems: 'center',
    padding: 12
  },
  actionText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center'
  },
  counterText: {
    width: 50,
    fontWeight: '600',
    fontSize: 16,
    textAlign: 'center'
  }
});

To achieve the desired layout, we’ll nest the Counter Action Buttons and Counter Text inside a View and apply the styles.actionWrapper style to ensure the items are center-aligned and stacked horizontally. Additionally, we’ll update the justifyContent property from center to space-between to create space between the Reset Button and the newly created View containing the Counter Action Buttons and Counter Text. Finally, we’ll add some row spacing in the Card Container to differentiate between the actions and the text content.

index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";

const Counter: React.FC = (): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <View style={styles.textWrapper}>
        <Text style={styles.headerText}>Header Text</Text>
        <Text style={styles.descriptionText}>Description Text</Text>
      </View>
      <View style={styles.actionWrapper}>
        <TouchableOpacity style={styles.actionButton}>
          <Text style={styles.actionText}>Reset</Text>
        </TouchableOpacity>
        <View style={styles.actionWrapper}>
          <CounterActionButton icon={"minus"}/>
          <Text style={styles.counterText}>0</Text>
          <CounterActionButton icon={"plus"}/>
        </View>
      </View>
    </View>
  );
}

export default Counter;
styles.ts – src/components/Counter
import {StyleSheet} from "react-native";

export const styles = StyleSheet.create({
  cardContainer: {
    backgroundColor: '#F2F2F2',
    borderRadius: 8,
    padding: 16,
    rowGap: 16,
  },
  textWrapper: {
    rowGap: 6
  },
  headerText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: 'bold'
  },
  descriptionText: {
    color: "#212121",
    fontSize: 12,
    fontWeight: 'regular'
  },
  actionWrapper: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  actionButton: {
    backgroundColor: "#FFFFFF",
    borderRadius: 8,
    width: 70,
    alignItems: 'center',
    padding: 12
  },
  actionText: {
    color: "#212121",
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center'
  },
  counterText: {
    width: 50,
    fontWeight: '600',
    fontSize: 16,
    textAlign: 'center'
  }
});

To finalize the design, we need to update the App.tsx styles to ensure that the Counters are properly spaced and don’t touch each other or the device borders. We’ll start by nesting both counters within a View and adding appropriate padding and spacing.

App.tsx
import {SafeAreaView, StatusBar, StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <Header/>
      <View style={styles.countersWrapper}>
        <Counter/>
        <Counter/>
      </View>
      <StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
  },
  countersWrapper: {
    padding: 16,
    rowGap: 16
  }
});

Enabling Interactions

To complete the implementation, we’ll add interaction to the components by following these steps:

  1. We’ll create an interface to replace hardcoded values with dynamic props. This allows us to customize the counter’s label, initial value, and other properties.
  2. We’ll modify the Counter component to accept props based on the new interface, making the component flexible.
  3. We’ll add functionality to the increment, decrement, and reset buttons to modify the counter value.
index.tsx – src/components/Counter
import React, {useState} from "react";
import {Text, TouchableOpacity, View} from "react-native";
import {styles} from "./styles";
import CounterActionButton from "../CounterActionButton";

interface CounterProps {
  header: string;
  description: string;
}

const Counter: React.FC<CounterProps> = ({header, description}): React.JSX.Element => {
  const [count, setCount] = useState(0);

  function increment() {
    setCount((prevState) => prevState + 1);
  }

  function decrement() {
    setCount((prevState) => prevState - 1);
  }

  function reset() {
    setCount(0);
  }

  return (
    <View style={styles.cardContainer}>
      <View style={styles.textWrapper}>
        <Text style={styles.headerText}>{header}</Text>
        <Text style={styles.descriptionText}>{description}</Text>
      </View>
      <View style={styles.actionWrapper}>
        <TouchableOpacity style={styles.actionButton} onPress={reset}>
          <Text style={styles.actionText}>Reset</Text>
        </TouchableOpacity>
        <View style={styles.actionWrapper}>
          <CounterActionButton icon={"minus"} onPress={decrement}/>
          <Text style={styles.counterText}>{count}</Text>
          <CounterActionButton icon={"plus"} onPress={increment}/>
        </View>
      </View>
    </View>
  );
}

export default Counter;

Let’s update App.tsx to provide the necessary props for each Counter component to make them dynamic and interactive.

App.tsx
import {SafeAreaView, StatusBar, StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
import Header from './src/components/Header';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <Header/>
      <View style={styles.countersWrapper}>
        <Counter
          header={"Daily Water"}
          description={"Counting my water intake per day"}
        />
        <Counter
          header={"Gaming Hours"}
          description={"Counting my gaming hours per week"}
        />
      </View>
      <StatusBar barStyle={'dark-content'} backgroundColor={'white'}/>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
  },
  countersWrapper: {
    padding: 16,
    rowGap: 16
  }
});

Great to see that the interactions are now working! It sounds like the functional aspect of the Counter components is now properly set up. Since the code updates were focused on making the values dynamic and adding interactions, they might not have visibly altered the UI layout. The layout would remain the same unless additional styles were applied or UI-related properties were adjusted.

At the moment, we’re using just two counters. But imagine if we needed to manage ten counters—updating and maintaining them would become quite cumbersome! What’s the simplest way to handle this? Let’s explore FlatList.

Thank you! 🎉 We’re glad you found the tutorial helpful. If you have any more questions or need further assistance as you start implementing React Native interfaces, feel free to ask. Good luck with your development journey! 🚀



Share with your audience and help them grow too!

About Author

Arslan Mushtaq

A skilled software developer with 10+ years of experience in mobile and web development, proficient in technologies such as React Native, Native Android, and iOS for mobile, and React.js and Next.js for web development. Additional expertise in backend technologies including Node.js, Express.js, Amazon Web Services, and Google Firebase, facilitating collaboration with cross-functional teams across various domains.