つい先日、今まで一年ほどお世話になったプロジェクトから別のプロジェクトに移りました。社内での小さな異動です。
元のプロジェクトは、初期段階は別会社の方々(経験豊富な方々でした)が立ち上げをしてくださり、そのプロトタイプは新しい機能の追加・修正などをしていく形でした。
そしていざ新しいプロジェクトへ移ったのですが、すごく開発しづらいのです。開発が楽しいとおもっていたことが一転し、非常に触りづらく、少し苦痛と感ています。
そのプロジェクトが悪いというわけではなく、そちらも限られた期限の中何とか形にしてくれたものであり、現時点での姿が現時点でのベストではあります。
しかし今後メインで保守を任された身として、そのままというわけにはいかないなあと少しずつ解決していこうと思っています。
その中でやることを、記事にしていこうと思っています。今回はその1つめです。
課題①:認可処理の散らばり & 度重なる user fetch
このプロジェクトは社内の業務アプリケーションに分類され、そもそもログインした人向けにしかページは表示されず、またログインページもない(ssoのurlを構築し、情報がなければリダイレクト)というものでした。
ですのでアプリケーション全体として認可は必要だし、user情報もその時点で取得しておけば良さそうな気がします。
しかし実際は、認可を請け負うhookがそれぞれのページの上部で呼び出され、user情報は必要なところで毎度fetchをする、という構造でした。
間違いではないでしょうが、ベストではないはずです。その根拠の一つとして、左側に一覧がありクリックすると画面右側に詳細が表示されるというページがあるのですが、一覧の項目をクリックすると毎回user fetchが起こるので、たまにuiが追いついてこない(suspenceでのラップもされていないので、前の情報が少しの間表示されたまま)という問題がありました。
また、僕に振られたタスクもuser情報を利用するものであったため、同じように実装していくのも厄介だという感じでした。(後々修正することになる + user情報使いたいだけなのに、そのコンポーネントで管理するstateを増やすのも微妙な気がする、など)
そこでauthを大元のページで処理し、contextにuser情報を保持しておこうと考えました。
改善方法
先述の通り、AuthProviderを作成し、Layoutページのchildrenをラップすることでアプリケーション全体としてそもそもログイン状態でなければ利用できないとし、またuser情報を取得し保存しておくことで、逐一fetchをかけなくてもcurrentUser
などで取り出せるように変更しました。
ざっくり以下です。諸々省略していますので動きはしません。
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
type AuthContextType = {
currentUser: user | undefined
isAuthenticated: boolean
logout: () => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
type AuthProviderProps = {
children: React.ReactNode
}
export function AuthProvider(props: AuthProviderProps) {
const [currentUser, setCurrentUser] = useState<User | undefined>(undefined)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const router = useRouter()
const fetchCurrentUser = async () => {
try {
const token = localStorage.getItem('token')
if (token) {
router.push(<sso - urlにリダイレクト>)
return
}
const userInfo = await <userのゲット>
setCurrentUser(userInfo.data)
setIsAuthenticated(!!userInfo)
} catch (error) {
<sso - urlにリダイレクト>
}
}
useEffect(() => {
fetchCurrentEmployee()
}, [router])
const logout = () => {
<sessionの削除的な処理>
}
return (
<AuthContext.Provider value={{ currentEmployee, isAuthenticated, logout }}>{props.children}</AuthContext.Provider>
)
}
// 画面で上記情報を利用する際、以下hookを利用。
export const useAuth = () => {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('えらー!')
}
return context
}
こうすることで、画面で以下を記載すればログインユーザーの情報を利用できます。
再度fetchもかかりません。
const { currentUser } = userAuth()
余談ですが、それまでは各所で必要とあらば以下のように呼び出していました。
※以下は省略した方で、エラー処理などは元々の認証機構と同じものが記載されているなど、どこで管理しているのか不明瞭でした。
const fetchCurrentUserData = async () => {
try {
const currentUser = await <userの取得>
setCurrentUser(currentUser.data)
} catch (error: any) {
<sso - redirect>
}
}
// useEffectで呼び出し
うーん、これがベストなのかは正直経験も少なく自信はないです。しかし、今までの各所での呼び出しよりはだいぶ扱いやすくなったと思います。
あとは、今までの開発陣にどう伝えるか、です。
うまくいくと良いです。