Components How-To
Components are like modular addons that can be snapped into contracts to add reusable logic, storage, and events. They are used to separate the core logic from common functionalities, simplifying the contract's code and making it easier to read and maintain. It also reduces the risk of bugs and vulnerabilities by using well-tested components.
Key characteristics:
- Modularity: Easily pluggable into multiple contracts.
- Reusable Logic: Encapsulates specific functionalities.
- Not Standalone: Cannot be declared or deployed independently.
How to create a component
The following example shows a simple Switchable
component that can be used to add a switch that can be either on or off.
It contains a storage variable switchable_value
, a function switch
and an event Switch
.
#[starknet::interface]
pub trait ISwitchable<TContractState> {
fn is_on(self: @TContractState) -> bool;
fn switch(ref self: TContractState);
}
#[starknet::component]
pub mod switchable_component {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
switchable_value: bool,
}
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub struct SwitchEvent {}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
SwitchEvent: SwitchEvent,
}
#[embeddable_as(Switchable)]
impl SwitchableImpl<
TContractState, +HasComponent<TContractState>
> of super::ISwitchable<ComponentState<TContractState>> {
fn is_on(self: @ComponentState<TContractState>) -> bool {
self.switchable_value.read()
}
fn switch(ref self: ComponentState<TContractState>) {
self.switchable_value.write(!self.switchable_value.read());
self.emit(Event::SwitchEvent(SwitchEvent {}));
}
}
#[generate_trait]
pub impl SwitchableInternalImpl<
TContractState, +HasComponent<TContractState>
> of SwitchableInternalTrait<TContractState> {
fn _off(ref self: ComponentState<TContractState>) {
self.switchable_value.write(false);
}
}
}
A component is really similar to a contract and can also have:
- An interface defining entrypoints (
ISwitchableComponent<TContractState>
) - A Storage struct
- Events
- Internal functions
It doesn't have a constructor, but you can create an _init
internal function and call it from the contract's constructor. In the previous example, the _off
function will be used this way.
How to use a component
Now that we have a component, we can use it in a contract.
The following contract incorporates the Switchable
component:
#[starknet::contract]
pub mod SwitchContract {
use super::switchable_component;
component!(path: switchable_component, storage: switch, event: SwitchableEvent);
#[abi(embed_v0)]
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
switch: switchable_component::Storage,
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
SwitchableEvent: switchable_component::Event,
}
// You can optionally use the internal implementation of the component as well
impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;
#[constructor]
fn constructor(ref self: ContractState) {
// Internal function call
self.switch._off();
}
}
How to test a component
In order to effectively test a component, you need to test it in the context of a contract.
A common practice is to declare a Mock
contract that has the only purpose of testing the component.
To test the Switchable
component, we can use the previous SwitchableContract
:
#[cfg(test)]
mod test {
use super::SwitchContract; // Used as a mock contract
use super::switchable_component::SwitchEvent;
use super::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
use starknet::{syscalls::deploy_syscall, ContractAddress};
use starknet::SyscallResultTrait;
fn deploy() -> (ISwitchableDispatcher, ContractAddress) {
let (address, _) = deploy_syscall(
SwitchContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
(ISwitchableDispatcher { contract_address: address }, address)
}
#[test]
fn test_constructor() {
let (switchable, _) = deploy();
assert_eq!(switchable.is_on(), false);
}
#[test]
fn test_switch() {
let (switchable, contract_address) = deploy();
switchable.switch();
assert_eq!(switchable.is_on(), true);
assert_eq!(
starknet::testing::pop_log(contract_address),
Option::Some(SwitchContract::Event::SwitchableEvent(SwitchEvent {}.into()))
);
}
#[test]
fn test_multiple_switches() {
let (switchable, _) = deploy();
switchable.switch();
assert_eq!(switchable.is_on(), true);
switchable.switch();
assert_eq!(switchable.is_on(), false);
switchable.switch();
assert_eq!(switchable.is_on(), true);
}
}
Deep dive into components
You can find more in-depth information about components in The Cairo book - Components.