Skip to content

Commit b060e2e

Browse files
committed
feat: add data fetching
* added and configured TanStack Query * replaced placeholder data with fetches to OpenWeather
1 parent ccda50c commit b060e2e

12 files changed

+166
-52
lines changed

App.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {
33
StaticParamList,
44
} from '@react-navigation/native';
55
import { createNativeStackNavigator } from '@react-navigation/native-stack';
6+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
67
import React from 'react';
7-
import Details from './src/screens/Details';
8+
import Details, { DetailsRouteParams } from './src/screens/Details';
89
import Home from './src/screens/Home';
910

1011
const RootStack = createNativeStackNavigator({
@@ -18,13 +19,13 @@ const RootStack = createNativeStackNavigator({
1819
Details: {
1920
screen: Details,
2021
options: ({ route }) => ({
21-
title: route.params?.name,
22+
title: (route.params as DetailsRouteParams).name,
2223
}),
2324
},
2425
},
2526
});
2627

27-
type RootStackParamList = StaticParamList<typeof RootStack>;
28+
export type RootStackParamList = StaticParamList<typeof RootStack>;
2829

2930
declare global {
3031
namespace ReactNavigation {
@@ -34,6 +35,12 @@ declare global {
3435

3536
const Navigation = createStaticNavigation(RootStack);
3637

38+
const queryClient = new QueryClient();
39+
3740
export default function App() {
38-
return <Navigation />;
41+
return (
42+
<QueryClientProvider client={queryClient}>
43+
<Navigation />
44+
</QueryClientProvider>
45+
);
3946
}

package-lock.json

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@react-navigation/native": "^7.0.0",
1414
"@react-navigation/native-stack": "^7.0.0",
1515
"@shopify/flash-list": "^1.7.1",
16+
"@tanstack/react-query": "^5.59.20",
1617
"react": "18.3.1",
1718
"react-native": "0.76.1",
1819
"react-native-config": "^1.5.3",

src/api/forecast.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1491,8 +1491,8 @@ export const sampleForecastResponse: ForecastResponse = {
14911491
},
14921492
};
14931493

1494-
export async function fetchForecast(city: string | number) {
1494+
export async function fetchForecast(name: string) {
14951495
return fetchData<ForecastResponse>(
1496-
`https://api.openweathermap.org/data/2.5/forecast?q=${city}`,
1496+
`https://api.openweathermap.org/data/2.5/forecast?q=${name}`,
14971497
);
14981498
}

src/api/group.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export const sampleGroupWeather: GroupResponse = {
149149
],
150150
};
151151

152-
export async function getGroupWeather(ids: number[]) {
152+
export async function fetchGroupWeather(ids: number[]) {
153153
return fetchData<GroupResponse>(
154154
`https://api.openweathermap.org/data/2.5/group?id=${ids.join(',')}`,
155155
);

src/api/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ export type City = {
164164
name: string;
165165
coord?: Coord;
166166
local_names?: Record<`${string}${string}`, string>;
167-
lat: number;
168-
lon: number;
167+
lat?: number;
168+
lon?: number;
169169
country: CountryCode;
170170
state?: string;
171171
population?: number;

src/data/queries.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { fetchGroupWeather } from '../api/group';
3+
import { fetchForecast } from '../api/forecast';
4+
5+
const cityIds = [728193, 3081368, 2643743, 5391959];
6+
7+
export function useCityListWeather() {
8+
return useQuery({
9+
queryKey: ['weather'],
10+
queryFn: () => fetchGroupWeather(cityIds),
11+
staleTime: 60 * 1000,
12+
});
13+
}
14+
15+
export function useCityWeather(id: number) {
16+
return useQuery({
17+
queryKey: ['cities'],
18+
queryFn: () => fetchGroupWeather(cityIds),
19+
staleTime: 60 * 1000,
20+
select: data => data.list.find(c => c.id === id),
21+
});
22+
}
23+
24+
export function useCityForecast(name: string) {
25+
return useQuery({
26+
queryKey: ['forecast', name],
27+
queryFn: () => fetchForecast(name),
28+
staleTime: 60 * 60 * 1000,
29+
});
30+
}

src/screens/CityForecast.tsx

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import React from 'react';
2-
import { StyleSheet, Text, View } from 'react-native';
3-
import { GroupedWeather } from '../api/group';
4-
import { sampleForecastResponse } from '../api/forecast';
2+
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
53
import { formatDate, formatTemperature } from '../utils';
4+
import { RouteProp, useRoute } from '@react-navigation/native';
5+
import { useCityForecast } from '../data/queries';
6+
import { RootStackParamList } from '../../App';
67

7-
type Props = {
8-
city: GroupedWeather;
9-
};
10-
11-
export default function CityForecast({ city }: Props) {
8+
export default function CityForecast() {
9+
const route = useRoute<RouteProp<RootStackParamList, 'Details'>>();
10+
const { data, isPending, isError, error } = useCityForecast(
11+
route.params.name,
12+
);
13+
if (isPending) {
14+
return <ActivityIndicator size="large" style={styles.progress} />;
15+
}
16+
if (isError) {
17+
return <Text style={styles.error}>Error: {error.message}</Text>;
18+
}
1219
return (
1320
<View style={styles.wrapper}>
1421
<Text style={styles.header}>5-day forecast</Text>
15-
{sampleForecastResponse.list.map(x => (
22+
{data.list.map(x => (
1623
<Text key={x.dt}>
1724
{formatDate(x.dt)} {formatTemperature(x.main.temp_max)}{' '}
1825
{formatTemperature(x.main.temp_min)}
@@ -29,8 +36,15 @@ const styles = StyleSheet.create({
2936
header: {
3037
fontSize: 24,
3138
},
39+
progress: {
40+
flex: 1,
41+
alignSelf: 'center',
42+
},
3243
icon: {
3344
width: 128,
3445
height: 128,
3546
},
47+
error: {
48+
color: 'red',
49+
},
3650
});

src/screens/CityItem.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ export default function CityItem({ city }: Props) {
1414
const navigation = useNavigation();
1515
return (
1616
<PressableScale
17-
onPress={() =>
18-
navigation.navigate('Details', { id: city.id, name: city.name })
19-
}>
17+
onPress={() => {
18+
navigation.navigate('Details', {
19+
id: city.id,
20+
name: city.name,
21+
});
22+
}}>
2023
<View style={styles.card}>
2124
<View>
2225
<Text style={styles.name}>

src/screens/CityList.tsx

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
import { FlashList } from '@shopify/flash-list';
22
import React from 'react';
3+
import { ActivityIndicator, StyleSheet, Text } from 'react-native';
4+
import { useCityListWeather } from '../data/queries';
35
import CityItem from './CityItem';
4-
import { sampleGroupWeather } from '../api/group';
56

67
export default function CityList() {
8+
const { data, isPending, isError, error } = useCityListWeather();
9+
if (isPending) {
10+
return <ActivityIndicator size="large" style={styles.progress} />;
11+
}
12+
if (isError) {
13+
return <Text style={styles.error}>Error: {error.message}</Text>;
14+
}
715
return (
816
<FlashList
9-
data={sampleGroupWeather.list}
17+
data={data?.list ?? []}
1018
renderItem={({ item }) => <CityItem city={item} />}
1119
estimatedItemSize={200}
1220
/>
1321
);
1422
}
23+
24+
const styles = StyleSheet.create({
25+
progress: {
26+
flex: 1,
27+
alignSelf: 'center',
28+
},
29+
error: {
30+
color: 'red',
31+
},
32+
});

src/screens/CityWeather.tsx

+20-11
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
import React from 'react';
22
import { Image, StyleSheet, Text, View } from 'react-native';
3-
import { GroupedWeather } from '../api/group';
43
import { formatTemperature } from '../utils';
54
import { iconUrl } from '../api/api';
5+
import { useCityWeather } from '../data/queries';
6+
import { RouteProp, useRoute } from '@react-navigation/native';
7+
import { RootStackParamList } from '../../App';
68

7-
type Props = {
8-
city: GroupedWeather;
9-
};
10-
11-
export default function CityWeather({ city }: Props) {
9+
export default function CityWeather() {
10+
const route = useRoute<RouteProp<RootStackParamList, 'Details'>>();
11+
const { data, isPending, isError, error } = useCityWeather(route.params?.id);
12+
if (isPending) {
13+
return <Text>loading...</Text>;
14+
}
15+
if (isError) {
16+
return <Text>{error.message}</Text>;
17+
}
18+
if (!data) {
19+
return <Text>No data</Text>;
20+
}
1221
return (
1322
<View style={styles.wrapper}>
1423
<View>
1524
<View style={styles.tempWrapper}>
16-
<Text style={styles.temp}>{formatTemperature(city.main.temp)}</Text>
25+
<Text style={styles.temp}>{formatTemperature(data.main.temp)}</Text>
1726
<Image
18-
source={{ uri: iconUrl(city.weather[0].icon) }}
27+
source={{ uri: iconUrl(data.weather[0].icon) }}
1928
style={styles.icon}
2029
/>
2130
</View>
2231
</View>
2332
<View style={styles.details}>
24-
<Text>Feels like {Math.round(city.main.feels_like - 273.15)}°</Text>
25-
<Text>Humidity: {city.main.humidity}%</Text>
26-
<Text>Wind: {city.wind.speed} km/h</Text>
33+
<Text>Feels like {Math.round(data.main.feels_like - 273.15)}°</Text>
34+
<Text>Humidity: {data.main.humidity}%</Text>
35+
<Text>Wind: {data.wind.speed} km/h</Text>
2736
</View>
2837
</View>
2938
);

src/screens/Details.tsx

+9-18
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
import type { StaticScreenProps } from '@react-navigation/native';
22
import React from 'react';
3-
import { StyleSheet, Text, View } from 'react-native';
4-
import { sampleGroupWeather } from '../api/group';
3+
import { StyleSheet, View } from 'react-native';
54
import CityForecast from './CityForecast';
65
import CityWeather from './CityWeather';
76

8-
export type Props = StaticScreenProps<{
7+
export type DetailsRouteParams = {
98
id: number;
109
name: string;
11-
}>;
10+
};
1211

13-
export default function Details({ route }: Props) {
14-
const city = sampleGroupWeather.list.find(c => c.id === route.params.id);
15-
if (!city) {
16-
return <Text>City not found</Text>;
17-
}
12+
export type DetailsScreenProps = StaticScreenProps<DetailsRouteParams>;
13+
14+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
15+
export default function Details({ route }: DetailsScreenProps) {
1816
return (
1917
<View style={styles.screen}>
20-
<CityWeather city={city} />
21-
<CityForecast city={city} />
18+
<CityWeather />
19+
<CityForecast />
2220
</View>
2321
);
2422
}
@@ -28,11 +26,4 @@ const styles = StyleSheet.create({
2826
flex: 1,
2927
backgroundColor: 'white',
3028
},
31-
city: {
32-
fontSize: 32,
33-
},
34-
icon: {
35-
width: 128,
36-
height: 128,
37-
},
3829
});

0 commit comments

Comments
 (0)