Core
In practice, Duck is based on two things,
- Stores
- Components
In a nutshell, stores are the logical/stateful part of your app and should be developed first.
Components are the visual representation of your app state and should be developed after.
When used together, stores and components provide a simple yet scalable means of developing reactive applications 👍.
You can organize your stores and components however you'd like - it's up to you and your unique application.
Simple applications may have a single store and just a few components.
Below is an architectural diagram of what a Duck app with a main store, a secondary store, a main component, and several additional components may be designed like,
To provide an example, the above architecture could be a chat app with a login page followed by another page with the chat itself. You could use one store for user data (perhaps a list of friends) and another store for the actual chat data (a list of messages).
Before diving deeper into stores and components in Duck, let's quickly go over their basis, the
Duck
(conveniently named).
Ducks
In Duck, a
Duck
is a "factory function" that takes in a single parameter, a data
object,
and returns an object which exposes things to the outside world.
That may sound fancy for beginners, but don't panic, it's extremely simple.
Here it is,
To create an instance of a Duck,
Here's a quick example,
RESULT
Components
In Duck, a
Component
is a manager for data and a DOM element.
More technically, a
Component
is a Duck
with the following structure,
elem
The variable
elem
is the DOM element that the component manages.
Duck leaves it up to the outside world to add/remove elem
to/from the DOM.
In other words, don't call document.append(elem)
in your components.
construct()
The
construct()
function is a private function for creating elem
.
update()
The
update()
function is called to make the contents of elem
reflect the contents of data
.
In update()
, do whatever you want to do to make elem
reflect data
.
enter()
& exit()
The
enter()
and exit()
functions are essentially "lifecycle methods".
They must be defined, but can be left empty.
Duck leaves it up to the outside world to call enter()
and exit()
, but they don't necessarily have to called.
destroy()
The
destroy()
function should be called when elem
is being removed from the DOM in order to prevent memory leaks.
Just like enter()
and exit()
, destroy()
must be called externally.
data
The last, but not least, thing to mention is
data
. You can think of data
as a sandbox of values/references.
The data
variable is the interface that the component has with the outside world.
It's conventional to change something in component.data
and then call component.update()
to see the changes be reflected.
IMPORTANT CAVEAT: You must always pass a fresh/new/unique object as
data
when creating a component in Duck.
This is because component instances use that same passed-in data
object directly as their own component.data
object.
The reason why this is done is to keep Duck as stripped down as possible - following the Duck philosophy of being as basic as possible.
Here's a quick example of implementing a toggle button in Duck,
RESULT
Stores
In Duck, a store is a container for mediating state that exposes getters, setters, and events.
More technically, a
Store
is a Duck
with the following structure,
NOTE: Please ignore the "events" section for now...
Unlike components where data is entirely public and
update()
is the single go-to function to call, stores are seemingly completely opposite.
A store has all variables entirely private and can have as many publicly exposed functions as you want in order to get/change those private variables.
The
events
variable at the top along with the on()
, off()
, and emit()
functions at the bottom are a basic pubsub implementation.
These functions allow the outside world to subscribe to the happenings of the store.
In practice, you should just ignore the events variable and the pubsub functions entirely.
In fact, you can ignore them right now! (For whatever reason: not interested, it looks intimidating, etc)
There are two main types of functions that you can create in a store: getter functions and setter functions.
Getter functions should return the values of internal variables.
Setter functions should change the values of internal variables and then emit that a change has occured to the outside world.
NOTE: It's worth mentioning that you don't necessarily NEED to change a value or even emit in a setter function.
Technically, a Duck setter function is any function that isn't a getter function.
For example, a "setter function" could be an async function that fetches data from a REST API.
A more appropriate name for "setter functions" is probably something like "action functions", but I find that most of my "actions" are just plain old "set" actions, so I just call them setter functions... But, feel free to call them "action functions" if that makes more sense to you!
For example, a "setter function" could be an async function that fetches data from a REST API.
A more appropriate name for "setter functions" is probably something like "action functions", but I find that most of my "actions" are just plain old "set" actions, so I just call them setter functions... But, feel free to call them "action functions" if that makes more sense to you!
Here's a quick example,
RESULT
How to Duck
Now that you understand all of the two building blocks of Duck, we're now ready to "Duck"! Or, to "build apps with Duck".
When developing in Duck, always try to develop in a store-first fashion.
Iteratively build small features of your app as logic (ie, with no user interface elements) in a store, then incrementally build very basic components to visualize the state of that store.
And, that's it!
Beyond that, you can incrementally improve the components themselves with styling/animations/etc.
To give a component access a store, simply pass a reference to a store in through a component's
data
!
Remember the
construct()
and destroy()
functions in a component?
Simply do,
store.on('eventName', update)
in construct()
&
store.off('eventName', update)
in destroy()
... for whichever events are relevant to that component.
Then, in
update()
, simply call getter functions from the store and do
whatever you feel to make elem
(and DOM elements within elem
) display whatever state you wish to display.
Oh, and when the user interacts with your component, such as a click, instead of directly making UI changes in the onclick handler function, call a store setter function instead.
If you want to change your component as a result of an interaction, you can always just listen for the corresponding event that is emitted, and your component will update like clockwork!
Here's a visual,
Follow this coding style for everything that you build in your app, and your entire app will also work like clockwork!
Here's a quick example,
RESULT