Cookie Consent by Free Privacy Policy Generator 📌 👨🏼‍💻 Building a news app with React Native, Expo Router, and Tanstack Query 📰

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 👨🏼‍💻 Building a news app with React Native, Expo Router, and Tanstack Query 📰


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

Recently, I wrote about my first React Native application—a mobile calculator—and mentioned that you can should expect more React Native tutorials from me.

Well, guess what? Here is another one:

Another One

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.

App Demo

To preview the application, download Expo Go 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

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 by running the code snippet below within your terminal.

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.

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, React Native Snap Carousel, and React Native Webview packages.

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.

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.

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.

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:

import { Stack } from "expo-router";

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

To navigate between screens, you can use the Link component or useRouter hook provided by Expo Router.

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 - a library that uses Tailwind CSS as its scripting language.

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

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.

/** @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.

@tailwind base;
@tailwind components;
@tailwind utilities;

Update the babel.config.js file as done below.

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.

// 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.

//👉🏻 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 for a complete how-to guide.

Setting up TanStack Query in React Native

TanStack Query 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.

yarn add @tanstack/react-query

Wrap the entire screens of the application with the QueryClientProvider component via the app/_layout.tsx file.

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.

Building the application interface

In this section, I'll walk you through building the application screens and fetching news from the News API.

Create an account on the website and copy your API token into a .env.local file.

EXPO_PUBLIC_NEWS_API_KEY=<your_API_key>

News API

Next, create a fetchNews.ts file within the assets folder and copy the code snippet below into the file.

//👇🏻 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.

//👇🏻 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.

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

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

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

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.

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

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

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.

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

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

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.

//👇🏻 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:

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.

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.

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.

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

The Stack Screens

Update the (stack)/_layout.tsx file to display its files using the Stack layout.

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.

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

How to open web resources with React Native WebView

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:

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

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 tutorial on YouTube:

The source code for this tutorial is available here:
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 || LinkedIn || Twitter

Buy David a coffee
Thank You

...



📌 👨🏼‍💻 Building a news app with React Native, Expo Router, and Tanstack Query 📰


📈 95.36 Punkte

📌 This Week In React #146: Concurrency, Server Components, Next.js, React-Query, Remix, Expo Router, Skia, React-Native...


📈 70.66 Punkte

📌 Building a multi-tenant B2B SaaS with Vite and Tanstack Router & Query - Part 1: The boilerplate


📈 52.2 Punkte

📌 Setup Your Universal App with React Native, Expo Router, Tamagui, and Storybook


📈 46.67 Punkte

📌 TANStack Query: How It Changes the Way You Query APIs


📈 46.49 Punkte

📌 Tanstack's React Query Kicked `onSuccess`, `onError`, and `onSettled` Out of `useQuery`: Now What?!


📈 45.71 Punkte

📌 How to Use TanStack-Query to Write Cleaner React Code as a Junior Developer


📈 43.92 Punkte

📌 Building a Modern Document Website for React Native Library Like React Native ECharts


📈 43.4 Punkte

📌 TanStack Router: Query Parameters & Validators


📈 42.47 Punkte

📌 Fluxo de autenticação no React Native usando Expo Router


📈 41.83 Punkte

📌 Building a CRUD App with Next.js, React Query, React Hook Form, and Yup


📈 41.61 Punkte

📌 This Week In React #176: React Compiler, TanStack, Vinxi, Remix i18n, Next.js security, Shopify RN perf, Uni Stack...


📈 41.35 Punkte

📌 What is react-query? Why should we use react-query?


📈 40.15 Punkte

📌 TanStack Router: Setup & Routing in React


📈 39.9 Punkte

📌 BUILD YOUR FIRST APP WITH REACT NATIVE AND EXPO


📈 39.37 Punkte

📌 How to Save and Play Audio in React Native Expo App


📈 39.37 Punkte

📌 How to Add Tailwind CSS to Your React Native Expo App


📈 37.59 Punkte

📌 Build a Cross-Platform Messaging App with React Native Expo


📈 37.59 Punkte

📌 🧪 How to set up Jest & React Native Testing Library in your Expo App! 💥


📈 37.59 Punkte

📌 This Week In React #127: Nextra, React-Query, React Documentary, Storybook, Remix, Tamagui, Solito, TC39, Rome...


📈 37.58 Punkte

📌 react-native-awesome-template: A React Native starter template to have a better product and development experience


📈 37.24 Punkte

📌 Simple mutations with TanStack Query and Next.js


📈 36.95 Punkte

📌 A Deep Dive into Angular and Tanstack Query with Our Demo Store


📈 36.95 Punkte

📌 Initialize A React Native And Expo CLI Project


📈 36.31 Punkte

📌 The State of React Native Tooling (React Native CLI - The Ultimate Guide)


📈 35.46 Punkte

📌 React Native Networking – How To Perform API Requests In React Native using the FetchAPI


📈 35.46 Punkte

📌 How to Implement Face Detection in React Native Using React Native Vision Camera


📈 35.46 Punkte

📌 This Week In React #139: React.dev, Remix, Server Components, Error Boundary, Wakuwork, React-Native, Bottom Sheet...


📈 35.23 Punkte

📌 Using TanStack Query with Next.js


📈 35.17 Punkte

📌 From Prisma to TanStack Query: Fast Lane to Full-Stack Type Safety


📈 35.17 Punkte

📌 CVE-2024-24558 | TanStack Query prior 5.18.0 cross site scripting (GHSA-997g-27x8-43rf)


📈 35.17 Punkte

📌 This is your sign(al) to try TanStack Query & Angular


📈 35.17 Punkte











matomo