useResource may go into an infinite render loop if given a new promise on every render.
evert opened this issue · 1 comments
useResource can be called in 3 main ways:
useResource('/foo'); // with a string
useResource(someResource); // Passing a Resource object
useResource((async() => someResource)()); // Passing a Promise<Resource>
One of the advantages of passing promises, is that in theory it lets you do something like this:
const { data, error, loading } = useResource(
myBookResource.follow('author')
);
Unfortunately, useResource needs to have a way to detect that a different argument was passed for different renders, in case there are subsequent re-renders pointing to different endpoints:
const [url, setUrl] = useState('/video/1');
useResource('/video/1');
useEffect(() => {
setUrl('/video/2');
}, []);
This works fine for the string
and Resource
case, but it doesn't behave well for the Promise<Resource>
case.
As a result, doing:
const { data, error, loading } = useResource(
myBookResource.follow('author')
);
Will cause useResource to re-render continuously. The core reason is that myBookResource.follow('author')
returns a new promise every time it's called.
I currently have no idea yet on how we can solve this, but here's a few half-baked solutions:
- If the argument to useResource changed, maybe we don't go back to a
loading
state. We only re-render after the promise resolved. This might be surprising because it would mean that users will see the previous resource state until the new one has loaded. - Let users pass their own
dependency array
that we can use. Not very elegant but it could work.
I wish I had a better answer to this, but right now I don't.
Workaround
The best workaround currently is to use 2 components instead of 1.
Broken Example:
function MyComponent() {
const {loading, error, data} = useResource(blogResource.follow('author'));
if (loading) {
return <div>loading</div>;
}
return <div>{data.name}</div>;
Workaround
function MyComponent() {
return <ChildComponent resource={blogResource.follow('author')}/>;
}
type props: {
resource: Promise<Resource>;
}
function ChildComponent(props: Props) {
const {loading, error, data} = useResource(props.resource);
if (loading) {
return <div>loading</div>;
}
return <div>{data.name}</div>;
}