Svelteを触ってみた<コンポーネント, 親子間データ参照, API通信, Store, TypeScript使用>

# Svelteとは

公式サイト
https://svelte.dev/

私はNuxt, Vueのヘビーユーザーです。
Vue, Reactの上位互換と話題のSvelteが気になって仕方ないので触ってみました。

何が上位互換かというと、
軽量！早い！チュートリアルも充実！Store標準装備！トランジションも豊富！

らしい。
あとアップデートされてTypeScript導入が激しく簡単になったとのことです。

ロイターの大統領サイトでも使用されて話題になりましたね。


# はじめてみる

公式サイトに書いてある通りに始めてみる。

```
npx degit sveltejs/template my-svelte-project
cd my-svelte-project
```

私はTypeScriptを導入したいのでここで

```
node scripts/setupTypeScript.js
```

よし。ここまでは順調ですね。
TypeScript導入のスクリプトが非常に便利。

Nuxtも今はTS導入だいぶ楽ですけど、最初の方とか導入するだけで時間取るくらいのもんだったので感動。

さて。早速動かしてみよう。

公式サイトでは `npm` やっているけど、私は `yarn` ユーザーなので `yarn` でやります。

```
yarn install
yarn dev
```

.........
.........

.........

.........

え、なんか早速エラー出たんですけど。
なんかnode-modules/@typesがどうとかエラー。

他の人たちの記事見てもそんな事書いてないんですけど。。

と思いつつ、nodeのバージョン？と思って14以上に上げたら問題なく起動できました。

***node > 14.x.x***

って書いておいて！！（私が見落としてた説）


# env

今回API通信をするにあたってSecretKeyを使うので、env設定しないと〜ということで設定します。

```
yarn add dotenv
```

```js:.env
SECRET_KEY=XXXXXXXXXXXXXXX
```

```js:rollup.config.js
...
import dotenv from 'dotenv';
...

dotenv.config();
```
を追加。

```tsconfig.json
...
"compilerOptions": {
  "types": ["node"],
},
...
```
を追加。

これでenvが読み込まれるはず！


# 実装してみる

今回私は

①親ページを作る
②inputフォームコンポーネント(子)を作る
③子コンポーネントから親に値を渡す
④その値とAPIを用いてデータを取得する
⑤それをstoreに保持する
⑥取得したデータを画面に表示する

という流れをやってみます。

### ①親ページを作る

```js:src/pages/Top.svelte

<script lang="ts">
...
</script>

<main>
...
</main>
```

中身は一旦おいておいて、こんな感じで適当に作成

これをApp.svelteに読み込ませてあげる。

```js:src/App.svelte

<script lang="ts">
	import Top from './pages/Top.svelte'
</script>

<main>
	<Top />
</main>
```

これでTopページが読み込まれで表示される！！
App.svelteにはこんな感じでページをどんどん追加していってあげれば良さそう。



### ②inputフォームコンポーネント(子)を作る

これは一緒にやります。

```js:src/components/Form.svelte

<script lang="ts">
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();

  // この関数が呼ばれたら親にwordという名称で値が渡せる
  // VueでいうところのEmit
  const emitWord = () => {
    dispatch('word', {
      text: searchWord
    });
  }
  
  export let searchWord: string;
</script>

<main>
  <input type="text" bind:value={searchWord} />
  <button on:click={emitWord}>Search</button>
</main>

<style>
</style>
```

こんな感じですね。Vueだと@click内にemitを書いて親に値を渡せるけど、svelteはそうもいかず。
関数を作って送ってあげましょう。

### ③子コンポーネントから親に値を渡す

```js:src/pages/Top.svelte

<script lang="ts">
  import Form from '../components/Form.svelte'

  let word: string = '';
  const emitChild = (event) => {
    word = event.detail.text
  }

</script>

<main>
  // そのまま変数にデータ格納出来ないので関数をセット
  <Form on:word={emitChild} />
  <div>{word}</div>
</main>
```

コード内のコメントに書いた通り、Vueのように `hoge = $event` みたいに格納できないので、関数をセット。

event.detailの中に値が入っているみたい。
今回は{ text: 値}　のように返しているので、evett.detail.textとなっている。


### ④その値とAPIを用いてデータを取得する


```js:src/store/api/index.ts

sync function getImagesApi(query: string): Promise<void> {
  const params = {
    query,
  };
  const queryParams = new URLSearchParams(params); 

  try {
	  await fetch(`apiurl`, {
      method: 'GET',
      headers: {
        'Authorization': process.env.SECRET_KEY,
        'Content-Type': 'application/json'
      },
    }).then(async response => {
        const res = await response.json()
    });
  } catch(error) {
    ...
  }
}
```

API通信自体はこんな感じ。
まだ途中なので返り値等はなしで取得できる事が確認できる程度のコード。

ここで取得できたresponseをStoreに格納したいんだ！

### ⑤それをstoreに保持する


```js:src/store/api/index.ts

import { readable } from 'svelte/store';
import type { Readable } from 'svelte/store';

type T = {
	[key: string]: string | number |  {
		[key: string]: string | number | {
			[key: string]: string
		}
	}[]
} | null

type initVal = {
	response: T
	photos: any[]
	error: any
}
export function initVal(): initVal {
	return {
		response: null,
		photos: [],
		error: null
	}
}

export function makeImagesStore(query: string): Readable<initVal> {
	const init = initVal();
	const store = readable(init, makeSubscribe(init, query)); 
	return store;
}

function unsubscribe(): void {
	// Nothing to do in this case
}

function makeSubscribe(data: initVal, query: string): any {
	return set => {
		getImagesApi(data, set, query);
		
		return unsubscribe;
	};
}

async function getImagesApi(data: initVal, set: any, query: string): Promise<void> {
  const params = {
    query,
  };
	const queryParams = new URLSearchParams(params); 

	try {
	  await fetch(`apiURL`, {
      method: 'GET',
      headers: {
        'Authorization': process.env.SECRET_KEY,
        'Content-Type': 'application/json'
      },
    }).then(async response => {
			const res = await response.json()
			data.photos = res.photos
			data.response = res
			set(data)
		});
	} catch(error) {
		data.error = error;
		set(data);
	}
}


```

文字で説明するより見たほうが早いと思うので要点を。

初期化してあげる、値をセットするという事をやっている。

```js:src/pages/Top.svelte

<script lang="ts">
  import Form from '../components/Form.svelte'
  import { makeImagesStore, initVal } from '../store/pexels'

  let images = initVal();
  function updateUsers(data) {
    images = data;
  }

  let word: string = '';
  const emitChild = (event) => {
    word = event.detail.text

    // storeを初期化、検索ワードを送る
    let store = makeImagesStore(event.detail.text);
    // 更新された値を変数に格納するトリガー
    store.subscribe(updateUsers);
  }

</script>

<main>
  <Form on:word={emitChild} />
  <div>{word}</div>
  {#if images.photos.length === 0}
    該当する写真がありません。
  {/if}
  {#each images.photos as image}
    <img src={image.src.small} alt="img" />
  {/each}
</main>
```

こんな感じでStoreから値を取得することができる。

storeの書き方を変えれば、他のページからAPIを叩かずに保持している値を参照もできるはず。
（そのやり方やったら更新します）


簡単で雑ですがこんな感じでおわります。