Skip to content

Commit a386a0c

Browse files
committed
docs(react): update loading photos page
1 parent 290abfb commit a386a0c

File tree

1 file changed

+103
-106
lines changed

1 file changed

+103
-106
lines changed

docs/react/your-first-app/4-loading-photos.md

Lines changed: 103 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,196 +4,187 @@ sidebar_label: Loading Photos
44
---
55

66
<head>
7-
<title>Loading Photos from the Filesystem Using A Key-Value Store</title>
7+
<title>Loading Photos from the Filesystem with React | Ionic Capacitor Camera</title>
88
<meta
99
name="description"
1010
content="We’ve implemented photo taking and saving to the filesystem, now learn how Ionic leverages Capacitor Preferences API for loading our photos in a key-value store."
1111
/>
1212
</head>
1313

14+
# Loading Photos from the Filesystem
15+
1416
We’ve implemented photo taking and saving to the filesystem. There’s one last piece of functionality missing: the photos are stored in the filesystem, but we need a way to save pointers to each file so that they can be displayed again in the photo gallery.
1517

16-
Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](https://capacitorjs.com/docs/apis/preferences) to store our array of Photos in a key-value store.
18+
Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](../../native/preferences.md) to store our array of Photos in a key-value store.
1719

1820
## Preferences API
1921

20-
Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`:
22+
Open `usePhotoGallery.ts` and begin by defining a constant variable that will act as the key for the store.
2123

22-
```tsx
23-
// CHANGE: Create a constant variable that will act as a key to store
24-
const PHOTO_STORAGE = 'photos';
24+
```ts
25+
export function usePhotoGallery() {
26+
const [photos, setPhotos] = useState<UserPhoto[]>([]);
27+
// CHANGE: Add a key for photo storage.
28+
const PHOTO_STORAGE = 'photos';
2529

26-
// Same old code from before
27-
export function usePhotoGallery() {}
30+
// Same old code from before.
31+
}
2832
```
2933

30-
Then, use the `Storage` class to get access to the get and set methods for reading and writing to device storage:
34+
Next, at the end of the `addNewToGallery()` method, add a call to the `Preferences.set()` method to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved.
35+
36+
```ts
37+
const addNewToGallery = async () => {
38+
// Same old code from before.
39+
40+
// CHANGE: Add method to cache all photo data for future retrieval.
41+
Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
42+
};
43+
```
3144

32-
At the end of the `takePhoto` function, add a call to `Preferences.set()` to save the Photos array. By adding it here, the Photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved.
45+
With the photo array data saved, create a new method in the `usePhotoGallery()` called `loadSaved()` that can retrieve the photo data. We use the same key to retrieve the `photos` array in JSON format, then parse it into an array.
3346

34-
```tsx
35-
// Same old code from before.
47+
```ts
3648
export function usePhotoGallery() {
37-
// Same old code from before.
49+
const [photos, setPhotos] = useState<UserPhoto[]>([]);
3850

39-
const takePhoto = async () => {
40-
const photo = await Camera.getPhoto({
41-
resultType: CameraResultType.Uri,
42-
source: CameraSource.Camera,
43-
quality: 100,
44-
});
51+
const PHOTO_STORAGE = 'photos';
4552

46-
const newPhotos = [
47-
{
48-
filepath: fileName,
49-
webviewPath: photo.webPath,
50-
},
51-
...photos,
52-
];
53-
setPhotos(newPhotos);
54-
// CHANGE: Add a call to save the photos array
55-
Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
56-
};
57-
// Same old code from before
58-
return {
59-
photos,
60-
takePhoto,
61-
};
62-
}
53+
// CHANGE: Add useEffect hook.
54+
useEffect(() => {
55+
// CHANGE: Add `loadSaved()` method.
56+
const loadSaved = async () => {
57+
const { value } = await Preferences.get({ key: PHOTO_STORAGE });
58+
const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
59+
};
6360

64-
// Same old code from before.
61+
loadSaved();
62+
}, []);
63+
64+
// Same old code from before.
65+
}
6566
```
6667

67-
With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down:
68+
The second parameter, the empty dependency array (`[]`), is what tells React to only run the function once. Normally, [useEffect hooks](https://react.dev/reference/react/useEffect) run after every render, but passing an empty array prevents it from running again because none of the dependencies, the values the hook relies on, will ever change.
6869

69-
```tsx
70-
// Same old code from before.
70+
On mobile (coming up next!), we can directly set the source of an image tag - `<img src="x" />` - to each photo file on the `Filesystem`, displaying them automatically. On the web, however, we must read each image from the `Filesystem` into base64 format, using a new `base64` property on the `Photo` object. This is because the `Filesystem` API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Add the following code to complete the `loadSaved()` method.
71+
72+
```ts
7173
export function usePhotoGallery() {
72-
// Same old code from before.
74+
const [photos, setPhotos] = useState<UserPhoto[]>([]);
75+
76+
const PHOTO_STORAGE = 'photos';
7377

74-
// CHANGE: Add useEffect hook
7578
useEffect(() => {
79+
// CHANGE: Update `loadSaved()` method.
7680
const loadSaved = async () => {
7781
const { value } = await Preferences.get({ key: PHOTO_STORAGE });
7882
const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
7983

80-
for (let photo of photosInPreferences) {
84+
// CHANGE: Display the photo by reading into base64 format.
85+
for (const photo of photosInPreferences) {
8186
const file = await Filesystem.readFile({
8287
path: photo.filepath,
8388
directory: Directory.Data,
8489
});
85-
// Web platform only: Load the photo as base64 data
8690
photo.webviewPath = `data:image/jpeg;base64,${file.data}`;
8791
}
92+
8893
setPhotos(photosInPreferences);
8994
};
95+
9096
loadSaved();
9197
}, []);
9298

93-
const takePhotos = async () => {
94-
// Same old code from before.
95-
};
99+
// Same old code from before.
96100
}
97-
98-
// Same old code from before.
99101
```
100102

101-
This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`.
102-
103-
The useEffect hook, by default, gets called each time a component renders, unless, we pass in a dependency array. In that case, it will only run when a dependency gets updated. In our case we only want it to be called once. By passing in an empty array, which will not be changed, we can prevent the hook from being called multiple times.
104-
105-
The first parameter to `useEffect` is the function that will be called by the effect. We pass in an anonymous arrow function, and inside of it we define another asynchronous method and then immediately call this. We have to call the async function from within the hook as the hook callback can't be asynchronous itself.
106-
107-
On mobile (coming up next!), we can directly set the source of an image tag - `<img src=”x” />` - to each photo file on the Filesystem, displaying them automatically. On the web, however, we must read each image from the Filesystem into base64 format, because the Filesystem API stores them in base64 within [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood.
108-
109103
`usePhotoGallery.ts` should now look like this:
110104

111-
```tsx
105+
```ts
112106
import { useState, useEffect } from 'react';
113-
import { isPlatform } from '@ionic/react';
114107
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
115108
import { Filesystem, Directory } from '@capacitor/filesystem';
116109
import { Preferences } from '@capacitor/preferences';
117-
import { Capacitor } from '@capacitor/core';
118-
119-
const PHOTO_STORAGE = 'photos';
120110

121111
export function usePhotoGallery() {
122112
const [photos, setPhotos] = useState<UserPhoto[]>([]);
123-
const fileName = Date.now() + '.jpeg';
124-
const savePicture = async (photo: Photo, fileName: string): Promise<UserPhoto> => {
125-
const base64Data = await base64FromPath(photo.webPath!);
126-
const savedFile = await Filesystem.writeFile({
127-
path: fileName,
128-
data: base64Data,
129-
directory: Directory.Data,
130-
});
131113

132-
// Use webPath to display the new image instead of base64 since it's
133-
// already loaded into memory
134-
return {
135-
filepath: fileName,
136-
webviewPath: photo.webPath,
137-
};
138-
};
114+
const PHOTO_STORAGE = 'photos';
139115

140116
useEffect(() => {
141117
const loadSaved = async () => {
142118
const { value } = await Preferences.get({ key: PHOTO_STORAGE });
143119
const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
144120

145-
for (let photo of photosInPreferences) {
121+
for (const photo of photosInPreferences) {
146122
const file = await Filesystem.readFile({
147123
path: photo.filepath,
148124
directory: Directory.Data,
149125
});
150-
// Web platform only: Load the photo as base64 data
151126
photo.webviewPath = `data:image/jpeg;base64,${file.data}`;
152127
}
128+
153129
setPhotos(photosInPreferences);
154130
};
131+
155132
loadSaved();
156133
}, []);
157134

158-
const takePhoto = async () => {
159-
const photo = await Camera.getPhoto({
135+
const addNewToGallery = async () => {
136+
// Take a photo
137+
const capturedPhoto = await Camera.getPhoto({
160138
resultType: CameraResultType.Uri,
161139
source: CameraSource.Camera,
162140
quality: 100,
163141
});
164142

165-
const newPhotos = [
166-
{
167-
filepath: fileName,
168-
webviewPath: photo.webPath,
169-
},
170-
...photos,
171-
];
143+
const fileName = Date.now() + '.jpeg';
144+
// Save the picture and add it to photo collection
145+
const savedImageFile = await savePicture(capturedPhoto, fileName);
146+
147+
const newPhotos = [savedImageFile, ...photos];
172148
setPhotos(newPhotos);
149+
173150
Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
174151
};
175152

176-
return {
177-
photos,
178-
takePhoto,
153+
const savePicture = async (photo: Photo, fileName: string): Promise<UserPhoto> => {
154+
// Fetch the photo, read as a blob, then convert to base64 format
155+
const response = await fetch(photo.webPath!);
156+
const blob = await response.blob();
157+
const base64Data = (await convertBlobToBase64(blob)) as string;
158+
159+
const savedFile = await Filesystem.writeFile({
160+
path: fileName,
161+
data: base64Data,
162+
directory: Directory.Data,
163+
});
164+
165+
// Use webPath to display the new image instead of base64 since it's
166+
// already loaded into memory
167+
return {
168+
filepath: fileName,
169+
webviewPath: photo.webPath,
170+
};
179171
};
180-
}
181172

182-
export async function base64FromPath(path: string): Promise<string> {
183-
const response = await fetch(path);
184-
const blob = await response.blob();
185-
return new Promise((resolve, reject) => {
186-
const reader = new FileReader();
187-
reader.onerror = reject;
188-
reader.onload = () => {
189-
if (typeof reader.result === 'string') {
173+
const convertBlobToBase64 = (blob: Blob) => {
174+
return new Promise((resolve, reject) => {
175+
const reader = new FileReader();
176+
reader.onerror = reject;
177+
reader.onload = () => {
190178
resolve(reader.result);
191-
} else {
192-
reject('method did not return a string');
193-
}
194-
};
195-
reader.readAsDataURL(blob);
196-
});
179+
};
180+
reader.readAsDataURL(blob);
181+
});
182+
};
183+
184+
return {
185+
addNewToGallery,
186+
photos,
187+
};
197188
}
198189

199190
export interface UserPhoto {
@@ -202,4 +193,10 @@ export interface UserPhoto {
202193
}
203194
```
204195

196+
:::note
197+
If you're seeing broken image links or missing photos after following these steps, you may need to open your browser's dev tools and clear both [localStorage](https://developer.chrome.com/docs/devtools/storage/localstorage) and [IndexedDB](https://developer.chrome.com/docs/devtools/storage/indexeddb).
198+
199+
In localStorage, look for domain `http://localhost:8100` and key `CapacitorStorage.photos`. In IndexedDB, find a store called "FileStorage". Your photos will have a key like `/DATA/123456789012.jpeg`.
200+
:::
201+
205202
That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android!

0 commit comments

Comments
 (0)