Reputation: 60987
I'm trying to understand why the following code is causing a infinite loop which makes the page completely unresponsive.
function lazy(importTaskFactory) {
const f = function (props) {
console.log("enter");
debugger;
const [value, setValue] = useState(null);
const memoizedImportTask = useMemo(importTaskFactory, [importTaskFactory]);
useEffect(() => {
console.log("useEffect");
debugger;
memoizedImportTask
.then(m => {
console.log("settled");
debugger;
setValue(m);
})
.catch(err => {
console.error(f.displayName, err);
});
}, [memoizedImportTask]);
if (value) {
if (!value.default) {
throw new Error(`${importTaskFactory.toString()} - default export is required`);
}
return React.createElement(value.default, props);
}
console.log("throw");
debugger;
throw memoizedImportTask; // Suspense API
console.log("throw memoizedImportTask!", memoizedImportTask);
return null;
};
f.displayName = `lazy(${importTaskFactory.toString()})`;
return f;
}
The only thing this code accomplishes is logging enter
, throw
, enter
, throw
, enter
, throw
, etc. the useEffect
hooks is never called and so the value is never set. This despite the promise has settled (resolved). Why is react in this particular case running this is some manner of a synchronous fashion?
Upvotes: 2
Views: 690
Reputation: 60987
I figure it out whiling formulating the question. While I love the React.js hook style of writing components they do exhibit behaviour that is somewhat different from class components.
If we skip the useEffect
hook and move everything into the useMemo
hook (so that we know for sure that we actually set the value when the promise settle). It doesn't actually do anything. This is because when a component throws, it's hooks are reset which makes the useState
hook useless! Also, a component that throws an error while rendering does not get its useEffect
hook called, probably for the same reason (the component never did commit).
So even if that approach got the promise to resolve, the value is lost between setting the value and throwing the promise. But we can work around these limitations by implementing the state and memoization ourselves and not throwing promises to begin with (more on this later)...
export interface LazyComponent<P = {}> {
getComponent(): React.ComponentType<P> | undefined
importComponent(): Promise<React.ComponentType<P>>
}
type LazilyInstantiatedComponentModule<P> = {
default: React.ComponentType<P>
}
export function lazy<P = {}>(
importTaskFactory: () => Promise<LazilyInstantiatedComponentModule<P>>,
fallback?: React.ReactElement
): React.FunctionComponent<P> & LazyComponent<P> {
if (importTaskFactory === undefined) {
throw new TypeError("importTaskFactory is undefined")
}
let cached: LazilyInstantiatedComponentModule<P> | undefined
let importTask: Promise<LazilyInstantiatedComponentModule<P>> | undefined
function importComponent() {
if (!importTask) {
importTask = importTaskFactory().then(m => {
if (!(typeof m === "object" && typeof m.default === "function")) {
throw new Error(
`${importTaskFactory.toString()} - missing default export`
)
}
return (cached = m)
})
}
return importTask
}
function lazy(props: P) {
const [value, setValue] = useState(cached)
useEffect(() => {
if (!value) {
importComponent().then(setValue)
}
return undefined
})
if (value) {
return React.createElement(value.default, props)
}
return fallback || null
}
lazy.getComponent = () => (cached ? cached.default : undefined)
lazy.importComponent = () => importComponent().then(m => m.default)
lazy.displayName = `lazy(${importTaskFactory.toString()})`
return lazy
}
This works because our lazy
loader is a higher order component. You might be wondering why implementing lazy
at all when there is React.lazy
is even necessary but the reason for this has to do with that fact that React.lazy
requires a Suspense
component which is not supported by ReactDOMServer
.
The answer given here reflects how I have worked around this limitation since React 15. The public APIs getComponent
and importComponent
can be used to ensure that the cached
value is set before you reach render. Thus avoiding any flickering (i.e. rendering a fallback or null
).
Upvotes: 2