React Suspense と Hooks を同時に使う方法について
React Suspense とは
React には Suspense という機能があって、これは何かというと、あるコンポーネントの中で、Promise を throw する(!)と、throw した Promise が解決したときに、またレンダリングしに来てくれるというものです。その間は、親の Suspense の fallback で指定したコンポーネントがレンダリングを代理します。
const App: React.FC = () => {
// UserProfile 内で Promise が throw されたら、
// UserProfile の代わりに <div>Now Loading</div> が表示される
return (
<Suspense fallback={<div>Now Loading</div>}>
<UserProfile id={id}/>
</Suspense>
);
};
UserProfile の中身は、例えばこんな感じです。
let userProfile: string | null = null;
const UserProfile: React.FC<{id: number}> = ({id}) => {
if (userProfile == null) {
throw loadUserProfile(id).then(value => userProfile = value);
}
return <div>Loaded: {userProfile}</div>;
}
export default UserProfile;
「isLoading
みたいなフラグを使えば良くない?」みたいな話はありますが、Suspense のよいところは、throw を使うことで、大域脱出ができる点です。isLoading
のようなフラグを使う場合は、fallback するコンポーネントまで、バケツリレーなり、Context なりで状態を持ち運ばなければいけませんが、throw なら一発です。
Suspense と Hooks を同時に使う
さっきの例は、userProfile
が単一の状態しか持てず、非常に悪い例でした。しかし、throw する Promise は毎回同じものにしたいので、メモ化したいですね。なので、useMemo
を使って、 id 毎にメモ化するようにしましょう。
const UserProfile: React.FC<{ id: number }> = ({ id }) => {
const [userProfile, setUserProfile] = useState<string | null>(null);
const loadPromise = useMemo(() => loadUserProfile(id).then(setUserProfile), [id]);
if (userProfile == null) {
throw loadPromise;
}
return <div>Loaded: {userProfile}</div>;
}
export default UserProfile;
上手に書けましたね。イチコロです。偉大だ Hooks。ありがとう React 開発チーム。
と、言いたいところですが、上記のコードはなんと動きません!実際に動かしてみればわかりますが、永久にロードが終わりません。なぜでしょう?
throw された場合、Hooks の状態は破棄される
結論から言うと、↑の通りなのですが、コンポーネント内で throw が引き起こされた場合、Hooks の状態は破棄されます。useState
も useMemo
も毎回リセットされます。なので、毎回新しい Promise を生成してしまって、ローディングが終わらなかったんですね。useEffect も発火されません。
それでも、Hooks を使いたい!
useMemo
の内容が破棄されてしまうなら、自前でメモ化するようにすればよさそうですね。しかし、僕らは Hooks を使いたい!自前でメモ化などしたくない……!果たして、そんないい方法はあるのか……!
結論から言うと、コンポーネントを入れ子にすればいいです。
const UserProfile: React.FC<{ id: number }> = ({ id }) => {
const loadPromise = useMemo(() => loadUserProfile(id), [id]);
const getUserProfile = useGetPromiseValue(loadPromise);
return <Inner getUserProfile={getUserProfile}></Inner>
};
function useGetPromiseValue<T>(promise: Promise<T>): () => T {
return useMemo(() => {
const setValuePromise = promise.then(v => { value = v }).catch(e => { error = e });
let value: null | T = null;
let error: any = null;
return () => {
if (error != null) {
throw error;
}
if (value == null) {
throw setValuePromise;
}
return value;
}
}, [promise]);
}
const Inner: React.FC<{ getUserProfile: () => string }> = ({ getUserProfile }) => {
return <div>Loaded: {getUserProfile()}</div>;
};
useGetPromiseValue
は Promise をラップした関数を返し、呼び出しで値を読み出します。Promise が未解決の場合は、throw をします。重要なのは、この関数が、入れ子にしたコンポーネントの中で呼ばれていることです。親のコンポーネントは既に解決されているので、useMemo
の内容は保持されます。これで無事、Supense と Hooks を併用できましたね。
適当にラップして返せば、気分的にはただの値という世界観もある。
const UserProfile: React.FC<{ id: number }> = ({ id }) => {
const loadPromise = useMemo(() => loadUserProfile(id), [id]);
const userProfile = usePromiseElement(loadPromise);
return <div>Loaded: {userProfile}</div>;
};
type WrapperProp<T> = { read: () => T }
function Wrapper<T>(props: WrapperProp<T>): React.ReactElement {
return <Fragment>{props.read()}</Fragment>
}
function usePromiseElement<T>(promise: Promise<T>) {
return useMemo(() => {
const setValuePromise = promise.then(v => { value = v }).catch(e => { error = e });
let value: null | T = null;
let error: any = null;
const read = () => {
if (error != null) {
throw error;
}
if (value == null) {
throw setValuePromise;
}
return value;
}
return <Wrapper read={read}></Wrapper>
}, [promise]);
}
Author And Source
この問題について(React Suspense と Hooks を同時に使う方法について), 我々は、より多くの情報をここで見つけました https://qiita.com/hystking/items/296bfd233a06de794cc1著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .