oscklm
oscklm10mo ago

Awkward property access .default when using default export folder structure

Hey, I've come across a situation where I'm required to access the API object in a mutation using .default.
Relevant code:
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
↓↓↓↓↓
const storeUser = useMutation(api.users.store.default);
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
↓↓↓↓↓
const storeUser = useMutation(api.users.store.default);
In my store.ts file, I'm exporting the mutation as a default export. However, I find the usage of .default in the useMutation hook to be a bit awkward. I'm wondering if there's a way around this, or if there's a different recommended approach for accessing the API object in a mutation. Folder structure:
convex/
├── _generated/
│ ├── api.ts
│ └── dataModel.ts
├── users/
│ └── store.ts ← ← ← ←
convex/
├── _generated/
│ ├── api.ts
│ └── dataModel.ts
├── users/
│ └── store.ts ← ← ← ←
Just a mild annoyance, when trying to keep things neat.
6 Replies
Michal Srb
Michal Srb10mo ago
This is working as expected. If you want the mutation to have a reference api.users.store, you need to put the store mutation into the users.ts file.
oscklm
oscklmOP10mo ago
Yeah. That's actually usually how i had my mutations and queries structured, but I was mainly curious if there is a way around that, since i wanna keep my functions on a per file basis within a folder that is named after a given table. But im assuming this is just a limitation to how typescript works or something? There a few reasons why i prefer this approach: 1. I make objects containing validators for my invidual schemas, so when i build functions i can import that object and using a spread operator pick and choose the values i wanna use for the args. 2. It's a bit nicer as the project grows, to just have functions contained in their own files, specially for functions that are large. So i'm fine with the tradeoff being that i have to use the .default to grab the actual function off the api object. Example of what i mean:
import { mutation } from "../_generated/server";
import { gameValidators } from "../schema";

/**
* @name api.games.create
* @description Creates a new game.
*/

const { name, players } = gameValidators;

export default mutation({
args: { name, players },
handler: async (ctx, { name, players }) => {
ctx.db.insert("games", {
name,
players,
status: "waiting",
activeRound: null,
maxPlayers: 4,
});
},
});
import { mutation } from "../_generated/server";
import { gameValidators } from "../schema";

/**
* @name api.games.create
* @description Creates a new game.
*/

const { name, players } = gameValidators;

export default mutation({
args: { name, players },
handler: async (ctx, { name, players }) => {
ctx.db.insert("games", {
name,
players,
status: "waiting",
activeRound: null,
maxPlayers: 4,
});
},
});
Michal Srb
Michal Srb10mo ago
This is how the api object is set up (and we do need to disambiguate between api.users.store and api.users.store.default) You can also have a single named exported function in a file. Totally up to you.
oscklm
oscklmOP9mo ago
Yeah although that would result in api.users.store.store if im not misunderstanding, in the case i exported a single named function called store from the store.ts file that lives in my users folders (with all other related functions) Revisiting this. Is convex gonna allow for more control here at some point? I'd love to ask how you handle folder structure for a large project like the convex dashboard which i assume is using convex? We are trying to come up with a good solid, scalable folder structure. And keep hitting some awkward situation with how the api object gets generated, and how we wish to structure our backend. It's mostly an annoyance in how we end up having to access functions on the api object. If we choose to seperate our query / mutations functions into folders and or individual files. We end up having to access that with either .default if we default export it, or the name of a named export from that file. So say we have /convex/video/mutations/create.ts we would have to do: api.video.mutations.create.create or api.video.mutations.create.default It's kind of difficult finding a good folder structure for a large project, that plays well with how the api is generated imo
jamwt
jamwt9mo ago
we used to recommend default exports; these days I would say we pretty strongly recommend against it most teams settle on something like /convex/bar/baz.ts where baz.ts contains a bunch of queries/mutations around managing baz-es and then the calls are always api.bar.baz.getAll or whatever default exports end up problematic for a few reasons. (1) "default" is a kind of lame identifier and it takes up space without adding value; (2) normally, it ends up feeling more useful to put all the queries/mutations for a certain model together in the same file. that way they can share helper functions, type definitions, what-have-you actions are sometimes in this same file, if they're very specific to that model (table); but some teams keep actions somewhere, both b/c they're more likely to need to "use node";, and they often map workflows over many different models. so it doesn't feel right to attach them to one specific module with queries/mutations in your case, my general advice would be this api.video.mutations.create.create that should probably just be api.video.create and you keep queries/mutations about videos together
oscklm
oscklmOP9mo ago
Thanks a lot Jamie. So to confirm, splitting functions into its own files is not really supported, without some extra property being added to the dot notation, in order to index it on the api object?

Did you find this page helpful?