Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
Generated by create-expo-app 2.1.1.
  • Loading branch information
Kudo committed Nov 23, 2023
0 parents commit 7fd892f
Show file tree
Hide file tree
Showing 17 changed files with 370 additions and 0 deletions.
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/

# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo
23 changes: 23 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

import Main from './src/Main';

export default function App() {
return (
<View style={styles.container}>
<Main />
<StatusBar style="auto" />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 64,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
30 changes: 30 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"expo": {
"name": "rnsuspense",
"slug": "rnsuspense",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file added assets/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};
Binary file added bun.lockb
Binary file not shown.
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "rnsuspense",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
"react": "18.2.0",
"react-error-boundary": "^4.0.11",
"react-native": "0.72.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.2.14",
"typescript": "^5.1.3"
},
"private": true
}
33 changes: 33 additions & 0 deletions src/Albums.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { StyleSheet, Text, View } from 'react-native';

import { fetchData } from './data';
import { use } from './useHook';

// Note: this component is written using an experimental API
// that's not yet available in stable versions of React.

// For a realistic example you can follow today, try a framework
// that's integrated with Suspense, like Relay or Next.js.

export default function Albums({ artistId }: { artistId: string }) {
const albums = use(fetchData(`/${artistId}/albums`)) ?? [];
return (
<View style={styles.listContainer}>
{albums.map((album) => (
<View style={styles.listItem} key={album.id}>
<Text style={styles.listItemText}>
{album.title} ({album.year})
</Text>
</View>
))}
</View>
);
}

const styles = StyleSheet.create({
listContainer: {
flex: 1,
},
listItem: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#ccc' },
listItemText: { fontSize: 16 },
});
30 changes: 30 additions & 0 deletions src/ArtistPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Suspense } from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import Albums from './Albums';

import type { Artist } from './types';

export default function ArtistPage({ artist }: { artist: Artist }) {
return (
<ScrollView>
<Text style={styles.artistName}>{artist.name}</Text>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</ScrollView>
);
}

function Loading() {
return <Text style={styles.loadingText}>🌀 Loading...</Text>;
}

const styles = StyleSheet.create({
artistName: {
fontSize: 24,
textAlign: 'center',
},
loadingText: {
fontSize: 20,
},
});
35 changes: 35 additions & 0 deletions src/Main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Button, Text } from 'react-native';

import ArtistPage from './ArtistPage';
import type { Artist } from './types';

export default function Main() {
const [artist, setArtist] = useState<Artist>(null);

if (artist != null) {
return (
<ErrorBoundary fallbackRender={errorFallbackRenderer}>
<ArtistPage artist={artist} />
</ErrorBoundary>
);
} else {
return (
<>
<Button
title="Open The Beatles artist page"
onPress={() => setArtist({ id: 'the-beatles', name: 'The Beatles' })}
/>
<Button
title="Open non-existed artist page"
onPress={() => setArtist({ id: 'unknown', name: 'Unknown' })}
/>
</>
);
}
}

function errorFallbackRenderer({ error }: { error: Error }) {
return <Text>Error Boundary: {error.message}</Text>;
}
97 changes: 97 additions & 0 deletions src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

import type { Album } from './types';

let cache = new Map();

export function fetchData(url: string): Promise<Album[]> {
if (!cache.has(url)) {
cache.set(url, getData(url));
}
return cache.get(url);
}

async function getData(url: string): Promise<Album[]> {
if (url === '/the-beatles/albums') {
return await getAlbums();
} else {
throw Error('Not implemented');
}
}

async function getAlbums(): Promise<Album[]> {
// Add a fake delay to make waiting noticeable.
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

return [
{
id: 13,
title: 'Let It Be',
year: 1970,
},
{
id: 12,
title: 'Abbey Road',
year: 1969,
},
{
id: 11,
title: 'Yellow Submarine',
year: 1969,
},
{
id: 10,
title: 'The Beatles',
year: 1968,
},
{
id: 9,
title: 'Magical Mystery Tour',
year: 1967,
},
{
id: 8,
title: "Sgt. Pepper's Lonely Hearts Club Band",
year: 1967,
},
{
id: 7,
title: 'Revolver',
year: 1966,
},
{
id: 6,
title: 'Rubber Soul',
year: 1965,
},
{
id: 5,
title: 'Help!',
year: 1965,
},
{
id: 4,
title: 'Beatles For Sale',
year: 1964,
},
{
id: 3,
title: "A Hard Day's Night",
year: 1964,
},
{
id: 2,
title: 'With The Beatles',
year: 1963,
},
{
id: 1,
title: 'Please Please Me',
year: 1963,
},
];
}
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Album {
id: number;
title: string;
year: number;
}

export interface Artist {
id: string;
name: string;
}
43 changes: 43 additions & 0 deletions src/useHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Referenced from https://github.com/vercel/swr/blob/1d8110900d1aee3747199bfb377b149b7ff6848e/_internal/src/types.ts#L27-L31
type ReactUsePromise<T, E extends Error = Error> = Promise<T> & {
status?: 'pending' | 'fulfilled' | 'rejected';
value?: T;
reason?: E;
};

// Referenced from https://github.com/reactjs/react.dev/blob/6570e6cd79a16ac3b1a2902632eddab7e6abb9ad/src/content/reference/react/Suspense.md
/**
* A custom hook like `React.use` hook using private Suspense API.
*/
export function use<T>(promise: Promise<T> | ReactUsePromise<T>) {
if (isReactUsePromise(promise)) {
if (promise.status === 'fulfilled') {
return promise.value;
} else if (promise.status === 'rejected') {
throw promise.reason;
} else if (promise.status === 'pending') {
throw promise;
}
throw new Error('Promise is in an invalid state');
}

const suspensePromise = promise as ReactUsePromise<T>;
suspensePromise.status = 'pending';
suspensePromise.then(
(result: T) => {
suspensePromise.status = 'fulfilled';
suspensePromise.value = result;
},
(reason) => {
suspensePromise.status = 'rejected';
suspensePromise.reason = reason;
}
);
throw suspensePromise;
}

function isReactUsePromise<T>(
promise: Promise<T> | ReactUsePromise<T>
): promise is ReactUsePromise<T> {
return typeof promise === 'object' && promise !== null && 'status' in promise;
}
4 changes: 4 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"compilerOptions": {},
"extends": "expo/tsconfig.base"
}

0 comments on commit 7fd892f

Please sign in to comment.