erquhart
erquhart•5d ago

Convex Actions Limitations and Use Cases

This is correct, you generally don't want to call actions direct from your app. Look for "Architecture" > "Don't misuse actions" section in Zen of Convex: https://docs.convex.dev/understanding/zen
18 Replies
Cole
Cole•5d ago
t To continue on this, let's say I have several different data sources, some can be queried with convex queries, and some rely on actions, how could I create a unified hook such as useData(source: Source) {} that either returns useAction or useQuery?
erquhart
erquhartOP•5d ago
Honestly, I'd recommend against that in general. useQuery is sync data, actions aren't In a DAL use case you could use useAction, but there's typically some level of "did it work" that you want to capture. If it's truly a one off fetch and you don't need success guarantees or anything then useAction works as @Clever Tagline stated.
Cole
Cole•5d ago
You can still handle that though in a useEffect? Or I guess use() now
Clever Tagline
Clever Tagline•5d ago
In the situations where I'm using useAction, it's inside a custom hook, and I'm tracking (and returning) the success of the request in addition to the collected data.
Cole
Cole•5d ago
That's exactly what I need and what I've been doing But cases where I need a different action depending on a condition get tricky technically you'd have to do something like this ?
/**
* Custom hook that provides all data source actions
* Since React hooks can't be called conditionally, we need to call all of them upfront
*/
export function useDataSourceActions(): Readonly<
Record<string, DataSourceAction>
> {
// Call all hooks unconditionally to satisfy React's rules
const getServiceNowIncidents = useAction(api.servicenow.incidents);
const getServiceNowIncidentCount = useAction(api.servicenow.incidents);
const getGuruculAlerts = useAction(api.gurucul.alerts);
const getGuruculStatistics = useAction(api.gurucul.statistics);

// Memoize the return object to prevent infinite rerenders
return useMemo(
() => ({
servicenow_incidents:
getServiceNowIncidents as unknown as DataSourceAction,
servicenow_incident_count:
getServiceNowIncidentCount as unknown as DataSourceAction,
// Add new data source actions here as they are created
gurucul_alerts: getGuruculAlerts as unknown as DataSourceAction,
gurucul_anomaly_statistics: getGuruculStatistics as unknown as DataSourceAction,
}),
[getServiceNowIncidents, getServiceNowIncidentCount, getGuruculAlerts, getGuruculStatistics]
);
}
/**
* Custom hook that provides all data source actions
* Since React hooks can't be called conditionally, we need to call all of them upfront
*/
export function useDataSourceActions(): Readonly<
Record<string, DataSourceAction>
> {
// Call all hooks unconditionally to satisfy React's rules
const getServiceNowIncidents = useAction(api.servicenow.incidents);
const getServiceNowIncidentCount = useAction(api.servicenow.incidents);
const getGuruculAlerts = useAction(api.gurucul.alerts);
const getGuruculStatistics = useAction(api.gurucul.statistics);

// Memoize the return object to prevent infinite rerenders
return useMemo(
() => ({
servicenow_incidents:
getServiceNowIncidents as unknown as DataSourceAction,
servicenow_incident_count:
getServiceNowIncidentCount as unknown as DataSourceAction,
// Add new data source actions here as they are created
gurucul_alerts: getGuruculAlerts as unknown as DataSourceAction,
gurucul_anomaly_statistics: getGuruculStatistics as unknown as DataSourceAction,
}),
[getServiceNowIncidents, getServiceNowIncidentCount, getGuruculAlerts, getGuruculStatistics]
);
}
Which seems really bad to be fair
Clever Tagline
Clever Tagline•5d ago
I recommend splitting each service's data into its own hook instead of packing them together in one. If ServiceNow succeeds and Gurucul fails, it would feel messy (to me) to have to deal with both results coming from one hook.
Cole
Cole•5d ago
so something like use(DataSource.hook)
erquhart
erquhartOP•5d ago
I haven't even looked at use() 😭
Clever Tagline
Clever Tagline•5d ago
Same I'd just do useServiceNow() and useGurucul()
Cole
Cole•5d ago
It needs to be dynamic is the thing though
Clever Tagline
Clever Tagline•5d ago
I also don't use server-side anything
Cole
Cole•5d ago
It's based on user generated reports that define which data source the component should query from if i do it like that then i'd run into the conditional hooks problem again
Clever Tagline
Clever Tagline•5d ago
Not necessarily. From the sound of it, you're using this one mega-hook in a single component. I'd split up the component structure so that you can use separate hooks for each service, and each is only used in its own related component, using the conditional side of things to determine which component renders.
Cole
Cole•5d ago
it's a many -> many? relationship tho a single component can be used for different data sources
Clever Tagline
Clever Tagline•5d ago
Hmm...it still feels like there could be some refactoring done to avoid that. If I'm understanding the setup correctly, this single component renders the results of whichever data source is selected. If that's the case, my first thought would be to still make separate components for each service to collect the data, then each of those would pass the collected data to a single component to display the data. Separating collection from display would allow you to create as many collection components as you need, while having a single component for display. Something like this (very rough):
// Some component conditionally renders one of these two

function DisplayServiceNow() {
const data = useServiceNow()
return <DisplayGenericData data={data} />
}

function DisplayGurucul() {
const data = useGurucul()
return <DisplayGenericData data={data} />
}

// No matter which one is rendered, the data all goes here for display

function DisplayGenericData({ data }) {
// render the data as needed
}
// Some component conditionally renders one of these two

function DisplayServiceNow() {
const data = useServiceNow()
return <DisplayGenericData data={data} />
}

function DisplayGurucul() {
const data = useGurucul()
return <DisplayGenericData data={data} />
}

// No matter which one is rendered, the data all goes here for display

function DisplayGenericData({ data }) {
// render the data as needed
}
Does that make sense? Would that work in your use case?
Cole
Cole•5d ago
That makes perfect sense
Clever Tagline
Clever Tagline•5d ago
The trick here is formatting the data so that no matter which service you collect from, the output is consistent so that the display component can render it. (BTW, I have to get back to work, so I won't be able to jump back to this for a bit)
Cole
Cole•5d ago
No worries, you've been a huge help, thanks
const dataSource = DataSourceRegistry.get(source);

const { data: queryData } = dataSource?.hook?.() ?? { data: null };
const dataSource = DataSourceRegistry.get(source);

const { data: queryData } = dataSource?.hook?.() ?? { data: null };
@Clever Tagline easy fix

Did you find this page helpful?