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, starknet::Event)]
    struct SwitchEvent {}

    #[event]
    #[derive(Drop, 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 components::switchable::switchable_component;

    component!(path: switchable_component, storage: switch, event: SwitchableEvent);

    #[abi(embed_v0)]
    impl SwitchableImpl = switchable_component::Switchable<ContractState>;
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        switch: switchable_component::Storage,
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch._off();
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        SwitchableEvent: switchable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use components::switchable::switchable_component::SwitchableInternalTrait;
    use components::switchable::ISwitchable;

    use starknet::storage::StorageMemberAccessTrait;
    use super::SwitchContract;

    fn STATE() -> SwitchContract::ContractState {
        SwitchContract::contract_state_for_testing()
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let state = STATE();
        assert(state.is_on() == false, 'The switch should be off');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_switch() {
        let mut state = STATE();

        state.switch();
        assert(state.is_on() == true, 'The switch should be on');

        state.switch();
        assert(state.is_on() == false, 'The switch should be off');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_value() {
        let mut state = STATE();
        assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value');

        state.switch.switch();
        assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_internal_off() {
        let mut state = STATE();

        state.switch._off();
        assert(state.is_on() == false, 'The switch should be off');

        state.switch();
        state.switch._off();
        assert(state.is_on() == false, 'The switch should be off');
    }
}

Deep dive into components

You can find more in-depth information about components in The Cairo book - Components.

Last change: 2024-06-08, commit: 5e758e6