├── .expo-shared
└── assets.json
├── .gitignore
├── .prettierrc
├── App.tsx
├── README.md
├── app.json
├── assets
├── icon.png
└── splash.png
├── babel.config.js
├── package.json
├── src
├── components
│ ├── ClipButton.tsx
│ ├── ListItem.tsx
│ └── Loading.tsx
├── navigation
│ └── AppNavigator.tsx
├── screens
│ ├── ArticleScreen.tsx
│ ├── ClipScreen.tsx
│ └── HomeScreen.tsx
├── store
│ ├── actions
│ │ └── user.ts
│ ├── index.ts
│ └── reducers
│ │ └── user.ts
└── types
│ ├── article.ts
│ ├── clip.ts
│ ├── navigation.ts
│ ├── state.ts
│ └── user.ts
├── tsconfig.json
└── yarn.lock
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 | web-report/
12 |
13 | # macOS
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": false,
3 | "jsxBracketSameLine": true,
4 | "singleQuote": false,
5 | "trailingComma": "all",
6 | "tabWidth": 2,
7 | "printWidth": 80,
8 | "semi": true
9 | }
10 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {PersistGate} from "redux-persist/integration/react";
3 | import {Provider} from "react-redux";
4 | import store, {persistor} from "./src/store";
5 | import {AppNavigator} from "./src/navigation/AppNavigator";
6 | export default function App() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # news-app-ts
2 | Udemy「React Native入門」の**TypeScript版**サンプルです。
3 | TypeScriptで書いてみたい方はご参考にして下さい。
4 |
5 | React Navigation v5に対応した改訂版です。
6 |
7 | https://www.udemy.com/course/react-native-first-step/?referralCode=05B579F15272BFA71DE1
8 |
9 | 
10 |
11 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "news-app-ts",
4 | "slug": "news-app-ts",
5 | "platforms": ["ios", "android", "web"],
6 | "version": "1.0.0",
7 | "orientation": "portrait",
8 | "icon": "./assets/icon.png",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "updates": {
15 | "fallbackToCacheTimeout": 0
16 | },
17 | "assetBundlePatterns": ["**/*"],
18 | "ios": {
19 | "supportsTablet": true
20 | },
21 | "extra": {
22 | "newsApiKey": "49a9ea5cfe5c4377b90ee6083946d195"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/takahi5/news-app-ts/f41562607bced89865ef4114fa565f38e8305eca/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/takahi5/news-app-ts/f41562607bced89865ef4114fa565f38e8305eca/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject"
9 | },
10 | "dependencies": {
11 | "@react-native-community/masked-view": "0.1.6",
12 | "@react-navigation/bottom-tabs": "^5.2.6",
13 | "@react-navigation/native": "^5.1.5",
14 | "@react-navigation/stack": "^5.2.10",
15 | "@types/react-redux": "^7.1.7",
16 | "axios": "^0.19.2",
17 | "expo": "~37.0.3",
18 | "expo-constants": "^9.0.0",
19 | "react": "~16.9.0",
20 | "react-dom": "~16.9.0",
21 | "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz",
22 | "react-native-gesture-handler": "~1.6.0",
23 | "react-native-reanimated": "~1.7.0",
24 | "react-native-safe-area-context": "0.7.3",
25 | "react-native-screens": "~2.2.0",
26 | "react-native-web": "~0.11.7",
27 | "react-native-webview": "^9.1.4",
28 | "react-redux": "^7.2.0",
29 | "redux": "^4.0.5",
30 | "redux-devtools-extension": "^2.13.8",
31 | "redux-persist": "^6.0.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.8.6",
35 | "@types/react": "~16.9.23",
36 | "@types/react-native": "~0.61.17",
37 | "babel-preset-expo": "~8.1.0",
38 | "typescript": "~3.8.3"
39 | },
40 | "private": true
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/ClipButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {StyleSheet, TouchableOpacity} from "react-native";
3 | import {FontAwesome} from "@expo/vector-icons";
4 |
5 | const styles = StyleSheet.create({
6 | container: {
7 | padding: 5,
8 | },
9 | });
10 |
11 | type Props = {
12 | onPress: () => void;
13 | enabled: boolean;
14 | };
15 |
16 | export const ClipButton = ({onPress, enabled}: Props) => {
17 | const name = enabled ? "bookmark" : "bookmark-o";
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/ListItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {StyleSheet, Text, View, Image, TouchableOpacity} from "react-native";
3 |
4 | const styles = StyleSheet.create({
5 | itemContainer: {
6 | height: 100,
7 | width: "100%",
8 | borderColor: "gray",
9 | borderWidth: 1,
10 | flexDirection: "row",
11 | },
12 | leftContainer: {
13 | width: 100,
14 | alignItems: "center",
15 | justifyContent: "center",
16 | },
17 | rightContainer: {
18 | flex: 1,
19 | padding: 16,
20 | justifyContent: "space-between",
21 | },
22 | text: {
23 | fontSize: 16,
24 | },
25 | subText: {
26 | fontSize: 12,
27 | color: "gray",
28 | },
29 | image: {
30 | width: 95,
31 | height: 95,
32 | },
33 | });
34 |
35 | type Props = {
36 | imageUrl: string;
37 | title: string;
38 | author: string;
39 | onPress: () => void;
40 | };
41 |
42 | export const ListItem = ({imageUrl, title, author, onPress}: Props) => {
43 | return (
44 |
45 |
46 | {!!imageUrl && }
47 |
48 |
49 |
50 | {title}
51 |
52 | {author}
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {StyleSheet, View, ActivityIndicator, Dimensions} from "react-native";
3 |
4 | const styles = StyleSheet.create({
5 | container: {
6 | width: "100%",
7 | height: "100%",
8 | position: "absolute",
9 | top: 0,
10 | left: 0,
11 | backgroundColor: "rgba(255, 255, 255, 0.5)",
12 | alignItems: "center",
13 | justifyContent: "center",
14 | },
15 | });
16 |
17 | export const Loading = () => {
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/navigation/AppNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {NavigationContainer} from "@react-navigation/native";
3 | import {createStackNavigator} from "@react-navigation/stack";
4 | import {
5 | createBottomTabNavigator,
6 | BottomTabNavigationOptions,
7 | } from "@react-navigation/bottom-tabs";
8 | import {FontAwesome} from "@expo/vector-icons";
9 | /* screens */
10 | import {HomeScreen} from "../screens/HomeScreen";
11 | import {ArticleScreen} from "../screens/ArticleScreen";
12 | import {ClipScreen} from "../screens/ClipScreen";
13 | /* types */
14 | import {RootStackParamList} from "../types/navigation";
15 |
16 | const Stack = createStackNavigator();
17 | const Tab = createBottomTabNavigator();
18 |
19 | const HomeStack = () => {
20 | return (
21 |
22 |
27 |
28 |
29 | );
30 | };
31 |
32 | const ClipStack = () => {
33 | return (
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const screenOption = ({route}): BottomTabNavigationOptions => ({
42 | tabBarIcon: ({color, size}) => {
43 | let iconName;
44 | switch (route.name) {
45 | case "Home":
46 | iconName = "home";
47 | break;
48 | case "Clip":
49 | iconName = "bookmark";
50 | break;
51 | }
52 | return ;
53 | },
54 | });
55 |
56 | export const AppNavigator = () => {
57 | return (
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/screens/ArticleScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {StyleSheet, SafeAreaView} from "react-native";
3 | import {WebView} from "react-native-webview";
4 | import {useDispatch, useSelector} from "react-redux";
5 | import {addClip, deleteClip} from "../store/actions/user";
6 | /* components */
7 | import {ClipButton} from "../components/ClipButton";
8 | import {Loading} from "../components/Loading";
9 | /* types */
10 | import {StackNavigationProp} from "@react-navigation/stack";
11 | import {RootStackParamList} from "../types/navigation";
12 | import {RouteProp} from "@react-navigation/native";
13 | import {State} from "../types/state";
14 | import {User} from "../types/user";
15 |
16 | type Props = {
17 | navigation: StackNavigationProp;
18 | route: RouteProp;
19 | };
20 |
21 | export const ArticleScreen: React.FC = ({navigation, route}: Props) => {
22 | const {article} = route.params;
23 |
24 | const user = useSelector((state: State) => state.user) as User;
25 |
26 | const dispatch = useDispatch();
27 |
28 | const isClipped = () => {
29 | return user.clips.some((clip) => clip.url === article.url);
30 | };
31 |
32 | const toggleClip = () => {
33 | if (isClipped()) {
34 | dispatch(deleteClip({clip: article}));
35 | } else {
36 | dispatch(addClip({clip: article}));
37 | }
38 | };
39 |
40 | return (
41 |
42 |
43 | {
47 | return ;
48 | }}
49 | />
50 |
51 | );
52 | };
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | flex: 1,
57 | backgroundColor: "#fff",
58 | justifyContent: "flex-start",
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/screens/ClipScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import {StyleSheet, SafeAreaView, FlatList} from "react-native";
3 | import {useSelector} from "react-redux";
4 | /* components */
5 | import {ListItem} from "../components/ListItem";
6 | /* types */
7 | import {StackNavigationProp} from "@react-navigation/stack";
8 | import {RootStackParamList} from "../types/navigation";
9 | import {RouteProp} from "@react-navigation/native";
10 | import {State} from "../types/state";
11 | import {User} from "../types/user";
12 |
13 | type Props = {
14 | navigation: StackNavigationProp;
15 | route: RouteProp;
16 | };
17 |
18 | export const ClipScreen: React.FC = ({navigation, route}: Props) => {
19 | const user = useSelector((state: State) => state.user) as User;
20 |
21 | return (
22 |
23 | (
26 | navigation.navigate("Article", {article: item})}
31 | />
32 | )}
33 | keyExtractor={(item, index) => index.toString()}
34 | />
35 |
36 | );
37 | };
38 |
39 | const styles = StyleSheet.create({
40 | container: {
41 | flex: 1,
42 | backgroundColor: "#fff",
43 | justifyContent: "flex-start",
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/src/screens/HomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import {StyleSheet, SafeAreaView, FlatList} from "react-native";
3 | import Constants from "expo-constants";
4 | import axios from "axios";
5 | /* components */
6 | import {ListItem} from "../components/ListItem";
7 | import {Loading} from "../components/Loading";
8 | /* types */
9 | import {StackNavigationProp} from "@react-navigation/stack";
10 | import {RouteProp} from "@react-navigation/native";
11 | import {RootStackParamList} from "../types/navigation";
12 | import {Article} from "../types/article";
13 |
14 | const URL = `https://newsapi.org/v2/top-headlines?country=jp&category=business&apiKey=${Constants.manifest.extra.newsApiKey}`;
15 |
16 | const styles = StyleSheet.create({
17 | container: {
18 | flex: 1,
19 | backgroundColor: "#fff",
20 | justifyContent: "flex-start",
21 | },
22 | });
23 |
24 | type Props = {
25 | navigation: StackNavigationProp;
26 | route: RouteProp;
27 | };
28 |
29 | export const HomeScreen: React.FC = ({navigation, route}: Props) => {
30 | const [articles, setArticles] = useState([]);
31 | const [loading, setLoading] = useState(false);
32 |
33 | useEffect(() => {
34 | fetchArticles();
35 | }, []);
36 |
37 | const fetchArticles = async () => {
38 | setLoading(true);
39 | try {
40 | const response = await axios.get(URL);
41 | setArticles(response.data.articles);
42 | } catch (error) {
43 | console.error(error);
44 | }
45 | setLoading(false);
46 | };
47 |
48 | return (
49 |
50 | {loading && }
51 | (
54 | navigation.navigate("Article", {article: item})}
59 | />
60 | )}
61 | keyExtractor={(item, index) => index.toString()}
62 | />
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/store/actions/user.ts:
--------------------------------------------------------------------------------
1 | import {Article} from "../../types/article";
2 |
3 | export type AddClipAction = {
4 | type: "ADD_CLIP";
5 | clip: Article;
6 | };
7 |
8 | export const addClip = ({clip}: {clip: Article}) => {
9 | return {
10 | type: "ADD_CLIP",
11 | clip,
12 | };
13 | };
14 |
15 | export type DeleteClipAction = {
16 | type: "DELETE_CLIP";
17 | clip: Article;
18 | };
19 |
20 | export const deleteClip = ({clip}: {clip: Article}) => {
21 | return {
22 | type: "DELETE_CLIP",
23 | clip,
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import {createStore, combineReducers} from 'redux';
2 | import {composeWithDevTools} from 'redux-devtools-extension';
3 | import {AsyncStorage} from 'react-native';
4 | import {persistReducer, persistStore} from 'redux-persist';
5 | import userReducer from './reducers/user';
6 |
7 | const persistConfig = {
8 | key: 'root',
9 | storage: AsyncStorage,
10 | whitelist: ['user'],
11 | };
12 |
13 | const rootReducer = combineReducers({
14 | user: userReducer,
15 | });
16 |
17 | const persistedReducer = persistReducer(persistConfig, rootReducer);
18 |
19 | const store = createStore(persistedReducer, composeWithDevTools());
20 |
21 | export const persistor = persistStore(store);
22 | export default store;
23 |
--------------------------------------------------------------------------------
/src/store/reducers/user.ts:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | clips: [],
3 | };
4 |
5 | const reducer = (state = initialState, action) => {
6 | switch (action.type) {
7 | case 'ADD_CLIP':
8 | return {
9 | ...state,
10 | clips: [...state.clips, action.clip],
11 | };
12 | case 'DELETE_CLIP':
13 | return {
14 | ...state,
15 | clips: state.clips.filter(clip => clip.url !== action.clip.url),
16 | };
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | export default reducer;
23 |
--------------------------------------------------------------------------------
/src/types/article.ts:
--------------------------------------------------------------------------------
1 | export type Article = {
2 | title: string;
3 | author: string;
4 | url: string;
5 | urlToImage: string;
6 | };
7 |
--------------------------------------------------------------------------------
/src/types/clip.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/takahi5/news-app-ts/f41562607bced89865ef4114fa565f38e8305eca/src/types/clip.ts
--------------------------------------------------------------------------------
/src/types/navigation.ts:
--------------------------------------------------------------------------------
1 | import {Article} from "./article";
2 |
3 | export type RootStackParamList = {
4 | Home: undefined;
5 | Clip: undefined;
6 | Article: {article: Article};
7 | };
8 |
--------------------------------------------------------------------------------
/src/types/state.ts:
--------------------------------------------------------------------------------
1 | import {AddClipAction, DeleteClipAction} from "../store/actions/user";
2 | import {User} from "./user";
3 |
4 | export interface State {
5 | user: User;
6 | }
7 |
8 | export type Actions =
9 | // user
10 | AddClipAction | DeleteClipAction;
11 |
--------------------------------------------------------------------------------
/src/types/user.ts:
--------------------------------------------------------------------------------
1 | import {Article} from "./article";
2 |
3 | export type User = {
4 | clips: Article[];
5 | };
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "jsx": "react-native",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "noEmit": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true,
10 | "strict": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------