How to avoid warning "You access a signal or memo outside a reactive tracking context"

465 views Asked by At

I have an async action which alters data in an external api (POST request). When the request has finished, then I need to refetch data from this api, in order to reflect the changes in the view. But when I call my_resource.refetch() inside the action, I get this warning in the web console:

At .../.cargo/registry/src/github.com-1ecc6299db9ec823/leptos_reactive-0.3.0/src/resource.rs:910:25, you access a signal or memo (defined at .../.cargo/registry/src/github.com-1ecc6299db9ec823/leptos_reactive-0.3.0/src/resource.rs:339:18) outside a reactive tracking context. This might mean your app is not responding to changes in signal values in the way you expect.

How to get rid of it?

Here is a minimal working example which reflects the problem (when clicking on the button, the warning appears):

use std::sync::atomic::{AtomicUsize, Ordering};

use leptos::*;

/// Let's pretend we have some data which can accessed and modified via an external API:
static EXTERNAL_DATA: AtomicUsize = AtomicUsize::new(1);

/// Alters the API data.
pub async fn alter_external_data() {
    EXTERNAL_DATA.fetch_add(1, Ordering::SeqCst); // Should be async
}

/// Gets the API data.
pub async fn fetch_external_data(n: i32) -> String {
    n.to_string().repeat(EXTERNAL_DATA.load(Ordering::SeqCst)) // Should be async
}

#[component()]
pub fn App(cx: Scope) -> impl IntoView {
    let (read_number, set_number) = create_signal(cx, 1);

    // `async_data` depends on `read_number`, but must be refreshed each time the button below has
    // been clicked, because the external data has been altered.
    let async_data = create_local_resource(cx, read_number, |n| async move {
        fetch_external_data(n).await
    });

    // Action which alters the external data. Then `async_data` has to be refetched.
    let alter_external_data_action = create_action(cx, move |_: &()| async move {
        alter_external_data().await;
        async_data.refetch(); // THIS TRIGGERS A WARNING
    });

    view! { cx,
        <p>
            "Fetched data: "
            {move || async_data.read(cx)}
        </p>

        <p>
            "N = "
            <input type="number"
                prop:value=read_number
                on:change=move |ev| set_number(event_target_value(&ev).parse().unwrap())
            />
        </p>

        <p>
            <button on:click=move |_| alter_external_data_action.dispatch(())>
                "Alter external data"
            </button>
        </p>
    }
}

fn main() {
    mount_to_body(|cx| view! { cx, <App/> })
}
2

There are 2 answers

0
yolenoyer On

This can be solved by using an effect which depends on the action's value:

let alter_external_data_action = create_action(cx, move |_: &()| async move {
    alter_external_data().await;
    // REMOVE THIS CALL:
    // async_data.refetch();
});

// ADD THIS EFFECT:
let resp = alter_external_data_action.value();
create_effect(cx, move |_| if resp().is_some() {
    async_data.refetch()
});

EDIT: This generic method can be used in order to reduce code verbosity:

pub fn refetch_resource_after_action<RS, RT, AI, AO>(
    cx: Scope,
    resource: Resource<RS, RT>,
    action: Action<AI, AO>,
)
where
    RS: Clone,
    AO: Clone,
{
    let action_value = action.value();
    create_effect(cx, move |_| if action_value().is_some() {
        resource.refetch()
    });
}

Now in the solution above, instead of adding the effect manually, you can simply write:

refetch_resource_after_action(cx, node_content, alter_external_data_action);
2
Ikea_Power On

The earlier answer is probably correct, but I thought I might as well give a more general workaround to avoid that warning.

There is a function in the leptos_reactive crate called SpecialNonReactiveZone::enter, which should mark the current function as a reactive zone, disabling that warning. You'll also need to add leptos_reactive as a dependency for you crate as well.

Please note that this is meant to be an internal feature, so don't rely on this too much.

use leptos_reactive::SpecialNonReactiveZone;

#[component]
pub fn App(cx: Scope) -> impl IntoView {

    ...

    let alter_external_data_action = create_action(cx, move |_: &()| async move {
        SpecialNonReactiveZone::enter();
        alter_external_data().await;
        async_data.refetch(); // THIS TRIGGERS A WARNING
    });

    ...

}

This is not completely correct yet though! As this is an internal function you should first store the value of SpecialNonReactiveZone::is_inside() and update the value with an if statement at the end, which will ensure that the warning will function correctly in other places.

But if you don't care about reenabling the warning and you're fine with potentially disabling it in other places it's perfectly fine to use the code above.

Updated code:

use leptos_reactive::SpecialNonReactiveZone;

#[component]
pub fn App(cx: Scope) -> impl IntoView {

    ...

    let alter_external_data_action = create_action(cx, move |_: &()| async move {
        let is_reactive = SpecialNonReactiveZone::is_inside();
        SpecialNonReactiveZone::enter();

        alter_external_data().await;
        async_data.refetch();

        if (!is_reactive) {
            SpecialNonReactiveZone::exit()
        }
    });

    ...

}

It should work like this, I haven't tested it though. So if it doesn't work feel free to complain!