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.

It is a good practice to prefix the component storage variables with the component name to avoid collisions.

#[starknet::interface]
pub trait ISwitchable<TContractState> {
    fn is_on(self: @TContractState) -> bool;
    fn switch(ref self: TContractState);
}

#[starknet::component]
pub mod switchable_component {
    #[storage]
    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.

It's currently not possible to use the same component multiple times in the same contract. This is a known limitation that may be lifted in the future.

For now, you can view components as implementations of specific interfaces or features (Ownable, Upgradeable, ... ~able). This is why we called the component in the above example Switchable, and not Switch; the contract is switchable, it does not have a switch.

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::{Event, SwitchEvent};
    use super::{ISwitchableDispatcher, ISwitchableDispatcherTrait};
    use starknet::{syscalls::deploy_syscall, contract_address_const, 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.

Last change: 2024-07-01, commit: 6f4d055