Next.js Practice:1-4 next/routerオブジェクトの練習
【0】はじめに:今回作成してみるもの
今回はSelectボックスの項目を選んでURLを書き換える(URLにクエリパラメータを付与する)。さらに、書き換えたURLにあるクエリパラメータを使って表示内容を切り替える(フィルタリングする)。
【1】vtuber一覧を取得する関数を作成
まずは、「vgroupname」をわたすと、その「vgroupid」に紐づくvtuber一覧を取得する関数を作成する。
【./utils/database/getVtubers.ts】
※今回はテーブルがわかれているので、VGROUPテーブルでの検索結果を使ってVTUBERテーブルを検索する挙動。
import { VtuberModel } from './../../interfaces/vtuber';
import { openDB } from "../../pages/openDB";
//SQLの結果を受け付ける型
interface Vgroup{
vgroupid:number;
vgroupname:string;
}
export async function getVtubers(vgroupname:string) {
const db = await openDB();
const result = await db.get<Vgroup|undefined>(`
SELECT vgroupid, vgroupname
FROM VGROUP
WHERE vgroupname = ?
`
,vgroupname);
//debug
//console.log(result?.vgroupid, result?.vgroupname);
const vtubers = await db.all<VtuberModel[]>(`
SELECT *
FROM VTUBER
WHERE vgroupid = ?
`
,result?.vgroupid
);
return vtubers || null;
}
【2】apiで動作確認(apiの復習、クエリパラメータの取り扱い)
■apiエンドポイント作り方
・「pages/api配下」にソースコードを書く。
・URL「/api/ファイル名」がapiエンドポイントになる。
・関数の引数の型は(req:NextApiRequest,res: NextApiResponse)
【./pages/api/vtubers.ts】※クエリパラメータへの前処理未実施
export default async function vtubers(req:NextApiRequest,res:NextApiResponse) {
const vgroup = req.query.vgroup;
const data = await getVtubers(vgroup);
res.json(data);
}
単純にクエリパラメータ「vgroup」から値を取って、それを作成した関数に渡せばよさそうに思えるが、エディタ側で以下のようなエラーを吐く。
「string | string[]」という記述があるが、これは同じクエリパラメータから取得する値が複数あると配列が返ってくるため。
【例】
・「www.myapp.com?myparam=hello」でアクセス
⇒「req.query.myparam」で「hello」が返ってくる
・「www.myapp.com?myparam=hello&myparam=hello2」みたいに同じパラメータ名を複数セットしてアクセス。
⇒「req.query.myparam」で「[hello, hello2]」の配列が返ってくる。
そこで
「同名のクエリパラメータを複数セットされ、配列データを取得しても最初のクエリパラメータだけを取ってくる」
ようにする。
【./pages/api/vtubers.ts】※クエリパラメータへの処理追加済み
import { NextApiRequest, NextApiResponse } from 'next';
import { getVtubers } from '../../utils/database/getVtubers';
function getAsString(value:string|string[]):string {
if(Array.isArray(value)){
return value[0];
}
return value;
}
export default async function vtubers(req:NextApiRequest,res:NextApiResponse) {
const vgroup = getAsString(req.query.vgroup)
console.log(vgroup);
const data = await getVtubers(vgroup);
res.json(data);
}
↓ さらにユーティリティ関数として分離して使いやすくする
【./utils/getAsString.ts】
export function getAsString(value:string|string[]):string {
if(Array.isArray(value)){
return value[0];
}
return value;
}
【./pages/api/vtubers.ts】※getAsString()はimportする
import { NextApiRequest, NextApiResponse } from 'next';
import { getVtubers } from '../../utils/database/getVtubers';
import { getAsString } from '../../utils/getAsString';
// export function getAsString(value:string|string[]):string {
// if(Array.isArray(value)){
// return value[0];
// }
// return value;
// }
export default async function vtubers(req:NextApiRequest,res:NextApiResponse) {
const vgroup = getAsString(req.query.vgroup)
//console.log(vgroup);
const data = await getVtubers(vgroup);
res.json(data);
}
■実行結果
「/api/vtubers?vgroup=VOMS」とかでアクセスする
【3】getServerSideProps内で複数回のデータ取得を実施する
「getServerSideProps()」内で「 getVgroupCount()」、「getVtubers()」の2つのデータ取得関数をコールする。
この時に注意すべきは非同期処理。
⇒ 両方のデータ取得が完了した後にpropsとしてコンポーネントに渡す
【./pages/home.tsx】
import { GetServerSideProps } from "next";
import { getVgroupCount, Vcount } from "../utils/database/getVgroupsCount";
import {Formik,Form} from 'formik';
import {Paper, Grid,Button} from '@material-ui/core';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { Field } from "formik";
import { useRouter } from "next/dist/client/router";
import { getVtubers } from "../utils/database/getVtubers";
import { getAsString } from "../utils/getAsString";
import { VtuberModel } from "../interfaces/vtuber";
export interface HomeProps{
vgcounts:Vcount[];
vtubers:VtuberModel[];
}
//css:マージン調整など
const useStyles = makeStyles((theme: Theme) =>
createStyles({
paper: {
margin: 'auto',
maxWidth: 500,
padding: theme.spacing(3) //padding設定
}
}),
);
const Home = ({vgcounts,vtubers}:HomeProps) =>{
const classes = useStyles();
const {query} = useRouter();
//Formikフォームの初期値
const initialValues ={
vgroup: query.vgroup || '' //selectボックスの初期値
}
return(
<div>
<Formik
initialValues={initialValues}
onSubmit={(values,formikHelpers)=>{
//alert(values.vgroup);
//console.log(values);
//console.log(formikHelpers);
//console.log("--------");
}}>
{(values)=>(
<Form>
<Paper elevation={5} className={classes.paper}>
<FormControl fullWidth variant="outlined">
<InputLabel id="search-vgroup">Vgroup</InputLabel>
<Field name="vgroup" as={Select} labelId="search-vgroup" label="Vgroup">
<MenuItem value="">
<em>None</em>
</MenuItem>
{
vgcounts.map((vgcount)=>{
return(
<MenuItem value={vgcount.vgroupname} key={vgcount.vgroupname}>
{`${vgcount.vgroupname} (${vgcount.count})`}
</MenuItem>
);
})
}
</Field>
</FormControl>
<Button type="submit" variant="contained" color="primary">Submit(Debug)</Button>
</Paper>
</Form>
)}
</Formik>
<h1>{JSON.stringify(vgcounts,null,4)}</h1>
<h1>{JSON.stringify(vtubers,null,4)}</h1>
</div>
);
}
export default Home
export const getServerSideProps:GetServerSideProps = async(ctx)=>{
//const vgcounts = await getVgroupCount();
const vgroup = getAsString(ctx.query.vgroup);
const [vgcounts, vtubers] = await Promise.all(
[
getVgroupCount(),
getVtubers(vgroup)
]
);
//return {props:{vgcounts}};
return { props:{vgcounts,vtubers}};
}
ここまでで、「URL直打ち」+[Enter]でクエリパラメータを使ったフィルタリングみたいなことができた。
【4】URLを書き換える処理(next/routerオブジェクトの練習)
最後に、「submitボタンをおすとURLを書き換える処理」を追加する。
ここで使うのが「next/router」の「router.push」。
submitボタンを押すとURLを書き換える
⇒ 「Formik」の「onSubmit」内に記述する。
【./pages/home.tsx】
※必要なクエリパラメータ以外に、動作確認用にクエリパラメータ:「test:1」を仕込んでみている
import { GetServerSideProps } from "next";
import { getVgroupCount, Vcount } from "../utils/database/getVgroupsCount";
import {Formik,Form} from 'formik';
import {Paper, Grid,Button} from '@material-ui/core';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { Field } from "formik";
import router, {useRouter } from "next/router";
import { getVtubers } from "../utils/database/getVtubers";
import { getAsString } from "../utils/getAsString";
import { VtuberModel } from "../interfaces/vtuber";
export interface HomeProps{
vgcounts:Vcount[];
vtubers:VtuberModel[];
}
//css:マージン調整など
const useStyles = makeStyles((theme: Theme) =>
createStyles({
paper: {
margin: 'auto',
maxWidth: 500,
padding: theme.spacing(3) //padding設定
}
}),
);
const Home = ({vgcounts,vtubers}:HomeProps) =>{
const classes = useStyles();
const {query} = useRouter();
//Formikフォームの初期値
const initialValues ={
vgroup: query.vgroup || '' //selectボックスの初期値
}
return(
<div>
<Formik
initialValues={initialValues}
onSubmit={(values,formikHelpers)=>{
router.push({
pathname:'/home',
query:{...values,test:1} //付与するクエリストリング
}
, undefined, {shallow:false} //trueだとgetServerSidePropsが動かないshallowになる
)
}}>
{(values)=>(
<Form>
<Paper elevation={5} className={classes.paper}>
<FormControl fullWidth variant="outlined">
<InputLabel id="search-vgroup">Vgroup</InputLabel>
<Field name="vgroup" as={Select} labelId="search-vgroup" label="Vgroup">
<MenuItem value="">
<em>None</em>
</MenuItem>
{
vgcounts.map((vgcount)=>{
return(
<MenuItem value={vgcount.vgroupname} key={vgcount.vgroupname}>
{`${vgcount.vgroupname} (${vgcount.count})`}
</MenuItem>
);
})
}
</Field>
</FormControl>
<Button type="submit" variant="contained" color="primary">Submit(Debug)</Button>
</Paper>
</Form>
)}
</Formik>
<h1>{JSON.stringify(vgcounts,null,4)}</h1>
<h1>{JSON.stringify(vtubers,null,4)}</h1>
</div>
);
}
export default Home
export const getServerSideProps:GetServerSideProps = async(ctx)=>{
//const vgcounts = await getVgroupCount();
const vgroup = getAsString(ctx.query.vgroup);
const [vgcounts, vtubers] = await Promise.all(
[
getVgroupCount(),
getVtubers(vgroup)
]
);
//return {props:{vgcounts}};
return { props:{vgcounts,vtubers}};
}
■Shallow Routingについて
router.pushの記述の「shallow」という項目について。
router.push(
{
pathname:'/home',
query:{...values,test:1} //付与するクエリストリング
}
, undefined
, {
shallow:false //trueだとgetServerSidePropsが動かないshallowになる
}
)
これをtrueにするとgetServerSidePropsやgetStaticPropsといったデータフェッチを動かさずに、URLを書き換えるようになる。
例えば、今回の例で「shallow:true」としてみる。
こんな感じでパス名とクエリパラメタの書き換えだけをしてstateを変更しないような挙動をさせるときにshallow routingを使う。
もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。