何がしたかったのか
Reactには、Lazy Componentというものがあります。
import React, { FC } from 'react';
const MyComponent: FC = () => (
<div>Hello LazyComponent!</div>
);
export default MyComponent;
import React, { FC, Suspense, lazy } from 'react';
const MyComponent = lazy(() => import('./MyComponent'));
const MyApp: FC = () => (
<div>
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</div>
);
export default MyApp;
とすると、MyComponentのロードが完了するまでfallbackに設定された<div>Loading...</div>
を代わりにレンダリングしてくれるというものです。
で、いろいろ調べてたらこんなこともできると判明。
LazyComponent.jsimport React, { Component } from 'react'; let result = null; const timeout = (msec) => new Promise(resolve => { setTimeout(resolve, msec) }); const LazyComponent = () => { if (result !== null) { return ( <div>{result}</div> ) } throw new Promise(async(resolve) => { await timeout(1000); result = 'Done' resolve(); }) }; export default LazyComponent;
こう書いたら、throwしたPromiseがresolveされたときにもう1回レンダリングされるらしく。私の探し方が悪いのか何なのか、この仕様はReactのドキュメント上で見つけることができませんでした。どこに書いてあるのか知っている人がいたらこっそり教えてほしいです。
それはさておきこの仕様、ドキュメントで見つからなかったので動かない前提で試しに書いてみました。
import React, { FC, lazy, Suspense } from 'react';
const PromiseTest= lazy(async () => {
let state = 0;
const TestInner: FC = () => {
if(state) {
return (
<div>Done! {state}</div>
)
}
throw new Promise((res) => {
setTimeout(() => {
state = 5;
res();
}, 5000);
});
};
return {
default: TestInner,
};
});
const TestApp: FC = () => {
return (
<div>
<Suspense fallback={<div>WAITING...</div>}>
<PromiseTest />
</Suspense>
</div>
);
}
やってみた結果……動く!動くぞ!
さて、問題のコードに移ろうじゃないか
さて、Promiseをthrowしたら期待通りに動くことが分かったんですけれど。state = 5
ってPromiseの中で変数に代入しちゃってるじゃないですか。ぶっちゃけキモいですよね。
useState
フックに置き換えてもいけるんじゃね?って思った私、置き換えてみました。
import React, { FC, lazy, Suspense, useState } from 'react';
const PromiseTest= lazy(async () => {
const TestInner: FC = () => {
const [state, setter] = useState(0);
if(state) {
return (
<div>Done! {state}</div>
)
}
throw new Promise((res) => {
setTimeout(() => {
setter(5);
res();
}, 5000);
});
};
return {
default: TestInner,
};
});
const TestApp: FC = () => {
return (
<div>
<Suspense fallback={<div>WAITING...</div>}>
<PromiseTest />
</Suspense>
</div>
);
}
あれ、動かん
動かんぞ。
useState
に置き換える前は動いたコードが、置き換えた瞬間動かなくなりました。てゆうか、setter
は普通に呼ばれているはずなのに、state
の値は0のまま。なんでや、、、。
諦めてComponent classにしてみた
というわけで、PromiseTest
の実装をComponent classに変えてみました。state
をthis.state.state
、setter
をthis.setState
に変えただけですけどね。
class TestInner extends React.Component<{}, { state: number }> {
constructor(props: {}) {
super(props);
this.state = {
state: 0,
};
}
render() {
if(this.state.state) {
return (
<li>Done! {this.state.state}</li>
);
}
throw new Promise((res) => {
setTimeout(() => {
console.log('resolved');
this.setState({ state: 5 });
res();
}, 5000);
});
}
}
const PromiseTest = lazy(async () => {
return {
default: TestInner,
};
});
const TestApp: FC = () => {
return (
<div className='board-list-container'>
<Suspense fallback={<div>WAITING...</div>}>
<PromiseTest />
</Suspense>
</div>
);
}
こうすると、動きました。動いてしまいました。え、なんでなんや、、、
……Function Componentが使えない極めてまれなケースの1つを発見した身としては、非常に頭が痛いです。こういう重要なことはもっとわかりやすくドキュメントに書いておいて下せぇ……。
結論
React、なんもわからん。