Custom Hooks

Hooks are a great way to encapsulate business logic. If none of the existing hooks work for your problem, you can write your own.

When writing your hook, you can make a function that starts with use_ and takes any arguments you need. You can then use the use_hook method to create a hook that will be called the first time the component is rendered.

Composing Hooks

To avoid repetition, you can encapsulate business logic based on existing hooks to create a new hook.

For example, if many components need to access an AppSettings struct, you can create a "shortcut" hook:

src/hooks_composed.rs
fn use_settings() -> Signal<AppSettings> {
    consume_context()
}

Or if you want to wrap a hook that persists reloads with the storage API, you can build on top of the use_signal hook to work with mutable state:

src/hooks_composed.rs
use gloo_storage::{LocalStorage, Storage};
use serde::{de::DeserializeOwned, Serialize};

/// A persistent storage hook that can be used to store data across application reloads.
#[allow(clippy::needless_return)]
pub fn use_persistent<T: Serialize + DeserializeOwned + Default + 'static>(
    // A unique key for the storage entry
    key: impl ToString,
    // A function that returns the initial value if the storage entry is empty
    init: impl FnOnce() -> T,
) -> UsePersistent<T> {
    // Use the use_signal hook to create a mutable state for the storage entry
    let state = use_signal(move || {
        // This closure will run when the hook is created
        let key = key.to_string();
        let value = LocalStorage::get(key.as_str()).ok().unwrap_or_else(init);
        StorageEntry { key, value }
    });

    // Wrap the state in a new struct with a custom API
    UsePersistent { inner: state }
}

struct StorageEntry<T> {
    key: String,
    value: T,
}

/// Storage that persists across application reloads
pub struct UsePersistent<T: 'static> {
    inner: Signal<StorageEntry<T>>,
}

impl<T> Clone for UsePersistent<T> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<T> Copy for UsePersistent<T> {}

impl<T: Serialize + DeserializeOwned + Clone + 'static> UsePersistent<T> {
    /// Returns a reference to the value
    pub fn get(&self) -> T {
        self.inner.read().value.clone()
    }

    /// Sets the value
    pub fn set(&mut self, value: T) {
        let mut inner = self.inner.write();
        // Write the new value to local storage
        LocalStorage::set(inner.key.as_str(), &value);
        inner.value = value;
    }
}

Custom Hook Logic

You can use use_hook to build your own hooks. In fact, this is what all the standard hooks are built on!

use_hook accepts a single closure for initializing the hook. It will be only run the first time the component is rendered. The return value of that closure will be used as the value of the hook – Dioxus will take it, and store it for as long as the component is alive. On every render (not just the first one!), you will get a reference to this value.

Note: You can use the use_on_destroy hook to clean up any resources the hook uses when the component is destroyed.

Inside the initialization closure, you will typically make calls to other cx methods. For example:

  • The use_signal hook tracks state in the hook value, and uses schedule_update to make Dioxus re-render the component whenever it changes.

Here is a simplified implementation of the use_signal hook:

src/hooks_custom_logic.rs
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

struct Signal<T> {
    value: Rc<RefCell<T>>,
    update: Arc<dyn Fn()>,
}

impl<T> Clone for Signal<T> {
    fn clone(&self) -> Self {
        Self {
            value: self.value.clone(),
            update: self.update.clone(),
        }
    }
}

fn my_use_signal<T: 'static>(init: impl FnOnce() -> T) -> Signal<T> {
    use_hook(|| {
        // The update function will trigger a re-render in the component cx is attached to
        let update = schedule_update();
        // Create the initial state
        let value = Rc::new(RefCell::new(init()));

        Signal { value, update }
    })
}

impl<T: Clone> Signal<T> {
    fn get(&self) -> T {
        self.value.borrow().clone()
    }

    fn set(&self, value: T) {
        // Update the state
        *self.value.borrow_mut() = value;
        // Trigger a re-render on the component the state is from
        (self.update)();
    }
}
  • The use_context hook calls consume_context (which would be expensive to call on every render) to get some context from the component

Here is an implementation of the use_context and use_context_provider hooks:

src/hooks_custom_logic.rs
pub fn use_context<T: 'static + Clone>() -> T {
    use_hook(|| consume_context())
}

pub fn use_context_provider<T: 'static + Clone>(f: impl FnOnce() -> T) -> T {
    use_hook(|| {
        let val = f();
        // Provide the context state to the component
        provide_context(val.clone());
        val
    })
}