Closed15
queryParamsと連動するTabがほしい
ピン留めされたアイテム
全体像
TabWithRouter.tsx
"use client";
import { Route } from "next";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { ComponentPropsWithoutRef, ElementRef, FC, forwardRef } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { TabItem } from "./type";
type Props = {
tabItems: TabItem[];
param?: string;
};
export const TabWithRouter: FC<Props> = ({ tabItems, param = "tab" }) => {
const searchParams = useSearchParams();
if (!tabItems[0]) return <></>;
const tabParam = searchParams.get(param) ?? tabItems[0].value;
return (
<Tabs value={tabParam}>
<TabsList>
{tabItems.map(({ value, label }) => (
<TabsTriggerWithRouter value={value} key={value} param={param}>
{label}
</TabsTriggerWithRouter>
))}
</TabsList>
<>
{tabItems.map(({ value, content }) => (
<TabsContent value={value} key={value}>
{content}
</TabsContent>
))}
</>
</Tabs>
);
};
const TabsTriggerWithRouter = forwardRef<
ElementRef<typeof TabsTrigger>,
ComponentPropsWithoutRef<typeof TabsTrigger> & { param?: string }
>(({ param = "tab", ...props }, ref) => {
const router = useRouter();
// Bug: pathname should be Route when experimental.typedRoutes === true
const pathname = usePathname() as Route;
const onClick = () => router.push(`${pathname}?${param}=${props.value}`);
return <TabsTrigger ref={ref} {...props} onClick={onClick} />;
});
TabsTriggerWithRouter.displayName = "TabsTriggerWithRouter";
types.ts
import { ReactNode } from "react";
export type TabItem = {
value: string;
label: string;
content: ReactNode;
};
Tabs
にonValueChange
というメソッドがあったのでこっちを使うほうが良いと思った
"use client";
import { Route } from "next";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { ComponentPropsWithoutRef, ElementRef, FC, forwardRef } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { TabItem } from "./type";
type Props = {
tabItems: TabItem[];
param?: string;
};
export const TabWithRouter: FC<Props> = ({ tabItems, param = "tab" }) => {
if (!tabItems[0]) return <></>;
const defaultValue = tabItems[0].value;
return (
<TabsWithRouter defaultValue={defaultValue} param={param}>
<TabsList>
{tabItems.map(({ value, label }) => (
<TabsTrigger value={value} key={value}>
{label}
</TabsTrigger>
))}
</TabsList>
<>
{tabItems.map(({ value, content }) => (
<TabsContent value={value} key={value}>
{content}
</TabsContent>
))}
</>
</TabsWithRouter>
);
};
const TabsWithRouter = forwardRef<
ElementRef<typeof Tabs>,
ComponentPropsWithoutRef<typeof Tabs> & {
param?: string;
defaultValue: string;
}
>(({ param = "tab", defaultValue, ...props }, ref) => {
const router = useRouter();
const searchParams = useSearchParams();
// Bug: pathname should be Route when experimental.typedRoutes === true
const pathname = usePathname() as Route;
const tabValue = searchParams.get(param) ?? defaultValue;
const onValueChange = (value: string) =>
router.push(`${pathname}?${param}=${value}`);
return (
<Tabs ref={ref} value={tabValue} onValueChange={onValueChange} {...props} />
);
});
TabsWithRouter.displayName = "TabsWithRouter";
defaultValueを強制する必要があるので、型を上書きしている
これでqueryParamsと連動するTabをつくる
Usageを見ると、TabsTriggerとTabsContentがvalueで連動していることが分かる
<Tabs defaultValue="account" className="w-[400px]">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Make changes to your account here.</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
</Tabs>
Accountをクリックしたら、例えば?tab=account
になってほしい。tab
のところは選べるとなおよい。
一旦、機能は置いておいて抽象化する
"use client";
import { FC, ReactNode, useMemo } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
type TabsTriggerItem = {
value: string;
label: string;
};
type TabsContentItem = {
value: string;
content: ReactNode;
};
type TabItem = TabsTriggerItem & TabsContentItem;
const unpackTabItems = (tabItems: TabItem[]) => {
const tab: { triggers: TabsTriggerItem[]; contents: TabsContentItem[] } = {
triggers: [],
contents: [],
};
tabItems.forEach(({ value, label, content }) => {
tab.triggers = [...tab.triggers, { value, label }];
tab.contents = [...tab.contents, { value, content }];
});
return tab;
};
type Props = {
tabItems: TabItem[];
};
export const TabWithRouter: FC<Props> = ({ tabItems }) => {
const { triggers, contents } = useMemo(
() => unpackTabItems(tabItems),
[tabItems]
);
return (
<Tabs>
<TabsList>
{triggers.map(({ value, label }) => (
<TabsTrigger value={value} key={value}>
{label}
</TabsTrigger>
))}
</TabsList>
<>
{contents.map(({ value, content }) => (
<TabsContent value={value} key={value}>
{content}
</TabsContent>
))}
</>
</Tabs>
);
};
unpackTabItemsは余計だったかもな、計算の回数が増えている
"use client";
import { FC, ReactNode } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
type TabsTriggerItem = {
value: string;
label: string;
};
type TabsContentItem = {
value: string;
content: ReactNode;
};
type TabItem = TabsTriggerItem & TabsContentItem;
type Props = {
tabItems: TabItem[];
};
export const TabWithRouter: FC<Props> = ({ tabItems }) => {
return (
<Tabs>
<TabsList>
{tabItems.map(({ value, label }) => (
<TabsTrigger value={value} key={value}>
{label}
</TabsTrigger>
))}
</TabsList>
<>
{tabItems.map(({ value, content }) => (
<TabsContent value={value} key={value}>
{content}
</TabsContent>
))}
</>
</Tabs>
);
};
TabsTriggerのonClickでrouterが変わってくれれば良い
TabsTriggerだけ切り出せばいいか
これでいいのかな?
const TabsTriggerWithRouter = forwardRef<
ElementRef<typeof TabsTrigger>,
ComponentPropsWithoutRef<typeof TabsTrigger> & { param?: string }
>(({ param = "tab", ...props }, ref) => {
const router = useRouter();
// Bug: pathname should be Route when experimental.typedRoutes === true
const pathname = usePathname() as Route;
const onClick = () => router.push(`${pathname}?${param}=${props.value}`);
return <TabsTrigger ref={ref} {...props} onClick={onClick} />;
});
TabsTriggerWithRouter.displayName = "TabsTriggerWithRouter";
一応tabを押せばrouterが変わる。
でもTabs自体はURLを見ていないのでリロードしたらだめになる。
Tabsのvalueに値を渡したい
useSearchParams()
で渡してみる。tabの値がないときは最初のやつってことで。
export const TabWithRouter: FC<Props> = ({ tabItems }) => {
const searchParams = useSearchParams();
if (!tabItems[0]) return <></>;
const tab = searchParams.get("tab") ?? tabItems[0].value;
return (
<Tabs value={tab}>
<TabsList>
{tabItems.map(({ value, label }) => (
<TabsTriggerWithRouter value={value} key={value}>
{label}
</TabsTriggerWithRouter>
))}
</TabsList>
<>
{tabItems.map(({ value, content }) => (
<TabsContent value={value} key={value}>
{content}
</TabsContent>
))}
</>
</Tabs>
);
};
いい感じに動いている
外側からparamを指定できるようにした
export const TabWithRouter: FC<Props> = ({ tabItems, param = "tab" }) => {
const searchParams = useSearchParams();
if (!tabItems[0]) return <></>;
const tabParam = searchParams.get(param) ?? tabItems[0].value;
return (
<Tabs value={tabParam}>
<TabsList>
{tabItems.map(({ value, label }) => (
<TabsTriggerWithRouter value={value} key={value} param={param}>
{label}
</TabsTriggerWithRouter>
))}
</TabsList>
<>
{tabItems.map(({ value, content }) => (
<TabsContent value={value} key={value}>
{content}
</TabsContent>
))}
</>
</Tabs>
);
};
このスクラップは2023/06/11にクローズされました