Part 8 – Factories
Mirage includes a Factory layer to help simplify the process of seeding your Mirage server with realistic, relational data.
Currently, if we want to make a Reminder, we need to do something like this in our seeds()
hook:
seeds(server) {
server.create("reminder", { text: "Walk the dog" });
}
Having to always specify every attribute for every model you create can add a lot of boilerplate to your code, especially during testing. It gets even more complicated when relationships are required for your data to be valid.
Factories are the perfect place to encode the constraints of your data, making it easier for you to quickly create valid graphs of data. Let's see how they work.
For this app, Reminders always have a text
property. Let's define a Reminder factory that encodes this:
import {
createServer,
Model,
hasMany,
belongsTo,
RestSerializer,
Factory,
} from "miragejs"
export default function () {
createServer({
models: {
list: Model.extend({
reminders: hasMany(),
}),
reminder: Model.extend({
list: belongsTo(),
}),
},
factories: {
reminder: Factory.extend({
text: "Reminder text",
}),
},
// ...rest of server
})
}
First we import Factory
, and then define a Reminder factory using the new factories
key of our server. We can use Factory.extend(config)
to pass in a config object, where the keys of the config object correspond to properties on our models.
Now we can update our seeds()
to just call server.create('reminder')
, without passing anything else in:
seeds(server) {
server.create("reminder");
server.create("reminder");
server.create("reminder");
}
With this seed data, the app now looks like this:
Valid, but not very realistic! We can use function properties to make this a bit more dynamic:
factories: {
reminder: Factory.extend({
text(i) {
return `Reminder ${i}`
}
}),
},
Now each reminder has its own unique text:
Libraries like faker.js also work well in function properties to create even higher fidelity data.
Combined with server.createList
, we can now easily create many Reminders using our Factory definition:
seeds(server) {
server.createList("reminder", 100);
}
This quickly gives us a big dataset:
But what if we want to override specific properties that we've defined on our Factory? We can always do that by passing attributes into server.create()
. These attributes will override anything defined on our base factories.
seeds(server) {
// Create a specific reminder
server.create('reminder', { text: 'Walk the dog' })
// Create 5 more generic reminders
server.createList("reminder", 5);
}
Here's the result:
Let's turn to our List model. We'll start by defining a Factory for it:
factories: {
list: Factory.extend({
name(i) {
return `List ${i}`;
},
}),
reminder: Factory.extend({
text(i) {
return `Reminder ${i}`;
},
}),
},
Now if we want to make generic Lists and Reminders, we don't need to specify any attributes:
seeds(server) {
server.create("list", {
reminders: server.createList("reminder", 5),
});
}
As you can see, passing Reminders into our List on creation still lets us create valid graphs of relational data:
What if we wanted to make it even easier to create a List with many reminders? We can use the afterCreate
hook on our List Factory, passing in our newly created list into any new Reminders we create:
factories: {
list: Factory.extend({
name(i) {
return `List ${i}`;
},
afterCreate(list, server) {
server.createList('reminder', 5, { list })
}
}),
reminder: Factory.extend({
text(i) {
return `Reminder ${i}`;
},
}),
},
Now with just a server.create('list')
, we can have a valid List with 5 Reminder models, like we had before:
seeds(server) {
server.create("list")
}
But what if we wanted to bring back some of our more curated, realistic data, either because we're testing something specific or because we want less generic data in development?
Copy this seeding logic in:
seeds(server) {
server.create("list", {
name: "Home",
reminders: [server.create("reminder", { text: "Do taxes" })],
});
server.create("list")
}
If we use this logic, we'll see that our afterCreate
hook is triggered for both lists, and now our Home list has 5 randomly generated reminders along our specific "Do taxes" one:
We can update our afterCreate
logic to first check if our newly created List already has Reminders passed into it, and only create default reminders if it doesn't:
factories: {
list: Factory.extend({
name(i) {
return `List ${i}`;
},
afterCreate(list, server) {
if (!list.reminders.length) {
server.createList('reminder', 5, { list })
}
}
}),
reminder: Factory.extend({
text(i) {
return `Reminder ${i}`;
},
}),
},
Now our same seeds logic lets us easily create a curated "Home" list with only the "Do taxes" reminder we passed in, while our plain call to server.create('list')
quickly scaffolds out a generic list with five reminders for us:
As one last refactoring step, there's an even better approach to only creating generic Reminders associated with our List when we want to: Factory Traits.
Traits let us group attributes and afterCreate
logic together under a name, so they're only invoked when we use that name. Let's add a withReminders
trait to our List factory.
First we'll import trait
:
import {
createServer,
Model,
hasMany,
belongsTo,
RestSerializer,
Factory,
trait,
} from "miragejs"
and then we'll move our afterCreate
logic to a withReminders
trait on our List Factory:
factories: {
list: Factory.extend({
name(i) {
return `List ${i}`;
},
withReminders: trait({
afterCreate(list, server) {
server.createList('reminder', 5, { list })
}
})
}),
reminder: Factory.extend({
text(i) {
return `Reminder ${i}`;
},
}),
},
Note that we removed the list.reminders.length
check since this logic will now only be invoked if we explicitly call the trait.
If you save and check your app, you'll see that our second generic List no longer has any associated reminders. Let's update our seeds
hook to invoke our new trait for that list:
seeds(server) {
server.create("list", {
name: "Home",
reminders: [server.create("reminder", { text: "Do taxes" })],
});
server.create("list", "withReminders")
}
And now our UI is right back to where it was! But we've improved our List Factory by making it less surprising – the base case only creates a single List model.
As we'll see in the next section, good Factory definitions are important for writing good tests.
Factories have even more features that give you flexible ways to create data scenarios, so you can do things like quickly switch your development environment from an anonymous user to an authenticated user, or set up just the data you need within a test.
Let's revert our seeds logic to our curated data set for now:
seeds(server) {
server.create("reminder", { text: "Walk the dog" });
server.create("reminder", { text: "Take out the trash" });
server.create("reminder", { text: "Work out" });
server.create("list", {
name: "Home",
reminders: [server.create("reminder", { text: "Do taxes" })],
});
server.create("list", {
name: "Work",
reminders: [server.create("reminder", { text: "Visit bank" })],
});
}
Using Factories well is one of the best ways to take advantage of all the capabilities of Mirage's data layer.
Takeaways
- Factories are like blueprints that help you easily create graphs of relational data
- You can use static values or functions as attributes
- You can override Factory defaults wherever you call
server.create()
- Use the
afterCreate
hook to perform additional logic, like automatically creating related data for a model - Use Traits to group related attribute and
afterCreate
logic, and to keep your factories composable