それSWRじゃなくてgetServerSidePropsでいいよねっていう場面の話
Next.jsでフロントエンドを作っているプロジェクトにSWRを導入した際に「SWRかgetServerSidePropsのどちらでデータ取得をすればいいのか迷う」みたいな意見があったので、チームで方針を作ったときに考えたことの話。
フロントエンドの構成については、バックエンドのREST APIを叩きに行ってデータを取得・更新する至って普通の構成。ただユーザーの認証をする必要があり、sessionの有無を見つつページの出し分けとかAPIの取得をする形になっている。認証部分はAWSのCognitoでクライアントにはnext-authを使用した。
データ取得の方針
データ取得・更新について、基本的にSWRを使用する方針にした。SWRについては周知の通りキャッシュ機構で高速にデータの表示ができるので使わない手は無いと思う。
また、next-authのuseSession
を使ってsession(に含まれるtoken)の有無を検証してからAPIコールするように fetcher を実装したので、SWRと合わせるとクライアントサイドで完結する(getServerSideProps
にデータ取得中と後のコンポーネントの出し分けをグダグダ書かないで済む)のも良い。
じゃあ全部SWRでやろうね!となるのだが...
SWRが向いてない場面
以下のような認証後のページの出し分けはクライアントサイドで処理しないほうがいいので getServerSideProps
を使う。
- ユーザーがアプリの
/auth
からログイン、Cognitoのページにリダイレクトして認証 - アプリの
/auth
にリダイレクト- sessionがあるかつユーザーのプロフィールがあれば
/
に - sessionがあるかつプロフィールがなければ
/auth/signup
に
- sessionがあるかつユーザーのプロフィールがあれば
以上のフローの際に Cognito からのリダイレクトで/auth
を開き直すので、ページが再度読み込まれる。
ここで以下のようにSWRによってプロフィールを取得、有無を検証してリダイレクトを実装すると一瞬 /auth
ページが見えたり、profileがまだ取得できずrevalidateを待つ前にリダイレクトが走って予期せぬUIを表示したりする。
// 🙅♂️
const AuthPage: NextPage = () => {
const { data: session } = useSession();
const router = useRouter();
const { data: profile, error: profileError } = useSWR(['process.env.ENDPOINT_URL' + '/users/profile', session?.accessToken], fetcher);
useEffect(() => {
if (!profile) router.push('/auth/signup');
if (session && profile) router.push('/')
}, [session, profile, router]);
// ~~~
}
そこで以下のように getServerSideProps
でリダイレクトするようにする。またredirectの分岐を増やすのが面倒だったので、Cognitoページからのredirect先は /auth/signup
に変更している。
// 🙆♂️
const Signup: NextPage = () => {
return <SignupPage />;
};
export default Signup;
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context);
if (!session) {
return {
props: {},
};
}
const profile = await fetchProfile(
process.env.ENDPOINT_URL + '/users/profile',
session.accessToken
);
// プロフィールがある場合は / へ
if (profile.status === 200) {
return {
redirect: {
permanent: false,
destination: '/',
},
};
}
return {
props: {},
};
};
以上まとめると全部SWRでとりあえず書くのではなく、リダイレクトなどHTMLが表示されてしまうとまずい場合のデータ取得はgetServerSideProps
、表示された後の取得はSWRでするように使い分けようねという方針を立てておくと迷わないで進められる話。
おまけ - fetcher の設計
データ取得時のエラーと、取得したデータの内容がエラーなのかで切り分けたいのでこういう設計にした。
レスポンスが snake_case で返ってくるので、camelcase-keysとtype-festで値と型をパースしている。
export type ReturnFetchProfileType = {
body: {
id: number;
cognito_user_id: string;
user_name: string;
age: number;
gender: string;
email: string;
created_at: string;
updated_at: string;
profile_image: string;
profile_description: string;
following: number;
followers: number;
};
} & Status;
export const fetchProfile = async (
url: string,
jwtToken?: string
): Promise<CamelCasedPropertiesDeep<ReturnFetchProfileType>> => {
if (!jwtToken) throw new NoJwtTokenError();
const res = await getFetcher({ url, jwtToken }).catch((err) => {
throw new ReadSchemaError(err);
});
const json = await res.json();
return Promise.resolve({ status: res.status, body: camelcaseKeys(json) });
};
// getFetcher.ts
type GetFetcherParams = {
url: string;
jwtToken: string;
};
const getFetcher = ({ url, jwtToken }: GetFetcherParams) => {
return fetch(url, {
headers: {
Authentication: jwtToken,
},
});
};
useSWR
で呼び出すと data
に status
が入るので返ってきたデータが正常か異常か判断が出来る。
また、エラーも種類によって Error
クラスをextendsしたものにしておくと便利。
export class ReadSchemaError extends Error {
public info: ExtendableObject;
public status: number;
constructor(...params: ErrorParamsTuple) {
super(...params);
this.info = {};
this.status = 0;
}
}
export class NoJwtTokenError extends Error {
public info: ExtendableObject;
public status: number;
constructor(...params: ErrorParamsTuple) {
super(...params);
this.info = { message: 'No jwt token.' };
this.status = 0;
}
}
Discussion