-
-
Notifications
You must be signed in to change notification settings - Fork 461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(fix) - fetching on initial render #248
Conversation
@JoviDeCroock Cheers for opening a PR! 🎉 We'll have to do some other fixes though but I think that could be solved in combination with #218. The reason is that we possibly want the initial state to match up with what the cache exchange gives us back synchronously. So depending on the cache it can also make sense to set |
@JoviDeCroock If you'd like to help out with that that'd be amazing! ❤️ I don't have much time this weekend myself but would otherwise take a look at that next week. Essentially we can use the fact that the exchange pipeline should run synchronously. So on an initial run of the hook we could try to get the initial state with So far so good I hope 😓 This would then allow me to add suspense support to the client later on, since we're running the initial state query synchronously, which means I can easily add some more code to throw a promise in the client. |
I find it hard to determine the fact that your client.execute would return synchronously isn't the point of that subscribe to not have to worry about that? Sorry if I'm totally missing the point. |
@JoviDeCroock It doesn't return anything synchronously, but if there isn't anything concurrent going on in the exchange chain it'll step through each operator synchronously. So the callback in |
Yes, I can understand that and then we aren't fetching but as far as I know we can't be aware of a sync return in the first render since the setting of the state happens asynchronously anyway. I'll try and think a bit more in depth about this, don't want to bother you about these things if you don't have a lot of time |
@JoviDeCroock It should in theory work, since we're only dealing with initial mount, i.e. in some pseudo-code-ish fashion const useQuery = (/* ... */) => {
const isInitialRender = useRef(true);
const initialState = useRef({ fetching: true });
const updateState = useRef(update => { initialState.current = update; });
const teardown = useRef(null);
if (isInitialRender.current) {
isInitialRender.current = false;
teardown.current = pipe(
client.executeQuery(/* ... */),
subscribe(result => updateState.current(result))
);
}
const [state, setState] = useState(initialState.current);
updateState.current = setState;
useEffect(() => {
// this is where the old updating logic goes with request.key diffing etc
}, [/* ... */]);
// ...
}; Can probably be simplified but I can totally see a way to make this work so that on initial mount we get the correct initial state 🤔 |
Implemented this for useQuery, will try to find a way to test this asap and extend it to Query component as well. |
src/hooks/useQuery.ts
Outdated
error: undefined, | ||
data: undefined, | ||
}); | ||
if (!isMounted.current) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the idea of reusing isMounted
for this 👌
I suppose we could also reuse executeQuery = useCallback( /* ... */
now?
Maybe it could check whether isMounted
is still false
to update initialState
? That would also mean that updateState
becomes unnecessary.
I'm also not sure how useState
works with Fiber but setState
on Component
was always batched, so it would only update state once. This even worked when the Component
was still mounting. At least during researching the current SSR implementation I found out that useState
still works similarly to that there. So if it also does in non-SSR then we could simply call setState
and not worry about initialState
either, since it might be batched into the first render
This would at least simplify the implementation since in summary we can:
- Reuse
executeQuery
below - Remove
updateState
- And remove
initialState
potentially
Edit: Btw thanks again for picking this up! Means a lot since this is one of the more important changes and it's always great to have more people helping out 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how we can reuse executeQuery since to use that we already need our setState defined, that useState inturns relies on initialState.current.
I did implement the second remove updateState
part, will ook more in depth later. Thanks for your excellent and through guidance. Amazing!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, true, reusing executeQuery
definitely hinges on whether useState
can again come before it and whether it's actually going to change the initialState
. At least in SSR this works because calling it triggers another "loop" through the component, so not technically a rerender but a rerun. Not sure if that's the case in normal, non-SSR React
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a few suggestions but if I'm not mistaken, the issue described leaves us only concerned about the initial state on mount.
It looks like we have two scenarios:
- Pause is false and the initial state is { fetching: true }
- Pause is true and the initial state is { fetching: false }
If that's the case, we can make this change without the need for refs or additional logic.
const [state, setState] = useState<UseQueryState<T>>({
fetching: args.pause !== true,
error: undefined,
data: undefined,
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other scenario is some preparation for suspense SSR so we'll also would want to keep the executeQuery excursion that runs on initial mount outside of useEffect
But depending on how React works the executeQuery useCallback could maybe be reused for that, simplifying this PR. We'll see how that pans out I guess. Might or might nor work 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSR
I'm out of here 👀
Are you saying that following SSR, the state and ref values are not synchronized from the backend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up to you what this PR becomes, my initial solution was to make the initial state based on pause. If the current wip helps out more for future work maybe we can make a seperate simple solution PR and keep this one for the experimental work.
@@ -33,19 +33,34 @@ export type UseQueryResponse<T> = [ | |||
export const useQuery = <T = any, V = object>( | |||
args: UseQueryArgs<V> | |||
): UseQueryResponse<T> => { | |||
const isMounted = useRef(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JoviDeCroock heads up, this is going to conflict with an existing pr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jup, saw that but in theory that invokes a will mount, no?
const request = useMemo( | ||
() => createRequest(args.query, args.variables as any), | ||
[args.query, args.variables] | ||
); | ||
|
||
const [state, setState] = useState<UseQueryState<T>>({ | ||
fetching: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we simplify this PR to only change the initial state?
This line being changed from fetching: false
to fetching: args.pause !== true
would set the initial state as fetching by default if pause is not specified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
args.pause !== true
is in theory not necessary anymore if we handle this correctly via executeQuery
being either active or not active after it's run.
I suppose this PR is still in a more "experimental" stage so maybe we can merge yours for now and if that leads to conflicts I can later resolve them 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
args.pause !== true is in theory not necessary anymore if we handle this correctly via executeQuery being either active or not active after it's run.
Adding this logic to executeQuery would not solve the problem of the initial rendering state though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Talking about pause this should probably also be accounted in first render trying to get from cache else we’d make that option obsolete for first call.
fetching: false, | ||
}; | ||
constructor(props) { | ||
super(props); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have further implemented the SSR approach but the sync would imply we execute our query here, this isn't possible since this.setState would result in a noop.
executeQuery: this.executeQuery, | ||
data: undefined, | ||
error: undefined, | ||
fetching: props.pause !== true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach was suggested but isn't good for SSR as far as I understood
@JoviDeCroock So I tested out my hypothesis and it works 🎉 |
@kitten Will do, thanks for the response I'll try and check your PR tonight. What code would you need there? Closing this PR so we can keep a good overview. Learned something new today thanks! |
@JoviDeCroock However you'd like to help 🙌 I also don't have tests for it yet... Mostly, I wouldn't have landed on that implementation this quickly without your help so at the very least I'll add you as a co-author to the merge commit. But let me know what you think about it and whether you think it'll cover everything we've talked about. That'd be super helpful. Cheers! ❤️ |
Fixes: #245
After the initial render the effect gets executed that sets fetching to
true
So when initially rendering we see that fetching is false implying we have data, since after this we'll always see fetching -> data fetching -> data
Only on initial render we see a scenario where: fetching false but no data -> fetching -> data
Made this as a draft because I understand the why but maybe we could resolve it differently with a loaded property or something?
Snapshot:
Fails atm due to the initialState change