LoginSignup
2
2
この記事誰得? 私しか得しないニッチな技術で記事投稿!

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

Last updated at Posted at 2023-06-30

※諸注意

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

どういうものかの説明

QiitaApiを使って記事を取ってきて表示する
とりあえず触ってみて vue版

できること

  • qiitaページに飛べる
  • キーワードを指定
  • 一度に取ってくるページの量指定
  • ページめくり

前置き

筆者がvueに慣れているため最初にvueで作って、その次にreactで作った
なので、reactのコードには普段vueを使っている人間の目線で色々コメントを書いてある

筆者のスペック (執筆時点)

react : 6月になって触り始めた
vue : 触り初めてからもうすぐ1年になる

環境

共通

vscodeで編集
api通信にはaxiosを使う
できるだけ素の状態で比較したいため、vuetify,react-selectなどのフレームワークは入れない

react

  • react-app-createを使って作成
  • 関数型形式で記述
  • 以下の記事を参考にパス周りに変更を加えた

vue

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

vue

github pages
リポジトリ

構成

いじってないものはすべて省略
cssは今回重要な情報ではないためリポジトリを確認してほしい。

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

コード

App.vue

主な役目

  • api通信をして記事を取得
  • 他のコンポーネントを表示
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

主な役目

  • 受け取った記事のデータを表示する
  • 文字にリンクを紐付ける
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

主な役目

  • ユーザーの入力を受け取る
  • 親コンポーネントにユーザーが入力した値を渡す
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

主な役割

  • ユーザーが数字ボタンを押した時にページをめくる
  • ユーザーに今何ページ目かを知らせる
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

主な役割

  • ユーザーに一度にどれくらいの記事を取ってくるかどうかを決めてもらう
  • ユーザーが決めた値を親コンポーネントに渡す
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
リポジトリ

構成

いじってないものはすべて省略
cssは今回重要な情報ではないためリポジトリを確認してほしい。

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

App.js

主な役割

  • api通信をして記事を取得
  • 他のコンポーネントを表示
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

主な役割

  • 受け取った記事のデータを表示する
  • 文字にリンクを紐付ける
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

主な役割

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

主な役割

  • ユーザーが数字ボタンを押した時にページをめくる
  • ユーザーに今何ページ目かを知らせる
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

主な役割

  • ユーザーに一度にどれくらいの記事を取ってくるかどうかを決めてもらう
  • ユーザーが決めた値を親コンポーネントに渡す
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の違いがわかるとおもう。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2