How to build a movie search app using React HooksをSvelteで試してみた

# はじめに

Svelte Advent Calendar 2020 2日目の担当です。
今回はReact HooksのTutorialである[How to build a movie search app using React Hooks
](https://www.freecodecamp.org/news/how-to-build-a-movie-search-app-using-react-hooks-24eb72ddfaf7/)をSvelteで試してみたいと思います。
React hooksの部分についてですが、今回は[react-hooks-in-svelte](https://github.com/joshnuss/react-hooks-in-svelte)を参考に代替することにしています。

# 完成物
![Svelte-Hooked](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325401/552d9f85-840e-4d2f-f985-0ea01746f559.gif)


# Tutorialを試してみる

## Header.svelte

```jsx

<script>
  export let title;
</script>

<style>
  .App-header {
    background-color: #282c34;
    height: 70px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
    padding: 20px;
    cursor: pointer;
  }

  .App-header h2 {
    margin: 0;
  }
</style>

<header class="App-header">
  <h2>{title}</h2>
</header>
```

`{title}`になっている箇所は`App.svelte`から値を渡すようにしています。
内容的にはReactのTutorialとほぼ変わらないですね。

## Movie.svelte

```jsx
<script>
  const DEFAULT_PLACEHOLDER_IMAGE =
  "https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg";
  export let movie;
  $: poster = movie.Poster === "N/A" ? DEFAULT_PLACEHOLDER_IMAGE : movie.Poster;
</script>

<style>
.movie {
  padding: 5px 25px 10px 25px;
  max-width: 25%;
}

@media screen and (min-width: 694px) and (max-width: 915px) {
  .movie {
    max-width: 33%;
  }
}

@media screen and (min-width: 652px) and (max-width: 693px) {
  .movie {
    max-width: 50%;
  }
}


@media screen and (max-width: 651px) {
  .movie {
    max-width: 100%;
    margin: auto;
  }
}
</style>

<div class="movie">
  <h2>{movie.Title}</h2>
  <div>
    <img
      width="200"
      alt={`The movie titled: ${movie.Title}`}
      src={poster}
    />
  </div>
  <p>({movie.Year})</p>
</div>
```

Movieを表示する部分になります。
ここでは`$:`Reactive Statementsを使用して値をリアクティブしています。
今回はmovie一つ一つのposterの有無を確認するような処理になっておりますが、一つのコンポーネント内でReactive Statementsを使うことも可能です。

## Search.svelte

```jsx
<script>
  export let query = '';
</script>

<style>
  .search > input[type="text"]{
    width: 40%;
    min-width: 170px;
  }
	
  .search {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    margin-top: 10px;
  }
</style>

<form class="search">
  <input
    bind:value={query}
    type="text"
    on:keyup
    placeholder="title" />
</form>
```

Search.svelteでは`bind:`, `on:`ディレクティブを使用してApp.svelteに値を渡したり、関数を呼び出ししています。


## App.svelte

```jsx
<script>
  import Header from './Header.svelte';
  import Movie from './Movie.svelte';
  import Search from './Search.svelte';
  import { onMount } from 'svelte'
  const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=4a3b711b"; // you should replace this with yours
  let query = '';
  let movies = [];
  let loading = null;
  let errorMessage = '';
	
  onMount(async () => {
    fetch(MOVIE_API_URL)
      .then(response => response.json())
      .then(jsonResponse => {
        if (jsonResponse.Response === "True") {
          movies = jsonResponse.Search;
        } else {
          errorMessage = jsonResponse.Error;
        }
    });
  });
	
  function searchMovies() {
    loading = true;
    fetch(`https://www.omdbapi.com/?s=${query}&apikey=4a3b711b`)
      .then(response => response.json())
      .then(jsonResponse => {
        if (jsonResponse.Response === "True") {
          movies = jsonResponse.Search;
          loading = false;
        } else {
	  errorMessage = jsonResponse.Error;
          loading = false;
        }
    });
  }
</script>

<style>
  .App {
    text-align: center;
  }
		
  .App-intro {
    font-size: large;
  }

/* new css for movie component */

  * {
    box-sizing: border-box;
  }

  .movies {
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
  }

  .errorMessage {
    margin: auto;
    font-weight: bold;
    color: rgb(161, 15, 15);
  }
</style>

<div class="App">
  <Header title="SVELTE HOOKED" />
  <Search bind:query on:click={searchMovies} />
  <p class="App-intro">Sharing a few of our favourite movies</p>
  
  <div class="movies">
    {#if loading && !errorMessage }
      <span>loading...</span>
    {:else if errorMessage }
      <div class="errorMessage">{errorMessage}</div>
    {:else}
      {#each movies as movie}
        <Movie movie={movie} />
      {/each}
    {/if}	
  </div>
</div>
```
Tutorialに記載されている`useEffect`の箇所は今回は`onMount`を使っています。
一覧表示の条件としてloadingがtrueであれば、loading...と表示して、エラーがあればエラー内容の表示、それ以外は一覧を表示するようになっています。
エラーメッセージはAPIレスポンスが`True`意外の時に`Error`から取得しています。
`bind:query`の部分ですが名前と値が一致する場合省略可能となるため、省略するようにしています。

# おわりに
今回作成したAppを[Svelte REPL](https://svelte.dev/repl/82e6c7009e204f31b4f6bfaf5b509f19?version=3.29.4)にて公開しております。
最後にSvelteに少しでも興味を持った方は[Svelte Tutorial](https://svelte.dev/tutorial/basics)をご覧ください！

# 参考資料

[How to build a movie search app using React Hooks
](https://www.freecodecamp.org/news/how-to-build-a-movie-search-app-using-react-hooks-24eb72ddfaf7/)[react-hooks-in-svelte](https://github.com/joshnuss/react-hooks-in-svelte)
[Svelte API](https://svelte.dev/docs)