Hexabase TypeScript SDKを使ったデモ（チャットアプリ）

[Hexabase（ヘキサベース）](https://www.hexabase.com/)は企業においても安心して利用できるBaaS（Backend as a Service）を提供しています。多くのBaaSがそうであるように、主にフロントエンド開発者に利用してもらいたいと考えています。そこで現在、TypeScript SDKの開発が進められています。

この記事ではv2系になって新しくなったHexabase TypeScript SDKのインストールと、データストア（クラウドデータベース）の通知機能を使ったチャットアプリの実装を紹介します。

## デモ

コードは以下で公開しています。

[hexabase/sample\-nextjs\-chat](https://github.com/hexabase/sample-nextjs-chat)

## インストール

インストールはnpmやyarnを使って行います。

```
# npmの場合
npm install @hexabase/hexabase-js

# yarnの場合
yarn add @hexabase/hexabase-js
```

## インポート

インポートすると、 `HexabaseClient` というオブジェクトが取得できます。

```jsx
import { HexabaseClient, Item } from "@hexabase/hexabase-js";
```

## 初期化

HexabaseClientを初期化します。

```tsx
// 本番閑居向け
const client = new HexabaseClient();

// 開発環境向け
const client = new HexabaseClient('dev');
```

なお、すでに認証済みだった場合は `setToken` メソッドを使ってトークンをセットすることもできます。

```tsx
client.setToken(token);
```

### 認証

Hexabaseでは業務利用を想定しているため、利用する際に認証情報が必須になります。最初はメールアドレスとパスワードで認証し、その後はトークンを使ってGraphQLにアクセスします。 `client` を使って処理します。

初回の認証は次のようになります。emailとパスワード、またはトークンが必須です。

```tsx
await client.login({email, password, token});
```

後はこの `client` に対して処理を行います。

## 認証の判定

ログイン済みかどうかは `client.currentUser` が `null` かどうかで判定できます。今回はそれをステートに入れることで、ログイン判定と画面の出し分けを行っています。

```tsx
const [loggedIn, setLoggedIn] = useState<boolean>(false);

useEffect(() => {
	const token = localStorage.getItem('token');
	if (token) init(token);
}, []);

const init = async (token: string) => {
	await client.setToken(token);
	await client.setWorkspace(process.env.NEXT_PUBLIC_WORKSPACE_ID!);
	setLoggedIn(true);
}

return (
	<>
		{ loggedIn ?
			<Chat client={client} />
		: <Login success={success} />}
	</>
)
```

## 認証画面

ログイン画面は `components/login.tsx` にて実装しています。メールアドレスとパスワードで認証を行っています。認証が通れば、 `success` に対してトークン文字列を送っています。

```tsx
const [form, setForm] = useState<FormType>({
	email: '',
	password: '',
	errorMessage: '',
});

const login = async (e: React.SyntheticEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement>) => {
	e.preventDefault();
	const bol = await client.login({email: form.email, password: form.password});
	if (bol) {
		params.success(client.tokenHxb);
	} else {
		setForm({...form, errorMessage: 'ログインに失敗しました。'});
	}
};
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/32b23409-6866-75a0-2ef9-0c5f68b0ebb6.png)


## チャットルーム一覧画面

認証が通ったら `<Chat />` を表示します。これは `components/chat.tsx` にて実装しています。Hexabase SDKのQueryを使って、チャットルーム一覧を取得しています。

```tsx
const [rooms, setRooms] = useState<Item[]>([]);

useEffect(() => {
	getRooms();
}, []);

const getRooms = async () => {
	const rooms = await client.query(process.env.NEXT_PUBLIC_PROJECT_ID!)
		.from(process.env.NEXT_PUBLIC_DATASTORE_CHAT_ID!)
		.select('*');
	setRooms(rooms);
}
```

一覧は画面に表示し、 Next.js の Linkを使って `/{room.id}` で遷移できるようにします。

```ts
{rooms.map((room, i) => {
	return (
		<div key={i}>
			<Link href={`/${room.id}`}>
				{room.get<string>('name')}
			</Link>
		</div>
	)
})}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/57fcf993-e1f7-565b-ba9e-ea98111bf9b2.png)


## チャット画面

チャット画面は `app/[id]/page.tsx` にて実装しています。この画面にはURL直打ちでアクセスする可能性を考慮しています。まずルームの取得、そしてこれまでのメッセージ一覧を取得します。

```tsx
// ルームのステート
const [room, setRoom] = useState<Item | undefined>(undefined);
// メッセージのステート
const [messages, setMessages] = useState<ItemHistory[]>([]);
// 新規入力されたメッセージのステート
const [comment, setComment] = useState<string>('');

// 最初にルームを取得する
useEffect(() => {
	getRoom();
}, []);

// ルームを取得する処理
const getRoom = async () => {
	// Hexabaseの初期化
	await initHexabase();
	// プロジェクト、データストア、アイテム（ルーム）の取得
	const project = await client.currentWorkspace?.project(process.env.NEXT_PUBLIC_PROJECT_ID!);
	const datastore = await project?.datastore(process.env.NEXT_PUBLIC_DATASTORE_CHAT_ID!);
	const room = await datastore?.item(id);
	if (!room) return;
	// ルームのステートを更新
	setRoom(room);
	// これまでのメッセージを取得
	setHistory(room);
	// ルームの更新を監視
	subscribe(room);
};

// Hexabaseの初期化処理
const initHexabase = async () => {
	const token = localStorage.getItem('token');
	if (!token) return;
	await client.setToken(token);
	await client.setWorkspace(process.env.NEXT_PUBLIC_WORKSPACE_ID!);
};

// これまでのメッセージを取得する処理
const setHistory = async (room: Item) => {
	const histories = await room?.histories();
	// 空文字列、または __refresh__ は除外
	setMessages((histories || [])
		.filter(history => history.comment !== '' && history.comment !== '__refresh__'));
}

// ルームの更新を監視する処理
const subscribe = (room: Item) => {
	// 更新通知がきたら、これまでのメッセージを取得し直す
	room?.subscribe('update', history => setHistory(room));
}
```

`messages` は一覧表示します。ログインユーザーと投稿者が同じ場合は、データを削除できるようにします。

```tsx
<List spacing={3}>
	{messages.map((message, i) => {
		return (
			<ListItem key={i}>
				{message.comment} by {message.user?.userName}
				{message.user?.userName === client.currentUser?.userName && <Button onClick={() => remove(message)}>削除</Button>}
			</ListItem>
		)
	})}
</List>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/115f069a-4e36-e4c0-2b23-9e8a191067d8.png)


## 新規メッセージの投稿

フォームで新しいメッセージを入力します。

```tsx
<form onSubmit={submit}>
	<input type="text" id="comment" value={comment} onChange={e => setComment(e.target.value)} />
	<button type="submit">送信</button>
</form>
```

`submit` 処理で、新しいメッセージを投稿します。

```ts
// メッセージを送信する処理
const submit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
	e.preventDefault();
	const history = room!.comment();
	history.set('comment', comment);
	await history.save();
	setComment('');
};
```

保存されると、 `subscribe` で登録した処理が実行され、 `setHistory` でメッセージ一覧を更新します。

```ts
// ルームの更新を監視する処理
const subscribe = (room: Item) => {
	// 更新通知がきたら、これまでのメッセージを取得し直す
	room?.subscribe('update', history => setHistory(room));
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/93a07a7f-104b-001f-9998-2fb67f988451.png)


## メッセージの削除

メッセージの削除は一覧の `remove` 関数呼び出しで行います。メッセージを削除しても通知されないので、特別に `__refresh__` というメッセージを送っています。このメッセージの通知を受け取ったら、一覧を強制リフレッシュしています。

```ts
// メッセージを削除する処理
const remove = async (history: ItemHistory) => {
	await history.delete();
	// 強制更新させるメッセージを送信
	const deleteMessage = room!.comment();
	deleteMessage.set('comment', '__refresh__');
	await deleteMessage.save();
}

// ルームの更新を監視する処理
const subscribe = (room: Item) => {
	// 更新通知がきたら、これまでのメッセージを取得し直す
	room?.subscribe('update', history => setHistory(room));
}

// これまでのメッセージを取得する処理
const setHistory = async (room: Item) => {
	const histories = await room?.histories();
	// 空文字列、または __refresh__ は除外
	setMessages((histories || [])
		.filter(history => history.comment !== '' && history.comment !== '__refresh__'));
}
```

APIの呼び出し回数を減らすなら、メッセージのIDを一緒に送ってフィルタリングする方法でも良さそうです。

## まとめ

SDKを使うことで、Hexabaseを使ったアプリに簡単なチャット機能を追加できます。ぜひWebアプリケーション開発に活用してください。

Hexabaseには他にもファイルストレージやスクリプトなどの機能があります。それらの機能も利用してください。

[Hexabase | 新規事業向け開発・競争領域でのDX実現をサポート](https://hexabase.com/)