とりあえず､reactとvueで同じものを作って見比べてみる (QiitaAPi編)

# ※諸注意
細かいところを省略したり､雑な部分がありますがご容赦ください。
今回はコードを見比べるのがメインなのでデザインは手を抜いています

# どういうものかの説明
QiitaApiを使って記事を取ってきて表示する
[とりあえず触ってみて vue版](https://s19013.github.io/qiita-api-vue/)

#### できること
* qiitaページに飛べる
* キーワードを指定
* 一度に取ってくるページの量指定
* ページめくり

# 前置き
筆者が`vue`に慣れているため最初に`vue`で作って､その次に`react`で作った
なので､`react`のコードには普段`vue`を使っている人間の目線で色々コメントを書いてある
## 筆者のスペック (執筆時点)
react : 6月になって触り始めた
vue : 触り初めてからもうすぐ1年になる
## 環境
### 共通
vscodeで編集
api通信には`axios`を使う
できるだけ素の状態で比較したいため､`vuetify`,`react-select`などのフレームワークは入れない
### react 
* `react-app-create`を使って作成
* 関数型形式で記述
* 以下の記事を参考にパス周りに変更を加えた

https://qiita.com/task-k/items/6286a2d9437cf6e182f0

### vue
* `npm init vue@latest`を使って作成
* `composition api`形式で記述(筆者の練習も兼ねて)

# vue
[github pages](https://s19013.github.io/qiita-api-vue/)
[リポジトリ](https://github.com/s19013/qiita-api-vue)

## 構成
いじってないものはすべて省略
cssは今回重要な情報ではないため[リポジトリ](https://github.com/s19013/qiita-api-vue)を確認してほしい｡

src/
├ ─asset/
│&emsp;&emsp;└ main.css
├Components/
│&emsp;&emsp;├ ArticleComponent.vue
│&emsp;&emsp;├ InputComponent.vue
│&emsp;&emsp;├ PaginationComponent.vue
│&emsp;&emsp;├ SelectComponent.vue
└ App.vue

## コード

### App.vue
#### 主な役目
* api通信をして記事を取得
* 他のコンポーネントを表示

```javascript:App.vue
<script setup>
import axios from 'axios';
import { ref,reactive,onMounted,nextTick } from 'vue';
import ArticleComponent from './components/ArticleComponent.vue';
import InputComponent from './components/InputComponent.vue';
import SelectorComponent from './components/SelectorComponent.vue';
import PaginationComponent from './components/PaginationComponent.vue';

// コンポーネントたち
const Input =ref(null)
const PerPageSelector = ref(null)

// qiita記事
const articles = ref([]);

// 入力記録(ページめくりで使う)
const InputHistory = reactive({
  page:1,
  per_page:10,
  keyword:null
})

/** qiitaの記事取ってくる */
const getArticle = async({page,per_page,keyword}) => {
  console.log(per_page,keyword);
  await axios.get('https://qiita.com/api/v2/items',{
    params:{
      page:page,
      per_page:per_page,
      query:keyword,
    }
  })
  /** @param {res} api通信で取ってきたデータたち */
  .then((res)=>{
      // 配列を入れ替え
      articles.value = res.data

      // 履歴更新
      Object.assign(InputHistory,{ 
        page:page,
        per_page:per_page,
        keyword:keyword
      });
  })
  .catch((errors) => {
    console.log(errors);
  })
}

const search = () =>{
  getArticle({
    page:1,
    per_page:PerPageSelector.value.passSelected(),
    keyword:Input.value.passKeyword()
  })
}

const turnPage = (num) => {
  getArticle({
    page:num,
    per_page:InputHistory.per_page,
    keyword:InputHistory.keyword
  })
}

onMounted(() => { 
  getArticle({
    page:1,
    per_page:InputHistory.per_page,
    keyword:InputHistory.keyword
  })
});
</script>

<template>
  <main>
    <p>1時間につき60回まで</p>
    <div class="search">
      <InputComponent ref="Input" :original="InputHistory.keyword"/>
      <button @click="search">検索</button>
    </div>

    <SelectorComponent 
      ref="PerPageSelector" 
      :original="InputHistory.per_page" 
      :options=[5,10,15,20,25,30] 
    />
    <template v-for="(article,index) in articles" :key="index">
      <ArticleComponent
        :article="article"
      />
    </template>
    <!-- qiitaapiでは100ページ目までしか見れないらしいので -->
    <PaginationComponent 
      :original="InputHistory.page" 
      :pageCount="100"
      @turnPage="turnPage"
    />
  </main>
</template>

<style scoped lang="scss">
.main{

}
</style>

```

### ArticleComponent.vue
#### 主な役目
* 受け取った記事のデータを表示する
* 文字にリンクを紐付ける

```javascript:ArticleComponent.vue
<script setup>
import { defineProps } from 'vue';

// 親から受け取るデータ
const props = defineProps(['article'])

</script>

<template>
    <div class="article">
        <a :href="props.article.url" target="_blank" rel="noopener noreferrer">
            <h2>{{ props.article.title }}</h2>
        </a>
    </div>
</template>

<style scoped lang="scss">
.article{
    border: 1px dashed #000000;
    margin: 0.5rem;
    padding:0.5rem;

}
</style>

```

### InputComponent.vue
#### 主な役目
* ユーザーの入力を受け取る
* 親コンポーネントにユーザーが入力した値を渡す

```javascript:InputComponent.vue
<script setup>
import {ref,defineProps,watch } from 'vue';

const props = defineProps({
    original:{
        type:String,
        default:null,
    }
})
const keyword = ref(props.original)

// 親が子の情報を受け取るための関数
// 親が任意のタイミング(検索ボタンを押した時など)に動かす
const passKeyword = () => {
    // QiitaApiではqueryが空文字だとなにも取ってこないので回避
    if (keyword.value == '') {keyword.value = null}
    return keyword.value
}

// propsの値が変化したらkeywordを更新する
// そのままprops.originalだと動かないらしい
// おそらくアロー関数使って帰り値で判断してるのかも?
watch( () => props.original,(newValue,oldValue) => {
  keyword.value = newValue
})

// これに入れることで､親が子の関数を動かすことができる
defineExpose({
    passKeyword
});
</script>

<template>
    <input type="text" v-model="keyword"/>
</template>

<style lang="scss" scope>
</style>
```

### PaginationComponent.vue
#### 主な役割
* ユーザーが数字ボタンを押した時にページをめくる
* ユーザーに今何ページ目かを知らせる

```javascript:PaginationComponent.vue
<script setup>
import {ref,defineProps,watch,onMounted,defineEmits } from 'vue';

const selected = ref(props.original)
const displayPages = ref([])

/**
 * @param original 初期値
 * @param pageCount ページ数
 */
const props = defineProps({
    original:{
        type:Number,
        default:1,
    },
    pageCount:{
        type:Number,
        default:100
    }
})

const emit = defineEmits(['turnPage'])

/** @func displayPagesを作り直す
 *  @param min 表示上の最小値
 *  @param max 表示上の最大値
*/
const rebuildingDisplayPages = (min,max) => {
    const temp = []
    for (let index = min; index <= max; index++) {
        temp.push(index)
    }

    displayPages.value = temp
}

// ページをめくる(親コンポーネントに連絡)
const turnPage = (num) => { emit('turnPage',num) }

// watchとmountedで使う処理
const divide = () => {
    // props...とか書くのめんどいので変数に入れとく
    const pageCount = props.pageCount.value

    // selectedが5以下の場合表示の仕方が特殊
    if (selected.value <= 5 ) {rebuildingDisplayPages(1,9)}
    // pageCountに近い時も特殊
    else if (selected.value > pageCount) {
        rebuildingDisplayPages(pageCount - 8,pageCount)
    }
    else {rebuildingDisplayPages(selected.value - 4,selected.value + 4)}
}

// propsの値が変化したらselectedを更新する
watch(() => props.original,(newValue,oldValue) => {
    selected.value = newValue
    divide()
})

onMounted(() => { divide() });
</script>

<template>
    <div class="pagination">
        <ul>
            <li v-for="num in displayPages" :key="num">
                <button 
                    :class="num == selected ? 'selected': ''" 
                    :disabled="num == selected"
                    @click="turnPage(num)"
                >
                    {{ num }}
                </button>
            </li>
        </ul>
    </div>
</template>

<style scoped lang="scss">
ul{display: flex;}
li{
    list-style: none;
    button{ padding: 0.5rem  1rem; }
}
.selected{ background-color: aquamarine; }
</style>
```

### SelectorComponent.vue
#### 主な役割
* ユーザーに一度にどれくらいの記事を取ってくるかどうかを決めてもらう
* ユーザーが決めた値を親コンポーネントに渡す

```javascript:SelectorComponent.vue
<script setup>
import {ref,defineProps,watch } from 'vue';

const selected = ref(props.original)

/** 
 * @param {original} 初期値
 * @param {options} optionタグの情報
*/
const props = defineProps({
    original:{
        type:[ String, Number],
        required: true
    },
    options:{
        type:Array,
        required: true
    }
})

/**  親が子の情報を受け取る */
const passSelected = () => { return selected.value }

/**  propsの値が変化したらselectedを更新する */
watch(() => props.original,(newValue,oldValue) => {
  selected.value = newValue
})

defineExpose({ passSelected });
</script>

<template>
    <div class="selector">
        <select v-model="selected">
            <option v-for="(option,index) in options" :key="index" :value="option">
                {{ option }}
            </option>
        </select>
    </div>
</template>

<style scoped lang="scss">

</style>
```

# React
[github pages](https://s19013.github.io/qiita-api-react/)
[リポジトリ](https://github.com/s19013/qiita-api-react)
## 構成
いじってないものはすべて省略
cssは今回重要な情報ではないため[リポジトリ](https://github.com/s19013/qiita-api-vue)を確認してほしい｡

src/
├Components/
│&emsp;&emsp;├ ArticleComponent.css
│&emsp;&emsp;├ ArticleComponent.js
│&emsp;&emsp;├ InputComponent.js
│&emsp;&emsp;├ PaginationComponent.css
│&emsp;&emsp;├ PaginationComponent.js
│&emsp;&emsp;├ SelectComponent.js
└ App.js

### App.js
#### 主な役割
* api通信をして記事を取得
* 他のコンポーネントを表示

```javascript:App.js
import './App.css';
import axios from 'axios';
import { useState,useEffect } from 'react';
import ArticleComponent from './Components/ArticleComponent';
import InputComponent from './Components/InputComponent';
import SelectorComponent from './Components/SelectorComponent';
import PaginationComponent from './Components/PaginationComponent';

function App() {
    /** 記事 */
    const [articles,setArticles] = useState([])

    /** 読み込み中の文字 */
    const [loading,setLoading] = useState("")

    /** 検索窓の文字 */
    const [keyword,setKeyword] = useState(null)

    /** 検索表示数 */
    const [per_page,setPerPage] = useState(10)

    /** 何ページ目か */
    const [page,setPage] = useState(1)

    /** 入力記録 ページめくりで使う */
    const [inputHistory,setInputHistory] = useState({
        page:page,
        per_page:per_page,
        keyword:keyword
    })



    /** 記事取ってくる */
    const getArticle = async({page,per_page,keyword}) => {
        // 読み込み中の文字表示
        setLoading(<p>読み込み中</p>)

        await axios.get('https://qiita.com/api/v2/items',{
            params:{
                page:page,
                per_page:per_page,
                query:keyword
            }
        })
        .then((res) => {
            setArticles(res.data) 
            // 検索した情報を保管
            setInputHistory({
                page:page,
                per_page:per_page,
                keyword:keyword
            })
        })
        .catch((err) => { console.log(err); })

        // 読み込み中の文字削除
        setLoading("")
    }

    // 記事を表示する用の変数
    const articleList = articles.map((article,index) => {
        return ( <ArticleComponent article={article} key={index}/> )
    })

    /** 検索 */
    const search = () => {
        // 空文字だったらnullにする必要がある じゃないと何も取ってこない
        if (keyword == "") { setKeyword(null) }
        getArticle({
            page:1,
            per_page:per_page,
            keyword:keyword
        })

        // pageをリセット
        setPage(1)
    }

    /** ページめくり */
    const turnPage = (page) => {
        setPage(page)
        getArticle({
            page:page,
            per_page:inputHistory.per_page,
            keyword:inputHistory.keyword
        })
    }

    // このwebページを最初に読み込んだ時の動き
    // vueでいうmounted
    useEffect(() => {
        getArticle({
            page:inputHistory.page,
            per_page:inputHistory.per_page,
            keyword:inputHistory.keyword
        })
    },[])

  return (
    <div className="App">
        <p>1時間につき60回まで</p>
        <InputComponent 
            text={inputHistory.keyword}
            setKeyword={setKeyword}
        />
        <button onClick={() => {search()}}>push</button>
        <SelectorComponent 
            options={[5,10,15,20,25,30]}
            per_page={inputHistory.per_page}
            setPerPage={setPerPage}
        />

        {loading}

        {articleList}
        <PaginationComponent
            selected={inputHistory.page}
            pageCount={100}
            turnPage={turnPage}
        />
    </div>
  );
}

export default App;

```

### ArticleComponent.js
#### 主な役割
* 受け取った記事のデータを表示する
* 文字にリンクを紐付ける

```javascript:ArticleComponent.js
import './ArticleComponent.css'

export default function ArticleComponent({article}) {
    return (
        <div className="article">
            <a href={article.url} target="_blank" rel="noopener noreferrer">
                <h2>{article.title}</h2>
            </a>
        </div>
    )
}
```

### InputComponent.js
#### 主な役割

```javascript:InputComponent.js
export default function ArticleComponent({keyword,setKeyword}) {

    /** 入力するたびに親コンポーネントの`keyword`に代入 */
    const handleChange = (e) => {setKeyword(e.target.value)}

    return (
        <div className='input'>
            <input
                type="text"
                value={keyword}
                onChange={handleChange}
            />
        </div>
    )
}
```

### PaginationComponent.js
#### 主な役割
* ユーザーが数字ボタンを押した時にページをめくる
* ユーザーに今何ページ目かを知らせる

```javascript:PaginationComponent.js
import { useState,useEffect } from 'react';
import './PageinationComponent.css'

export default function PaginationComponent({selected,pageCount,turnPage}) {

    // vueでいうwatchとmounted
    /** selectedに変化があったら動かす */
    useEffect(() => {
        /** 5以下の時の処理 */
        if (selected <= 5) {rebuildingDisplayPages(1,9)}
        /** pageCountに近い時も特殊 */
        else if (selected > pageCount - 5) {rebuildingDisplayPages(pageCount - 8,pageCount)}
        else {rebuildingDisplayPages(selected - 4,selected + 4)}
      }, [selected])


    // 一番下に表示するやつの原型
    const [pages,setPages] = useState([])

    /** 画面の一番したに表示するやつ */
    const displayPages = pages.map((page,index) => {
        return (
            <li key={index}>
                <button
                    className={page === selected ? 'selected': ''}
                    disabled={page === selected}
                    onClick={() =>{turnPage(page)}}
                >
                    { page }
                </button>
            </li>
        )
    })
    
    // value={page} にすればe.target.valueで受け取れる
    // turnPage(page)ってそのまま書くと表示されるのと同時に1度関数が動いてしまうもよう
    // 加えて無限ループぽい動きを見せた

    /** @func displayPagesを作り直す
     *  @param min 表示上の最小値
     *  @param max 表示上の最大値
    */
    const rebuildingDisplayPages = (min,max) => {
        const temp = []
        for (let index = min; index <= max; index++) { temp.push(index) }
        setPages(temp)
    }

    return (
        <div className="pagination">
            <ul>
                {displayPages}
            </ul>
        </div>
    )
}
```

### SelectorComponent
#### 主な役割
* ユーザーに一度にどれくらいの記事を取ってくるかどうかを決めてもらう
* ユーザーが決めた値を親コンポーネントに渡す
```javascript:SelectorComponent.js
export default function SelectorComponent({options,per_page,setPerPage}) {
    /** 入力するたびに親コンポーネントの`per_page`に代入 */
    const handleChange = (e) => {
        // e.target.valueでoptionのvalueを受けるもよう
        setPerPage(e.target.value)
    }


    /** オプションを表示 */
    const renderOptions = options.map((option,index) => {
        return (<option value={option} key={index}>{option}</option>)
    })

    return (
        <div className="selector">
            <select value={per_page} onChange={handleChange}>
                {renderOptions}
            </select>
        </div>
    )
}
```

# 思ったこと､気づいたこと､感想など
※ reactを触り初めてまもない実務未経験の人間の感想です

reactのほうが若干vueよりコード数が少ない気がする

##### propsを直接弄ってよいか
react:直接弄ってよい
vue:直接弄ろうとすると警告がでる(非推奨とされている)

##### reactとvueで設計を変える必要がある
例えば､私は`InputComponent.js`,`InputComponent.vue`で親コンポーネントが子コンポーネントの情報を取得するのに
Vueでは親コンポーネントが子コンポーネントの関数を呼び出して帰り値を受け取る方法をとったが｡
Reactでは`forwardRef`,`useRef`を使えば似たようなことができるが､React公式は非推奨としているので親コンポーネントの変数を変更させる関数を子コンポーネントに渡して､その関数を通して親コンポーネントに変更を伝える方法をとった｡
しかし､これでは親コンポーネントが沢山の変数や関数などを持ってそのうち神コンポーネントになってしまうのではないだろうかと私は思う｡

##### cssについて
vueではstyleタグの中にかける,scssでも書ける｡
reactでは別ファイルで書く必要があるし､scssで書きたい時はまた別で準備が必要

##### 本編の締め
執筆時点では､reactは触り初めてまだ1ヶ月もたっていないのでまた何かしらを作るなりして､reactへの理解を深めて行きたい｡

# 駄文のコーナー
Vueのoption apiに慣れているからという理由もあると思うが､私個人としては今現在(執筆時時点)では､vueのoption apiが一番楽にかけると感じているので､今後私が個人開発をする時は､vueのoption apiを採用すると思う

## composition apiについて
変数の値を参照したりするのに`.value`とか書くの面倒
親コンポーネントから子コンポーネントの関数を呼び出すのに`defineExpose`ってやつが必要なのが面倒
他面倒だと感じる部分がチラホラとある
現段階では､いまいち良さがまだ理解できない｡
reactと同じように触り始めてまだ1ヶ月もたっていないのでまた何かしらを作るなりして､理解を深めて行きたい｡

# ぶっちゃけ
この記事を読むよりも以下の記事を読んだほうがよりreactとvueの違いがわかるとおもう｡

https://www.hypertextcandy.com/react-for-vuejs-developers

https://zenn.dev/poteboy/articles/ce47ec05498cfa

https://designsupply-web.com/media/programming/7272/