Skip to content

Conversation

david-crespo
Copy link
Collaborator

@david-crespo david-crespo commented Jul 29, 2022

This started out being about making error handling easier, but now that I figured out how to call prefetchQuery in a loader, I'm going around adding loaders to most pages so there is no visible pop-in when the API call is not instantaneous.

  • requireParams, a version of useRequiredParams that works in loaders, where we get params as an arg instead of calling useParams
    • useRequiredParams now has a one-line definition because all the logic is in requireParams
  • Move queryClient into @oxide/api so we can
    • Define a typed queryClient.fetchQuery with an API like useApiQuery
    • Access it outside the component tree in loaders by importing it directly
  • Use it to fetch the VPC for the VPC detail page

What loaders do for page transitions

Both of these are with a 1s sleep in the VPC endpoint handler to simulate a slow response.

Before: render, then fetch

2022-07-30-without-loader

After: fetch first

React Router holds off on the route transition until the loader is done. Much more like a traditional web app despite still being all client-side.

2022-07-30-with-loader

@vercel
Copy link

vercel bot commented Jul 29, 2022

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated
console-ui-storybook ✅ Ready (Inspect) Visit Preview Aug 4, 2022 at 4:57AM (UTC)

const { data: vpc } = useApiQuery('vpcView', vpcParams)
// could do `as Vpc`, but this keeps us more honest until they get Remix's
// loader type inference into the router
const vpc = useLoaderData() as Awaited<ReturnType<typeof loader>>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting that this ends up being a one-line change in the component but the behavior is quite different:

  • This can never fail to be present because this component will only render if the loader is successful
    • Hence deleting the ? below
  • The fetch has to be done before the page will render, so we don't see the layout first and then the page contents

export const requireInstanceParams = requireParams('orgName', 'projectName', 'instanceName')
export const requireVpcParams = requireParams('orgName', 'projectName', 'vpcName')
export const requireProjectParams = requireParams('orgName', 'projectName')

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could live without the helpers but they're kind of nice?

Copy link
Contributor

@zephraph zephraph Aug 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think they're good. Shortens the params and makes them a bit explicit, so I'm good with it.


export const Router = () => (
<DataBrowserRouter fallbackElement={<span>loading</span>}>
<DataBrowserRouter fallbackElement={null}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the null is what makes it wait before transitioning? Would the loading indicator be provided here somewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the null is just telling it to show a blank screen while loading instead of flashing the word "loading". This is where you would drop a full-page loading state. I think a more integrated loading state like the little 1-2px bar across the top would have to be built right into the layout component using useNavigation().state to tell when you're in a loading state.

@david-crespo david-crespo changed the title Try using RR loaders as a basis for better error handling Using RR loaders in more places for clean page transitions Aug 3, 2022
loaders 4 all! you get a loader! you get a loader!
@david-crespo david-crespo changed the title Using RR loaders in more places for clean page transitions Using RR loaders all over for clean page transitions Aug 4, 2022
const colHelper = createColumnHelper<UserRow>()

export const OrgAccessPage = () => {
export function OrgAccessPage() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed all of these to use function because of hoisting — it lets me define OrgAccessPage.loader above rather than below

// used in useUserAccessRows to resolve user names
apiQueryClient.prefetchQuery('userList', { limit: 200 }),
])
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this Promise.all is nifty. so easy. I heard you like parallel loading so I put parallel loading in your parallel loading

const alreadyExistsBody = { error_code: 'ObjectAlreadyExists' } as const
type AlreadyExists = typeof alreadyExistsBody
const alreadyExistsErr = json(alreadyExistsBody, 400)
const alreadyExistsErr = json(alreadyExistsBody, { status: 400 })
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the noise in this file, you can basically ignore it. I was using the delay option here a lot for testing so I made it easier to use

): ResponseTransformer<B> {
const { status = 200, delay = 0 } = options
return compose(context.status(status), context.json(body), context.delay(delay))
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use an object instead of positional args for the options so you don't have to pass in a dummy status in order to pass a delay

options
)

export const wrapQueryClient = <A extends ApiClient>(api: A, queryClient: QueryClient) => ({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch, prefetch, and refetch needed both api and queryClient here

}
<A extends ApiClient>(api: A) =>
() =>
wrapQueryClient(api, useQueryClient())
Copy link
Collaborator Author

@david-crespo david-crespo Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh we don't even really need this anymore since we can just import the right queryClient directly. this one just makes it so you are pulling it off of context from the QueryClientProvider. Not sure what the point of that is. Maybe if you had different query clients for different sections of the route tree and you wanted the component to transparently pull the right one, but my god why would you do that?


// to be used in loaders, which are outside the component tree and therefore
// don't have access to context
export const apiQueryClient = wrapQueryClient(api.methods, queryClient)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is important, we're using this for all the prefetch calls

+ }, []);
return null;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pulling the fix from this PR. I will remove the patch as soon as they release this (should be this week)
remix-run/react-router#9124

limit: 10,
})
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all extremely straightforward. The big problem I see is making sure you are actually making the right request with the right params. I left the wrong method in a few times and it wasn't always easy to notice. Params are even worse — the limit: 10 I stuck everywhere is to match QueryTable but it's way too easy to mess up. We can think about it.

@david-crespo david-crespo marked this pull request as ready for review August 4, 2022 05:06
@david-crespo
Copy link
Collaborator Author

I started writing a doc for the docs dir describing the data fetching architecture overall, but if I'm gonna write it out (and I already wrote like 3/4 of it) then it might as well be an RFD? Console internals are underrepresented in the RFDs.

https://github.com/oxidecomputer/console/blob/data-arch-doc/docs/data-fetching.md

@david-crespo david-crespo merged commit 57fda45 into main Aug 4, 2022
@david-crespo david-crespo deleted the try-loaders branch August 4, 2022 15:38
@zephraph
Copy link
Contributor

zephraph commented Aug 4, 2022

@david-crespo on the RFD note... I'm uncertain. Does it require a discussion? It feels like internal documentation might be the better way to tackle it. If we were outlining general principles or something that could always be an RFD, but this seems like it'd be more cost than gain.

@david-crespo
Copy link
Collaborator Author

It does not require discussion. L'état, c'est moi.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants