👨🏼‍💻 Building a news app with React Native, Expo Router, and Tanstack Query 📰
Tags: react, reactnative, javascript, programming
Published: 2023-12-05 08:00:00

Recently, I wrote about my first React Native application— a mobile calculator and told you to expect more tutorials from me. 

Well, guess what? Here is another one:

<img src="https://media.giphy.com/media/GV3aYiEP8qbao/giphy.gif" alt="Another One" width="100%"/>

A mobile news application that enables users to read and search for news easily within the application. 
At the end of this tutorial, you'll learn how to use Expo Router, query data with Tanstack Query, open webpages within your application, and style your application using Tailwind CSS.

{% link https://dev.to/arshadayvid/building-a-calculator-app-with-react-native-and-tailwind-css-4k7n %}
*** 
## App Demo
To preview the application, download [Expo Go](https://expo.dev/client) app, and paste the links below into the app URL field.

Android: `exp://u.expo.dev/update/3be31284-3908-42e5-ad3e-23bf4823278d`

iOS: `exp://u.expo.dev/update/c98748eb-96a2-4f36-b32d-394b935b3106`

![App Brief Overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6cmo5809icmrtq2fad82.gif)

***
## Project Setup and Installation with Expo
Expo saves us from the complex configurations required to create a native application with the React Native CLI, making it the easiest and fastest way to build and publish React Native apps.

Create an Expo project that uses [Expo Router](https://docs.expo.dev/router/introduction/) by running the code snippet below within your terminal.

```bash
npx create-expo-app@latest --template tabs@49
```
> _Expo Router is an open-source file-based routing system that enables users to navigate between screens easily. It is similar to Next.js, where each file name represents its route name._

Start the development server to ensure the app is working as expected.

```bash
npx expo start
```

Next, delete the `components` and `constants` folders, and clear out everything inside the `app` folder except for the `_layout.tsx` file.

Install the [Expo Linear Gradient](https://www.npmjs.com/package/expo-linear-gradient), [React Native Snap Carousel](https://www.npmjs.com/package/react-native-snap-carousel), and [React Native Webview packages](https://www.npmjs.com/package/react-native-webview). 

Expo Linear Gradient enables us to add colour gradients to React Native elements, React Native Snap Carousel for creating interactive carousels, and React Native WebView for embedding web content within the mobile application.

```bash
npx expo install expo-linear-gradient react-native-webview react-native-snap-carousel @types/react-native-snap-carousel
```

Fix any compatibility issues within the packages by running the command below.

```bash
npx expo install --fix
```
PS: You may encounter this warning while using React Native Snap Carousel:
_`ViewPropTypes will be removed from React Native, along with all other PropTypes. We recommend that you migrate away from PropTypes and switch to a type system like TypeScript. If you need to continue using ViewPropTypes, migrate to the 'deprecated-react-native-prop-types' package.`_

To fix it, navigate into the `node_modules/react-native-snap-carousel` folder and change the `ViewPropTypes` import to be from the deprecated package below.

```bash
npx expo install deprecated-react-native-prop-types
```
***
### Navigating between screens with Expo Router

Within the `app` folder, there is a `_layout.tsx` file that describes the layout (either Screen / Tab) of the files within the folder and allows you to add some custom settings to each screen.

Consider you have a folder containing a `_layout.tsx` file, and you need all the screens to use the React Native Stack layout. You can achieve this using the code snippet below:

```tsx
import { Stack } from "expo-router";

function RootLayoutNav() {
	return (
		<Stack screenOptions={{ headerShown: false }}>
			{/**-- add screens for specific settings --*/}
		</Stack>
	);
}
```

To [navigate between screens](https://docs.expo.dev/router/navigating-pages/), you can use the Link component or `useRouter` hook provided by Expo Router.

```tsx
import { Link, useRouter } from "expo-router";

export default function Page() {
	const router = useRouter();
	const handleClick = () => {
		console.log("Pressed");
		router.push("/screen");
	};
	return (
		<View>
			{/** -- using useRouter hook ---*/}
			<Pressable onPress={handleClick}>
				<View>
					<Text>Hello World</Text>
				</View>
			</Pressable>

			{/** -- using Link component ---*/}
			<Link
				href={{
					pathname: "/news",
					params: {id: "1"},
				}}
				asChild
			>
				<Pressable>
					<View>
						<Text>Hello World</Text>
					</View>
				</Pressable>
			</Link>
		</View>
	);
}
```
***
### Styling Expo applications with Tailwind CSS

Tailwind CSS is a CSS framework that enables us to create modern and stunning applications easily. 

However, to style Expo applications using Tailwind CSS, you need to install [NativeWind](https://www.nativewind.dev/) - a library that uses Tailwind CSS as its scripting language.

Run the code snippet below to install NativeWind and its dependencies.

```bash
yarn add nativewind@^4.0.1 react-native-reanimated
yarn add -D tailwindcss
```
Run `npx tailwindcss init` to create a `tailwind.config.js` file. Update the file with the code snippet below.

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
	content: ["./app/**/*.{js,jsx,ts,tsx}"],
	presets: [require("nativewind/preset")],
	theme: {
		extend: {},
	},
	plugins: [],
};
```
Create a `globals.css` file within the root of your project and add the Tailwind directives below.
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Update the `babel.config.js` file as done below.

```js
module.exports = function (api) {
	api.cache(true);
	return {
		presets: [
			["babel-preset-expo", { jsxImportSource: "nativewind" }],
			"nativewind/babel",
		],
		plugins: [
			// Required for expo-router
			"expo-router/babel",
			"react-native-reanimated/plugin",
		],
	};
};
```
Create a `metro.config.js` file within the root of your project and paste the code snippet below into the file.
```js
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
	// [Web-only]: Enables CSS support in Metro.
	isCSSEnabled: true,
});

module.exports = withNativeWind(config, { input: "./globals.css" });
```

Finally, import the `./globals.css` file into the `app/_layout.tsx` file to enable you to style your application with Tailwind CSS.

```tsx
//👉🏻 Within ./app/_layout.tsx

import "../globals.css";
```
Congratulations, you can start styling your application using Tailwind CSS. If you encounter any issues, you can visit the [documentation](https://www.nativewind.dev/v4/getting-started/expo-router) for a complete how-to guide.

***

## Setting up TanStack Query in React Native
[TanStack Query](https://tanstack.com/query/latest) is a data fetching and state management library that handles API requests effectively within your applications. It provides various features such as caching, auto-refetching, paginated queries, and many others.

Run the code snippet to install TanStack Query to your Expo application.

```bash
yarn add @tanstack/react-query
```

Wrap the entire screens of the application with the [`QueryClientProvider`](https://tanstack.com/query/latest/docs/react/reference/QueryClientProvider) component via the `app/_layout.tsx` file.

```tsx
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";

function RootLayoutNav() {
	const queryClient = new QueryClient();

	return (
		<QueryClientProvider client={queryClient}>
			<Stack screenOptions={{ headerShown: false }}>
				<Stack.Screen name='index' options={{ title: "Home" }} />
			</Stack>
		</QueryClientProvider>
	);
}
```

Now, you can query data using [TanStack Query](https://tanstack.com/query/v4/docs/react/guides/queries).

***
## Building the application interface
In this section, I'll walk you through building the application screens and fetching news from the [News API](https://newsapi.org).

Create an account on the [website](https://newsapi.org) and copy your API token into a `.env.local` file.

```env
EXPO_PUBLIC_NEWS_API_KEY=<your_API_key>
```

![News API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vre2s2jdoxkextc55sgg.png)

Next, create a `fetchNews.ts` file within the assets folder and copy the code snippet below into the file.
```ts
//👇🏻 base URL
const apiBaseUrl = "https://newsapi.org/v2";

//👇🏻 breaking news endpoint
const breakingNewsUrl = `${apiBaseUrl}/top-headlines?country=ng&apiKey=${process
	.env.EXPO_PUBLIC_NEWS_API_KEY!}`;

//👇🏻 recommended news endpoint
const recommendedNewsUrl = `${apiBaseUrl}/top-headlines?country=ng&category=business&apiKey=${process
	.env.EXPO_PUBLIC_NEWS_API_KEY!}`;

//👇🏻 fetch by category  endpoint
const discoverNewsUrl = (discover: string) =>
	`${apiBaseUrl}/top-headlines?country=ng&category=${discover}&apiKey=${process
		.env.EXPO_PUBLIC_NEWS_API_KEY!}`;

//👇🏻 search news endpoint
const searchNewsUrl = (query: string) =>
	`${apiBaseUrl}/everything?q=${query}&apiKey=${process.env
		.EXPO_PUBLIC_NEWS_API_KEY!}`;

//👇🏻 API function call
const newsApiCall = async (endpoints: string) => {
	try {
		const response = await fetch(endpoints);
		const data = await response.json();
		return data;
	} catch (err) {
		console.error(err);
	}
};

//👇🏻 returns breaking news
export const fetchBreakingNews = async () => {
	return await newsApiCall(breakingNewsUrl);
};

//👇🏻 returns recommended news
export const fetchRecommendedNews = async () => {
	return await newsApiCall(recommendedNewsUrl);
};

//👇🏻 returns news based on a category
export const fetchDiscoverNews = async (discover: string) => {
	return await newsApiCall(discoverNewsUrl(discover));
};

//👇🏻 returns search query news
export const fetchSearchNews = async (query: string) => {
	const endpoint = searchNewsUrl(query);
	return await newsApiCall(endpoint);
};
```
The code snippet above fetches the breaking news, recommended news, and discover news from the API endpoint. The search news endpoint enables us to retrieve news based on a given input (query string).

Finally, create a `util.ts` file within the assets folder and copy the code snippet below into the file. It contains variables and functions used within the application.

```ts
//👇🏻 converts the date data from the API to a readable format
export function convertToReadableDate(
	utcDateString: string | undefined
): string {
	if (utcDateString === undefined) return "";
	const utcDate = new Date(utcDateString);
	const options: Intl.DateTimeFormatOptions = {
		year: "numeric",
		month: "long",
		day: "numeric",
	};
	const readableDate: string = utcDate.toLocaleDateString("en-US", options);
	return readableDate;
}

//👇🏻 list of news categories
export const categories: Categories[] = [
	{
		id: "business",
		name: "Business",
		description: "Business news",
		image_url:
			"https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTMyMzYyNg&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		id: "entertainment",
		name: "Entertainment",
		description: "Entertainment news",
		image_url:
			"https://images.unsplash.com/photo-1598743400863-0201c7e1445b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4NjIyMDI3Nw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		id: "general",
		name: "General",
		description: "General news",
		image_url:
			"https://images.unsplash.com/photo-1557992260-ec58e38d363c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTc1MTkwNg&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		id: "health",
		name: "Health",
		description: "Health news",
		image_url:
			"https://images.unsplash.com/photo-1495638488670-437e54b3bab4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTc2MDI3Mw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		id: "science",
		name: "Science",
		description: "Science news",
		image_url:
			"https://images.unsplash.com/photo-1614935151651-0bea6508db6b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTM0MzA0OA&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		id: "sports",
		name: "Sports",
		description: "Sports news",
		image_url:
			"https://images.unsplash.com/photo-1476480862126-209bfaa8edc8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTQ1MTE5NQ&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
];

//👇🏻 images array
const images: Image[] = [
	{
		url: "https://images.unsplash.com/photo-1579532536935-619928decd08?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTM3OTI3Ng&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		url: "https://images.unsplash.com/photo-1482160549825-59d1b23cb208?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTQxNzk3Mg&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		url: "https://plus.unsplash.com/premium_photo-1664297878197-0f50d094db72?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5OTk3MjQ2Ng&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		url: "https://images.unsplash.com/photo-1572375992501-4b0892d50c69?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5OTk3MjUxOQ&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		url: "https://images.unsplash.com/photo-1503694978374-8a2fa686963a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4MTMyMTY5MA&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		url: "https://plus.unsplash.com/premium_photo-1682098211431-6fbbaac9be2c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5OTk3MjYwMQ&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
	{
		url: "https://images.uns.lengthplash.com/photo-1529243856184-fd5465488984?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY4NjA3NzExNA&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=300",
	},
];
//👇🏻 returns a random image for news without an image header
export const generateRandomImage = () => {
	const index = Math.floor(Math.random() * images.length);
	return images[index].url;
};
//👇🏻 Required TypeScript interface
export interface Categories {
	id: string;
	name: string;
	description: string;
	image_url: string;
}
export interface Image {
	url: string;
}
```
***
### App Overview
The application is divided into six screens, including the entry point to the application. Therefore, create a `(tabs)` folder containing a `home.tsx`, `discover.tsx`, and `search.tsx` files.

```bash
cd apps
mkdir (tabs)
touch _layout.tsx home.tsx discover.tsx search.tsx
```
The Home screen displays the breaking and recommended news, The Discover screen allows users to read news based on a particular category, and the Search screen enables users to search for news.

![App Overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bepraum663qzksaufrax.gif)

Next, create a `(stack)` folder containing a `_layout.tsx`,`[title].tsx`, and `news.tsx` files.

```bash
cd apps
mkdir (stack)
touch _layout.tsx [title].tsx news.tsx
```
The `news.tsx` file displays all the news based on a particular category, and the `[title.tsx]` file displays the content of a particular news.

![Display two screens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8uzpr61qlk65r4t680dc.gif)

> _PS: The brackets around the tabs and stack folder names enable us to navigate between screens using the route name instead of the relative path. 
For instance when navigating to the news page, instead of `/(tabs)/news`, you can use `/news` from any point within the application._

Finally, add the stack and tab routes to the `app/_layout.tsx` file.

```tsx
function RootLayoutNav() {
	const queryClient = new QueryClient();

	return (
		<QueryClientProvider client={queryClient}>
			<Stack screenOptions={{ headerShown: false }}>
				<Stack.Screen name='index' options={{ title: "Home" }} />
				<Stack.Screen name='(tabs)' />
				<Stack.Screen name='(stack)' />
			</Stack>
		</QueryClientProvider>
	);
}
```
*** 
### The Welcome Screen
The welcome screen displays the content of the `index.tsx` file, showing a brief overview of the application and a button that redirects users to the home screen.

![Welcome Screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wcmluxvkjckjj8mmx4a7.png)

Update the `app/index.tsx` file with the code snippet below:

```tsx
import { ImageBackground, Pressable, Text, View } from "react-native";
import { useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { LinearGradient } from "expo-linear-gradient";

export default function TabOneScreen() {
	const router = useRouter();
	return (
		<ImageBackground
			source={require("../assets/images/background.jpg")}
			className='flex-1 items-center justify-center pb-10 bg-gradient-to-bl from-gray-200 to-gray-900'
		>
			<LinearGradient
				colors={["transparent", "rgba(0,0,0,0.9)"]}
				style={{
					position: "absolute",
					bottom: 0,
					width: "100%",
					height: "100%",
				}}
				start={{ x: 0.5, y: 0 }}
				end={{ x: 0.5, y: 1 }}
			/>
			<View className='absolute bottom-14 flex flex-col items-center justify-center w-full  bg-gradient-to-t from-gray-900 px-4'>
				<Text
					className='text-4xl text-white font text-center mb-4'
					style={{ fontFamily: "Bold" }}
				>
					Breaking Boundaries, Breaking News
				</Text>

				<Text
					className='text-gray-300 text-center text-xl mb-6'
					style={{ fontFamily: "Medium" }}
				>
					Explore the world through our lens. Your passport to a connected and
					informed world, right at your fingertips.
				</Text>
				<Pressable
					onPress={() => router.push("/home")}
					className='bg-stone-700 rounded-full p-4 w-full items-center justify-center shadow-lg'
				>
					<Text
						className='text-white text-2xl'
						style={{ fontFamily: "Medium" }}
					>
						Get Started
					</Text>
				</Pressable>
			</View>
			<StatusBar style='light' />
		</ImageBackground>
	);
}
```
***
### The Tab Screens
It contains the Home, Discover, and Search screens. Update the `(tabs)/_layout.tsx` file to display the tab icons and default settings for each screen within the Tab layout.

```tsx
import { Tabs } from "expo-router";
import { FontAwesome5, MaterialIcons, FontAwesome } from "@expo/vector-icons";

export default function Page() {
	return (
		<Tabs
			screenOptions={{
				tabBarShowLabel: false,
				tabBarActiveBackgroundColor: "#fff",
				tabBarActiveTintColor: "#a16207",
				headerShown: false,
			}}
		>
			<Tabs.Screen
				name='home'
				options={{
					title: "Home",
					tabBarIcon: ({ color }) => (
						<FontAwesome5 name='home' size={24} color={color} />
					),
				}}
			/>
			<Tabs.Screen
				name='discover'
				options={{
					title: "Discover",
					tabBarIcon: ({ color }) => (
						<MaterialIcons name='explore' size={24} color={color} />
					),
				}}
			/>

			<Tabs.Screen
				name='search'
				options={{
					title: "Search",
					tabBarIcon: ({ color }) => (
						<FontAwesome name='search' size={24} color={color} />
					),
				}}
			/>
		</Tabs>
	);
}
```

![Tab layout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mriwmjordzw6f71xv8d9.gif)

Import the functions from the `fetchNews.ts` file declared earlier, execute the functions using TanStack Query, and display the results within the Home screen.

```tsx
import { FlatList, StatusBar } from "react-native";
import Carousel from "react-native-snap-carousel";
import { useQuery } from "@tanstack/react-query";
import {
	fetchBreakingNews,
	fetchRecommendedNews,
} from "../../assets/fetchNews";

export default function Page() {
	//👇🏻 fetch the breaking news
	const breakingNewsQuery = useQuery({
		queryKey: ["breakingNews"],
		queryFn: fetchBreakingNews,
	});
	//👇🏻 fetch the recommended news
	const recommendedNewsQuery = useQuery({
		queryKey: ["recommendedNews"],
		queryFn: fetchRecommendedNews,
	});
	return (
		<SafeAreaView className='flex-1'>
			<View>
				{breakingNewsQuery.data && (
					<Carousel
						data={breakingNewsQuery.data.articles}
						renderItem={renderBreakingNewsItem}
						firstItem={1}
						inactiveSlideScale={0.86}
						sliderWidth={width}
						itemWidth={width * 0.8}
						slideStyle={{ display: "flex", alignItems: "center" }}
					/>
				)}
			</View>

			<View>
				{recommendedNewsQuery.data && (
					<FlatList
						data={recommendedNewsQuery.data.articles}
						renderItem={renderRecommendedNewsItem}
						showsVerticalScrollIndicator={false}
						keyExtractor={(item, index) => item.url}
					/>
				)}
			</View>

			<StatusBar style='dark' />
		</SafeAreaView>
	);
}
```

Create the functions that render each result within the Carousel and the Flatlist.

```tsx
//👇🏻 Renders the Breaking News UI (horizontal row)
const renderBreakingNewsItem = ({ item }: any) => {
	return (
		<Link
			href={{
				pathname: "/[title]",
				params: {
					data: JSON.stringify([item.url, item.title]),
				},
			}}
			asChild
		>
			<Pressable>
				<View className='relative'>
					<Image
						source={{ uri: item.urlToImage || generateRandomImage() }}
						style={{
							width: width * 0.8,
							height: height * 0.22,
							borderRadius: 10,
						}}
						resizeMode='cover'
						className='rounded-3xl'
					/>
					<LinearGradient
						colors={["transparent", "rgba(0,0,0,0.9)"]}
						start={{ x: 0.5, y: 0 }}
						end={{ x: 0, y: 1 }}
						style={{
							position: "absolute",
							bottom: 0,
							width: "100%",
							height: "100%",
							borderBottomLeftRadius: 24,
							borderBottomRightRadius: 24,
						}}
					/>

					<View className='absolute bottom-0 left-4 right-0 justify-end h-[80%] px-4 pb-4'>
						<Text
							className='text-xl text-white mb-2'
							style={{ fontFamily: "Bold" }}
						>
							{item.title.length > 48
								? item.title.slice(0, 47) + "..."
								: item.title}
						</Text>
						<Text className=' text-stone-200' style={{ fontFamily: "Medium" }}>
							{item.author}
						</Text>
					</View>
				</View>
			</Pressable>
		</Link>
	);
};

//👇🏻 Renders the Recommended News UI (vertical row)
const renderRecommendedNewsItem = ({ item }: any) => {
	return (
		<Link
			href={{
				pathname: "/[title]",
				params: {
					data: JSON.stringify([item.url, item.title]),
				},
			}}
			asChild
		>
			<Pressable className='px-4 w-full'>
				<View className='flex flex-row items-center justify-between w-full mb-4 bg-white shadow-xl rounded-xl'>
					<Image
						source={{ uri: item.urlToImage || generateRandomImage() }}
						style={{
							width: width * 0.4,
							height: width * 0.3,
							borderRadius: 5,
						}}
						resizeMode='cover'
						className='rounded-3xl mr-[1px]'
					/>

					<View className='px-3 flex-1'>
						<Text
							style={{ fontFamily: "Medium" }}
							className='text-stone-500 text-sm'
						>
							{item.author}
						</Text>
						<Text className='text-lg mb-[1px]' style={{ fontFamily: "Bold" }}>
							{item.title.length > 48
								? item.title.slice(0, 47) + "..."
								: item.title}
						</Text>
						<Text
							style={{ fontFamily: "Medium" }}
							className='text-stone-500 text-sm'
						>
							{convertToReadableDate(item.publishedAt)}
						</Text>
					</View>
				</View>
			</Pressable>
		</Link>
	);
};
```

The `discover.tsx` file displays the news categories within the application and redirects users to the `(stack)/news` screens containing all the news under that category. Copy the code snippet below into the `discover.tsx` file to render the news categories:

```tsx
import { Categories, categories } from "../../assets/util";

export default function Page() {
	return (
		<View className='rounded-2xl shadow-xl'>
			<FlatList
				data={categories}
				renderItem={renderItem}
				keyExtractor={(item, index) => item.id}
				numColumns={2}
				contentContainerStyle={{
					justifyContent: "space-between",
					alignItems: "center",
					padding: 10,
					width: "100%",
				}}
			/>
		</View>
	);
}
```
The `renderItem` function below represents the layout of each item rendered within the FlatList.

```tsx
const renderItem = ({ item }: { item: Categories }) => {
	return (
		<Link
			href={{
				pathname: "/news",
				params: {
					category: item.id,
				},
			}}
			asChild
		>
			<Pressable>
				<View className='relative m-[7px]'>
					<Image
						source={{ uri: item.image_url }}
						style={{
							width: width * 0.47,
							height: width * 0.45,
							borderRadius: 10,
						}}
						resizeMode='cover'
						className='rounded-xl'
					/>
					<LinearGradient
						colors={["transparent", "rgba(0,0,0,0.9)"]}
						start={{ x: 0.5, y: 0 }}
						end={{ x: 0, y: 1 }}
						style={{
							position: "absolute",
							bottom: 0,
							width: "100%",
							height: "100%",
							borderBottomLeftRadius: 20,
							borderBottomRightRadius: 20,
						}}
					/>
					<View className='absolute bottom-0 left-4 right-0 justify-end h-[80%] px-4 pb-4'>
						<Text
							className='text-xl text-white mb-2'
							style={{ fontFamily: "Bold" }}
						>
							{item.name}
						</Text>
					</View>
				</View>
			</Pressable>
		</Link>
	);
};
```

Update the `search.tsx` file to enable users enter an input to the search field and displays the results within a FlatList.

```tsx
import {
	View,
	Text,
	Pressable,
	Image,
	Dimensions,
	FlatList,
} from "react-native";
import { Link } from "expo-router";
import { SafeAreaView } from "react-native-safe-area-context";
import { TextInput } from "react-native-gesture-handler";
import { FontAwesome, MaterialIcons } from "@expo/vector-icons";
import { useState } from "react";
import { fetchSearchNews } from "../../assets/fetchNews";
import {
	News,
	convertToReadableDate,
	generateRandomImage,
} from "../../assets/util";
const { width } = Dimensions.get("window");

export default function Page() {
	const [query, onChangeQuery] = useState<string>("");
	const [results, setResults] = useState<any[]>([]);
	const [resultsCount, setResultsCount] = useState<number>(0);

	const handleTextChange = (text: string) => {
		onChangeQuery(text);
		if (text.length > 2) {
			fetchSearchNews(text).then((res) => {
				setResults(res.articles);
				setResultsCount(res.totalResults);
			});
		}
	};

	return (
		<SafeAreaView>
			<View className='px-4 '>
				<Text
					className='text-3xl text-stone-500 mb-3'
					style={{ fontFamily: "Bold" }}
				>
					Search
				</Text>
				<View className='flex flex-row items-center justify-between w-full rounded-2xl bg-gray-100 border-[1px] px-3 border-stone-300'>
					<FontAwesome name='search' size={24} color='gray' className='mr-2' />
					<TextInput
						className='flex-1  
				rounded-xl px-4 py-4'
						placeholder='Search for news'
						style={{ fontFamily: "Medium" }}
						value={query}
						onChangeText={(text) => handleTextChange(text)}
					/>
				</View>
				<Text className='text-lg mt-4 mb-4' style={{ fontFamily: "Semibold" }}>
					Total Results: {resultsCount}
				</Text>

				<View>
					{results && (
						<FlatList
							data={results}
							renderItem={newsItem}
							showsVerticalScrollIndicator={false}
							keyExtractor={(item) => item.url}
						/>
					)}
				</View>
			</View>
		</SafeAreaView>
	);
}
```

The results are rendered via a `newItem` component created as shown below.

```tsx
export interface News {
	title: string;
	url: string;
	image?: string;
	publishedAt?: string;
	author?: string;
	urlToImage?: string;
}

const newsItem = ({ item }: { item: News }) => {
	return (
		<Link
			href={{
				pathname: "/[title]",
				params: {
					url: item.url,
					title: item.title,
				},
			}}
			asChild
		>
			<Pressable className='px-4 w-full'>
				<View className='flex flex-row items-center justify-between w-full mb-4 bg-white shadow-xl rounded-xl p-3'>
					<Image
						source={{ uri: item.urlToImage || generateRandomImage() }}
						style={{ width: width * 0.2, height: width * 0.2, borderRadius: 5 }}
						resizeMode='cover'
						className='rounded-3xl mr-[1px]'
					/>

					<View className='px-3 flex-1'>
						<Text
							style={{ fontFamily: "Medium" }}
							className='text-stone-500 text-sm'
						>
							{item.author}
						</Text>
						<Text className='text-lg mb-[1px]' style={{ fontFamily: "Bold" }}>
							{item.title.length > 48
								? item.title.slice(0, 47) + "..."
								: item.title}
						</Text>
						<Text
							style={{ fontFamily: "Medium" }}
							className='text-stone-500 text-sm'
						>
							{convertToReadableDate(item.publishedAt)}
						</Text>
					</View>
					<MaterialIcons name='keyboard-arrow-right' size={26} color='brown' />
				</View>
			</Pressable>
		</Link>
	);
};
```


![Search Screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxxw90kqc4kh2i5e28hs.gif)
***
### The Stack Screens
Update the `(stack)/_layout.tsx` file to display its files using the Stack layout.

```tsx
import { Stack } from "expo-router";

export default function Page() {
	return <Stack></Stack>;
}
```
Modify the `news.tsx` file to display the list of news based on a chosen category. When a user selects a news category, the user is redirected to the news route where the API results (category news) are displayed.

```tsx
export default function Page() {
	const { category }: {category: string} = useLocalSearchParams();

	if (category === "breaking") {
		const breakingNewsQuery = useQuery({
			queryKey: ["breakingNews"],
			queryFn: fetchBreakingNews,
		});
		return <DisplayNews news={breakingNewsQuery} title='Breaking News' />;

	} else if (category === "recommended") {
		const recommendedNewsQuery = useQuery({
			queryKey: ["recommendedNews"],
			queryFn: fetchRecommendedNews,
		});
		return <DisplayNews news={recommendedNewsQuery} title='Recommended News' />;
		
	} else {
		const discoverNewsQuery = useQuery({
			queryKey: ["discoverNews", category],
			queryFn: () => fetchDiscoverNews(category),
		});
		return (
			<DisplayNews
				news={discoverNewsQuery}
				title={`${category[0].toUpperCase() + category.slice(1)} News`}
			/>
		);
	}
}
```
The `DisplayNews` component above displays the results in a FlatList.


![DiplayNews](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tkmh9wco5fr5hb716c9.gif)
***
## How to open web resources with React Native WebView
[React Native WebView](https://www.npmjs.com/package/react-native-webview) enables us to embed web content, such as HTML, CSS, and JavaScript, within a mobile app. It provides a way to display web pages in a React Native application.

Copy the code snippet below into the `[title].tsx` file:
```tsx
import {
	View,
	Pressable,
	ActivityIndicator,
	Dimensions,
	Text,
} from "react-native";
import React, { useState } from "react";
import { AntDesign } from "@expo/vector-icons";
import { WebView } from "react-native-webview";
import { Stack, router, useLocalSearchParams } from "expo-router";
const { width, height } = Dimensions.get("window");

export default function Page() {
	const [visible, setVisible] = useState<boolean>(false);
	const params = useLocalSearchParams();
	const data: [string, string] = JSON.parse(params.data)
	const pageTitle = data[1];
	const pageURL = data[0]
	
	return (
		<>
			<Stack.Screen options={{ headerTitle: `${pageTitle}` }} />
			<View className='pt-4 p-6 flex flex-row justify-between items-center bg-stone-200 fixed top-0'>
				<Pressable
					className='bg-stone-100 rounded-xl p-3 shadow-2xl'
					onPress={() => router.back()}
				>
					<AntDesign name='back' size={28} color='brown' />
				</Pressable>
				<Text style={{ fontFamily: "Medium" }}>Sponsored by Global Pulse </Text>
			</View>
			<WebView
				style={{ flex: 1 }}
				source={{ uri: pageURL }}
				onLoadStart={() => setVisible(true)}
				onLoadEnd={() => setVisible(false)}
			/>
			{visible && (
				<ActivityIndicator
					size={"large"}
					color={"green"}
					style={{
						position: "absolute",
						top: height / 2,
						left: width / 2,
					}}
				/>
			)}
		</>
	
	);
}

```
- From the code snippet above,
  - the `[title].tsx` route accepts the news title and source URL from the Link component.
  - the WebView component from React Native WebView accepts the news source URL and displays the news content within the app.
  - the `Stack.Screen` component changes its header title to the news title.

![News View](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pqh0a335p1qstu2jypix.gif)

Congratulation!🎉 You've completed the project for this tutorial.

***
## Conclusion
So far, you've learnt how to do the following:
- Create a mobile application with Expo, 
- Install and navigate between screens with Expo Router, 
- Style React Native applications with TailwindCSS, and
- Open web pages within a mobile application.

If you prefer a video format, check out this tutorial on YouTube:
{% embed https://youtu.be/hnoullGuo74 %}

The source code for this tutorial is available here: 
[https://github.com/dha-stix/global-pulse-app](https://github.com/dha-stix/global-pulse-app)

Thank you for reading!🎉
***

### Open to work🙂
Did you enjoy this article or need an experienced Technical Writer / React Developer for a remote, full-time, or contract-based role? Feel free to contact me.
[GitHub](https://github.com/dha-stix) ||  [LinkedIn](https://www.linkedin.com/in/david-asaolu234/) ||  [Twitter](https://twitter.com/dayvid_JS)

[![Buy David a coffee](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cinwy8wfhl6ra51t6ml5.png)](https://www.buymeacoffee.com/asaoludavid)
<img src="https://media.giphy.com/media/fVtcfEXWQJQUbsF1sH/giphy.gif" alt="Thank You" width="auto" height="auto"/>