※諸注意
細かいところを省略したり、雑な部分がありますがご容赦ください。
今回はコードを見比べるのがメインなのでデザインは手を抜いています
どういうものかの説明
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
構成
いじってないものはすべて省略
cssは今回重要な情報ではないためリポジトリを確認してほしい。
src/
├ ─asset/
│ └ main.css
├Components/
│ ├ ArticleComponent.vue
│ ├ InputComponent.vue
│ ├ PaginationComponent.vue
│ ├ SelectComponent.vue
└ App.vue
コード
App.vue
主な役目
- api通信をして記事を取得
- 他のコンポーネントを表示
<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
主な役目
- 受け取った記事のデータを表示する
- 文字にリンクを紐付ける
<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
主な役目
- ユーザーの入力を受け取る
- 親コンポーネントにユーザーが入力した値を渡す
<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
主な役割
- ユーザーが数字ボタンを押した時にページをめくる
- ユーザーに今何ページ目かを知らせる
<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
主な役割
- ユーザーに一度にどれくらいの記事を取ってくるかどうかを決めてもらう
- ユーザーが決めた値を親コンポーネントに渡す
<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
構成
いじってないものはすべて省略
cssは今回重要な情報ではないためリポジトリを確認してほしい。
src/
├Components/
│ ├ ArticleComponent.css
│ ├ ArticleComponent.js
│ ├ InputComponent.js
│ ├ PaginationComponent.css
│ ├ PaginationComponent.js
│ ├ SelectComponent.js
└ App.js
App.js
主な役割
- api通信をして記事を取得
- 他のコンポーネントを表示
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
主な役割
- 受け取った記事のデータを表示する
- 文字にリンクを紐付ける
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
主な役割
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
主な役割
- ユーザーが数字ボタンを押した時にページをめくる
- ユーザーに今何ページ目かを知らせる
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
主な役割
- ユーザーに一度にどれくらいの記事を取ってくるかどうかを決めてもらう
- ユーザーが決めた値を親コンポーネントに渡す
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の違いがわかるとおもう。