New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

watch with immediate:true is too immediate #9134

Closed
suau opened this Issue Dec 3, 2018 · 1 comment

Comments

Projects
None yet
2 participants
@suau

suau commented Dec 3, 2018

Version

2.5.13

Reproduction link

https://jsfiddle.net/suaujs/8b9m301c/

Steps to reproduce

Click run in the jsfiddle and check the console

What is expected?

The Vuex.Store.watch method returns a unwatch function, which can be called to stop watching.
With the immediate option, the current value should be called and the unwatch() function should be available in the callback.

What is actually happening?

The issue arises when Vuex.Store.watch is used with the immediate option, which will trigger the callback synchronously within the watch() call.
therefore it isn't possible to unwatch from within the callback anymore as the callback is called before the watch() method returns the unwatch method.


According to vuejs/vuex#1249 vuex uses vue's watch method.

Why is this important/usecase:

Get a store object until an expected value is returned, then stop watching and execute an action.
Example 1: Get the "user" object from the store, let's assume:

  • undefined: the user hasn't been loaded from localstorage yet
  • null: means there is no user
  • object: the user is loaded and logged in

here is some code on how I'd do that (not working, due to mentioned behavior/bug):

function getUser(store: Store): Promise<any> {
    return new Promise((resolve, reject) => {
        const unwatch = store.watch(
            (state: any) => {
                return state.user;
            },
            (value: any) => {
                if (value || value === null) {
                    unwatch(); // THIS WILL THROW AN ERROR IF IT IS THE FIRST VALUE RECEIVED
                    resolve(value)
                }
            },
            {
                immediate: true
            }
        );
    });
}

Possible solutions

  • Vuex.Store.watch should never run the getter or callback synchronously in the watch() call
  • pass in an unwatch function as the third parameter into the callback (see workaround wrapper below)
  • make use of the callbacks return value, e.g. return true will stop watching

Workaround

Manually check if the current state is already meeting your unwatch() expectations and only register a store watcher if that's not the case. Fix for Example 1:

function getUser(store: Store): Promise<any> {
    return new Promise((resolve, reject) => {
        const user = store.state.user;
        if (user || user === null) {
            resolve(user);
        } else {
            const unwatch = store.watch(
                (state: any) => {
                    return state.user;
                },
                (value: any) => {
                    if (value || value === null) {
                        unwatch();
                        resolve(value)
                    }
                }
            );
        }
    });
}

Workaround wrapper

This is a more general purpose wrapper as a workaround (not fully tested)

function wrapper(store: Store<any>, getter: (state?: any, getters?: any) => any,
                  callback: (newValue?: any, oldValue?: any, unwatch?: () => void) => void, options?: any): () => void {
    // callback receives a unwatch function as third parameter
    if (options.immediate) {
        let shouldStop = false;
        const current = getter(store.state, store.getters);
        callback(current, undefined, () => {
            shouldStop = true;
        });
        if (shouldStop) {
            return () => {};
        }
    }
    const unwatch = store.watch(
        getter,
        (newValue: any, oldValue: any) => {
            callback(newValue, oldValue, unwatch);
        },
        Object.assign({}, options, {immediate: false}));
    return unwatch;
}
@LinusBorg

This comment has been minimized.

Member

LinusBorg commented Dec 3, 2018

immediate is a synchronous process - and has to be, or the option wouldn't be useful in many situations that it's used in.

Also, changing this would be a significant breaking change.

Your Workaround isn't so bad. Alternatively, you can use a simple flag and defer the unwatch:

function getUser(store: Store): Promise<any> {
    return new Promise((resolve, reject) => {
        let found
        const unwatch = store.watch(
            (state: any) => {
                return state.user;
            },
            (value: any) => {
                if (found) return

                if (value || value === null) {
                    found = true
                    setImmedite(unwatch);
                    resolve(value)
                }
            },
            {
                immediate: true
            }
        );
    });
}

In summary, immediate works like it does for good reason, and edge cases like this one can be worked around easily enough.

@LinusBorg LinusBorg closed this Dec 3, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment