Typescript

How to upload and display images without backend

Using localStorage to save the image and display it on each visit

Table of Contents
  1. Attack Plan
  2. Implementation
    1. Creating the Component
    2. Creating the helper functions for state
    3. Persisting data
  3. That's all

I have been working on updating the web client for opsdroid. This new version comes with a new UI and some nice features, such as a theme selector, allowing you to set your username and profile picture.

The problem with the web client is that I didn't want to run a backend or have opsdroid save the user's avatar. Ideally, a user would be able to upload their picture from the client settings, and when a user sends a message to opsdroid, the image should be the one that was uploaded.

Attack Plan

The idea is pretty straightforward to implement. This was what I started with:

  • An input that allows users to upload their image
  • Convert the image into base64
  • Save the base64 in state
  • Update the message component to use that image
  • Make sure the image persists on new sessions/reloads

Implementation

The web client is written in React/Typescript, so the example I will give here will use the same stack. This is worth noting because when handling files, you need to dance with typing to make typescript happy.

We are also using pullstate as our main state manager, but in this article, I'll use React's useState hook to make it easier to understand.

Creating the Component

Since we will be using useState we need to create a type for the state.

typescript
1type userSettingsState = {
2 username: string
3 avatar?: string
4}

We will set a default for username, but not for avatar since this will be the base64 string for the uploaded image. This means that avatar will be either undefined | string.

Let's now build the UserSettings component and use our useSettingsState.

typescript
1type userSettingsState = {
2 username: string
3 avatar?: string
4}
5
6export const UserSettings = (): React.ReactElement => {
7 const [userSettings, setUserSettings] = React.useState<userSettingsState>({
8 username: "bob-the-builder",
9 })
10
11 const updateUsername = (e: React.FormEvent<HTMLInputElement>) => {}
12
13 const updateAvatar = (e: React.FormEvent<HTMLInputElement>) => {}
14
15 return (
16 <div>
17 <h2 className="flex align-items-center">User Settings</h2>
18 <div className="flex flex-column padding-left">
19 <div className="flex align-items-center">
20 <p className="padding-right settings-text">Username</p>
21 <input
22 type="text"
23 id="username"
24 placeholder={username}
25 defaultValue={username}
26 onChange={updateUsername}
27 />
28 </div>
29 <div className="flex align-items-center">
30 <p className="padding-right settings-text">Avatar</p>
31 <input type="file" onChange={updateAvatar} />
32 </div>
33 </div>
34 </div>
35 )
36}

Creating the helper functions for state

We now have two functions to fill, updateUsername and updateAvatar. All we need to do for the username is get the value from the input.

typescript
1const updateUsername = (e: React.FormEvent<HTMLInputElement>) => {
2 setUserSettings({
3 username: e.currentTarget.value,
4 // set avatar to whatever avatar is set
5 avatar: avatar,
6 })
7}

This one was pretty straightforward. The fun begins when we try to update the user's profile picture.

Since we are using an input to upload the image, we can get the image details with e.target.files[0] , but this doesn't work since typescript will throw an error at you saying that you are trying to access an attribute (files) that doesn't exist in the object.

typescript
1Property 'files' does not exist on type 'EventTarget'.

The trick here is setting the type of e.target to HTMLInputElement, by doing this, we can then access the files property.

typescript
1const updateAvatar = (e: React.FormEvent<HTMLInputElement>) => {
2 const target = e.target as HTMLInputElement
3 const avatar = target.files[0]
4}

This will still give you an error since files is possibly null. To fix that, we can check if target.files exists and if it has any element inside.

typescript
1const updateAvatar = (e: React.FormEvent<HTMLInputElement>) => {
2 const target = e.target as HTMLInputElement;
3 if (target.files && target.files.length) {
4 const avatar = target.files[0];
5}

Now that we have the image file details, we can use FileReader to get the representation of the image as a base64 encoded string. Once we have the base64 encoded string, we can update our userSettings state with this. Note that FileReader methods are async.

typescript
1const updateAvatar = (e: React.FormEvent<HTMLInputElement>) => {
2 const target = e.target as HTMLInputElement;
3 if (target.files && target.files.length) {
4 const avatar = target.files[0];
5 const reader = new FileReader();
6 reader.readAsDataURL(avatar);
7 reader.onload = () => {
8 const result = reader.result as string;
9 if (result) {
10 setUserSettings({
11 username: username,
12 avatar: result
13 })
14 }
15 };
16 }

Persisting data

With opsdroid web client, we are persisting a lot of data through cookies, although we won't be able to store the base64 encoded string of the user profile picture because this string is too large. The option here is using local storage to persist the profile picture and fetch it when we reload/restart the client.

typescript
1const updateAvatar = (e: React.FormEvent<HTMLInputElement>) => {
2 const target = e.target as HTMLInputElement;
3 if (target.files && target.files.length) {
4 const avatar = target.files[0];
5 const reader = new FileReader();
6 reader.readAsDataURL(avatar);
7 reader.onload = () => {
8 const result = reader.result as string;
9 if (result) {
10 setUserSettings({
11 username: username,
12 avatar: result
13 })
14 localStorage.setItem("profile-picture", result)
15 }
16 };
17 }

We can then update our initial state to get the data from local storage if it exists.

typescript
1const [userSettings, setUserSettings] = React.useState<userSettingsState>({
2 username: localStorage.getItem("username") || "bob-the-builder",
3 avatar: localStorage.getItem("profile-picture") || undefined,
4})

That's all

Now users can upload images, and we can save them to local storage and show them when the users come back. It's much better to have a backend so you can save and fetch the image. But, for this use case, local storage seems good enough.

Here's the full code:

typescript
1type userSettingsState = {
2 username: string;
3 avatar?: string;
4}
5
6export const UserSettings = (): React.ReactElement => {
7
8 const [ userSettings, setUserSettings] = React.useState<userSettingsState>({
9 username: localStorage.getItem("username") || "bob-the-builder",
10 avatar: localStorage.getItem("profile-picture") || undefined
11 });
12
13 const updateUsername = (e: React.FormEvent<HTMLInputElement>) => {
14 setUserSettings({
15 username: e.currentTarget.value,
16 // set avatar to whatever avatar is set
17 avatar: avatar
18 })
19 };
20
21 const updateAvatar = (e: React.FormEvent<HTMLInputElement>) => {
22 const target = e.target as HTMLInputElement;
23 if (target.files && target.files.length) {
24 const avatar = target.files[0];
25 const reader = new FileReader();
26 reader.readAsDataURL(avatar);
27 reader.onload = () => {
28 const result = reader.result as string;
29 if (result) {
30 setUserSettings({
31 username: username,
32 avatar: result
33 })
34 localStorage.setItem("profile-picture", result)
35 }
36 };
37 }
38
39 return (
40 <div>
41 <h2 className="flex align-items-center">
42 User Settings
43 </h2>
44 <div className="flex flex-column padding-left">
45 <div className="flex align-items-center">
46 <p className="padding-right settings-text">Username</p>
47 <input
48 type="text"
49 id="username"
50 placeholder={username}
51 defaultValue={username}
52 onChange={updateUsername}
53 />
54 </div>
55 <div className="flex align-items-center">
56 <p className="padding-right settings-text">Avatar</p>
57 <input
58 type="file"
59 onChange={updateAvatar}
60 />
61 </div>
62 </div>
63 </div>
64 );
65};

Webmentions

0 Like 0 Comment

You might also like these

While working on adding tests to Pyscript I came across a use case where I had to check if an example image is always generated the same.

Read More
Python

How to compare two images using NumPy

How to compare two images using NumPy

How to return an attribute from a many-to-many object relationship from a Django Ninja API endpoint.

Read More
Python

Django Ninja Schemas and Many To Many

Django Ninja Schemas and Many To Many

Cheatsheet for Bash scripting

Read More
Cheatsheet

Bash Scripting

Bash Scripting

When using monorepos it can be a bit confusing how to deploy to gitlab pages from a specific folder, this article will help you with it.

Read More
CI

How to setup Gitlab pages from a folder

How to setup Gitlab pages from a folder