Mastering Reusability in React Native
Are you frustrated with the constant need to write repetitive code and manage values manually? It’s time to embrace one of the core principles of programming: DRY (Don’t Repeat Yourself). However, this raises the question—how can you avoid repetition when the same functionality is required multiple times?
Let’s proceed with the Counter Application example. You have the option to either follow the tutorial from the beginning or clone the project and start from this point.
In the Counter application, suppose we decide to include two counters—one at the top and another at the bottom. We could either duplicate the existing counter and style it accordingly, or we could create a reusable component that can be utilized multiple times on a single screen. Let’s take the latter approach.
Structuring the application
Let’s begin by organizing things in a logical and easy-to-find manner. First, create a folder named src in the root of the project, and within that, create another folder called components since the counter is a reusable component. You can create these folders manually or by using the following command.
mkdir -p src/components
Isolating the Counter
Next, create a folder named Counter inside the newly created components folder, and then add two files within the Counter folder.
index.tsx
– The entry file for the componentstyles.ts
– Stylesheet for the counter
Using the following command to create the Counter folder:
mkdir src/components/Counter
Using the following command let’s create the necessary files:
touch src/components/Counter/index.tsx src/components/Counter/styles.ts
Creating the Counter functional component
Now, let’s set up the boilerplate code for the Counter functional component. Add the following code to the index.tsx
file located in the src/components/Counter
folder.
import React from "react";
const Counter: React.FC = (): React.JSX.Element => {
return <></>
}
export default Counter;
Copy the original component
Currently, the code in the index.tsx
file doesn’t properly represent the Counter as needed. We’ll need to copy it from App.tsx
. Please move the following lines from App.tsx
to src/components/Counter/index.tsx
.
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
export default function App() {
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.container}>
<Text style={styles.count}>{count}</Text>
<View style={styles.actionWrapper}>
<Button title="Decrease" onPress={decrement} disabled={count === 0} />
<Button title="Increase" onPress={increment} />
</View>
<Button title="Reset" onPress={reset} disabled={count === 0} />
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
rowGap: 24,
},
actionWrapper: {
flexDirection: "row",
columnGap: 18,
},
count: {
fontSize: 24,
fontWeight: "bold",
},
});
import React from "react";
import {Button, Text, View} from "react-native";
const Counter: React.FC = (): React.JSX.Element => {
return (
<View style={styles.container}>
<Text style={styles.count}>{count}</Text>
<View style={styles.actionWrapper}>
<Button title="Decrease" onPress={decrement} disabled={count === 0} />
<Button title="Increase" onPress={increment} />
</View>
<Button title="Reset" onPress={reset} disabled={count === 0} />
</View>
)
}
export default Counter;
Copy styles into styles.ts
We’re seeing errors related to the styles variable, which makes sense since we haven’t defined styles for the counter yet. To fix this, let’s copy the necessary styles into the styles.ts
file.
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
export default function App() {
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.container}>
<Text style={styles.count}>{count}</Text>
<View style={styles.actionWrapper}>
<Button title="Decrease" onPress={decrement} disabled={count === 0} />
<Button title="Increase" onPress={increment} />
</View>
<Button title="Reset" onPress={reset} disabled={count === 0} />
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
rowGap: 24,
},
actionWrapper: {
flexDirection: "row",
columnGap: 18,
},
count: {
fontSize: 24,
fontWeight: "bold",
},
});
import {StyleSheet} from "react-native";
export const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
rowGap: 24,
},
actionWrapper: {
flexDirection: "row",
columnGap: 18,
},
count: {
fontSize: 24,
fontWeight: "bold",
},
});
After copying the styles, we need to export the styles so we can use it in index.tsx
import React from "react";
import {Button, Text, View} from "react-native";
import {styles} from "./styles";
const Counter: React.FC = (): React.JSX.Element => {
return (
<View style={styles.container}>
<Text style={styles.count}>{count}</Text>
<View style={styles.actionWrapper}>
<Button title="Decrease" onPress={decrement} disabled={count === 0} />
<Button title="Increase" onPress={increment} />
</View>
<Button title="Reset" onPress={reset} disabled={count === 0} />
</View>
)
}
export default Counter;
The remaining issue with the component is handling the values that need to be passed from the parent component as properties, also known as props. Let’s create an interface for that.
import React from "react";
import {Button, Text, View} from "react-native";
import {styles} from "./styles";
interface CounterProps {
count: number;
decrement: () => void;
increment: () => void;
reset: () => void;
}
const Counter: React.FC<CounterProps> = ({count, decrement, increment, reset}): React.JSX.Element => {
return (
<View style={styles.container}>
<Text style={styles.count}>{count}</Text>
<View style={styles.actionWrapper}>
<Button title="Decrease" onPress={decrement} disabled={count === 0}/>
<Button title="Increase" onPress={increment}/>
</View>
<Button title="Reset" onPress={reset} disabled={count === 0}/>
</View>
)
}
export default Counter;
An interface serves as a blueprint for the properties of a component. To link an interface to a component, we use the generic React.FC<T>
, where T
represents the interface or type of the component, as shown on line 12. Now, let’s update our App.tsx
to include the Counter component.
import {StatusBar} from "expo-status-bar";
import {useState} from "react";
import {StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
export default function App() {
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.container}>
<Counter count={count} decrement={decrement} increment={increment} reset={reset}/>
<StatusBar style="auto"/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
We imported our Counter from the components folder and replaced all instances of the Counter components with this single Counter component. The component’s properties have been passed appropriately.
- The
counter
value is derived from the state created in theApp.tsx
component. - The increment, decrement, and reset functions are passed as values to their respective props from the parent
App.tsx
component.
To test if the counter functions as expected, run the application and try increasing, decreasing, and resetting the values. Everything should work fine so far, but how can we make it reusable? Simply copy and paste the Counter component below, as demonstrated.
import {StatusBar} from "expo-status-bar";
import {useState} from "react";
import {StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
export default function App() {
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.container}>
<Counter count={count} decrement={decrement} increment={increment} reset={reset}/>
<Counter count={count} decrement={decrement} increment={increment} reset={reset}/>
<StatusBar style="auto"/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
Awesome! We have created 2 counters with a single component but there is still another issue. Can you guess? Yes! you might have guessed it right. If you click the increase button, you will see that both the counters increment the counter at a single time. The problem is as follows
We need to isolate the Counter component by giving it its own counter state, along with the increment, decrement, and reset functions. Since the components are now reusable, we can move the counter state, increment, decrement, and reset functions directly into the Counter component.
import React, {useState} from "react";
import {Button, 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.container}>
<Text style={styles.count}>{count}</Text>
<View style={styles.actionWrapper}>
<Button title="Decrease" onPress={decrement} disabled={count === 0}/>
<Button title="Increase" onPress={increment}/>
</View>
<Button title="Reset" onPress={reset} disabled={count === 0}/>
</View>
)
}
export default Counter;
Now, let’s update App.tsx
import {StatusBar} from "expo-status-bar";
import {StyleSheet, View} from "react-native";
import Counter from "./src/components/Counter";
export default function App() {
return (
<View style={styles.container}>
<Counter/>
<Counter/>
<StatusBar style="auto"/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
As you can see, isolating the Counter component along with its functionality has significantly minimized the code.
Congratulations 🎉 you’ve just created your first reusable component! This approach, where components are isolated for reusability, is a common practice among developers. It’s not limited to just React Native or React. You can access the completed project at the following link
https://github.com/devnur-org/rn-expo-counter/tree/reusable-components