Counter example part:
// ------ ------
// Model
// ------ ------
// `Model` describes our app state.
type Model = i32;
pub struct Model {
pub base_url: Url,
pub page: Page,
pub selected_seed_version: SeedVersion,
pub guide_list_visibility: Visibility,
pub menu_visibility: Visibility,
pub in_prerendering: bool,
pub guides: Vec<Guide>,
pub search_query: String,
pub matched_guides: Vec<Guide>,
pub mode: Mode,
}
Model
is the state (aka store, data, ..) of your application.
In our case it's only a type alias for i32 because we only need to track one value - the number of clicks.
It can be almost anything - type alias, enum, etc. However the most Model
s are structs in real-world apps. And Model
has to be static
(it basically means that you can't save the most references into it).
Model
It should be as simple as possible. If there is a way how to derive data from the current Model
instead of extending it (i.e. adding more fields), you should derive it, even at the cost of small reduction in performance.
Try to save only data into your Model
- i.e. add field students: Vec<Student>
instead of manager: SchoolManager
, where manager
contains Vec<Student>
and thousand other things. Exceptions are Seed-related items like handles and DOM elements (we will discuss them in other chapters).
Don't make your life unnecessary hard.
Your Model
should be the single source of truth - i.e. add one field selected_menu_item: MenuItemId
into your main Model
instead of creating many instances of the component MenuItem
, where each instance has own Model
with field selected: bool
.
Try to be as expressive as possible and make impossible business rules unrepresentable in code by encoding them with Rust type system.
Try to reduce the number of bool
s and Option
s to a minimum.
When you see multiple fields with the same simple type (bool
, String
, u32
, Option
, etc.) in your Model
, you should try to remodel it.
I recommend to read the book Domain Modeling Made Functional or at least The "Designing with types" series.
When you need to create custom types that are used in the Model
, write them below the Model
. (The rule "children below the parent" is valid for all nested structures.) Example:
// ------ ------
// Model
// ------ ------
struct Model {
a_field: AType,
}
// ------ ATYPE ------
enum AType {
AVariant
}
Imagine the code with this pattern:
ChildA
impls for ChildA
ChildB
ChildC
..
Parent
You don't know what children are interesting for you because you don't know how and where they are used until you see also the parent.
Human short-term memory can hold only cca 7 items - that means it's very easily overloaded by reading child definitions and as a result the reader will start to jump between children and the parent to empty space and decrease cognitive load.
You can improve DX by moving children below the parent to allow readers to filter interesting children.
Another reason is scanning - readers (especially advanced developers) scan the code and try to recognize familiar patterns or basic building blocks - then blocks like
// ------ ------
// xxxxx
// ------ ------
xxxxxx Model / init / .. {
effectively work as checkpoints for the eyes.
Default
and other standard traitsGenerally all implementations of standard traits (From
, Into
, Default
, Display
) are very useful if the item (struct
, enum
...) is used in multiple contexts or with multiple other items - then the generalization makes sense because it implies you are writing idiomatic Rust and it plays nicely with other standard traits and other items. However when you start to implement standard traits for many items, your code-base is slowly turning into the sea of .into()
, ::default()
, .to_string()
, etc.
As the result:
Default
trait: We assume xx::default()
calls are pretty cheap operations (see Default
for primitive types or Vec) - in the most cases there isn't even memory allocation on the heap and you probably won't find more expensive operations in Default
implementations for other items. So when you write more sophisticated Default
code for your item and somebody use this item in a nested structure, he will be very surprised once he writes some benchmarks.
One Seed user was even able to accidentally write recursive loop of nested complex Default
s that was causing stack overflow.
In Seed apps, you need to create Model
only once, so when you implement Default
for Model
:
init
function because some Model
parts will depend on Url
or on other values => worse readability and slower code. (You'll learn about init
and Url
in other chapters.)Model
is/can be created on multiple places.A standard Seed app usually contains one app (aka root or main) Model
, several page Model
s and a few component Model
s. It's often pretty clear where you should save data and it's simple enough to keep the most of your models in your head during development. So there usually aren't problems with data synchronization or introducing unnecessary models (aka local states).
However it's already possible to integrate Javascript Web Components (we plan to support also Rust Web Components) and React-like Hooks - Seed Hooks - are waiting for integration into Seed. It means it will be pretty easy to create local states (even implicitly when you are using a component library), so there are some tips how to define your Model
s:
Your business data should be kept in standard Seed Model
s - ideally in the app model or in page models.
Then you should use state hooks for pure GUI data - e.g. a state variable mouse_over
for button
component when you want only to switch the button's color on hover.
And you can use Web Components for complex GUI elements or when you often need to interact with JS library / component.
(Don't worry, you'll learn more about Web Components and pages in other chapters; or look at custom_elements and pages examples if you are brave enough.)