Skip to content
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

Handling asynchronous select #142

Closed
augnustin opened this issue Jun 27, 2016 · 15 comments
Closed

Handling asynchronous select #142

augnustin opened this issue Jun 27, 2016 · 15 comments

Comments

@augnustin
Copy link

My store has 2 points and I want to calculate their distance through the google maps api.

I have been suggested there to use reselect since the distance is a resulting data from the store state.

I am ready to go. But now I wonder how to handle asynchronous calculation with reselect:

export const calculateItineraries = createArraySelector(
  state => state.point1,
  state => state.point2,
  (point1, point2) => {
    return new google.maps.DirectionsService.route({
    origin: {lat: point1.lat, lon: point1.lon},
    destination: {lat: point2.lat, lon: point2.lon},
    travelMode: point1.travelMode
  }, (response, status) => {
    if (status === google.maps.DirectionsStatus.OK) {
      return response;
    } else {
      return null;
    }
  });
  }
)

But since the route calculation is asynchronous, I am wondering how to deal with it.

Should my method return a promise that will wait for resolution? The docs doesn't mention anything about it.

@ellbee
Copy link
Collaborator

ellbee commented Jun 28, 2016

Reselect is not intended for this use case, it is for computed derived data from data that is already in the store. Try looking at Redux Thunk or, if more complicated orchestration is required, Redux Saga or Redux Loop.

@ellbee ellbee closed this as completed Jul 3, 2016
@augnustin
Copy link
Author

Thanks for coming back to me, apologies for replying late.

Unless I'm wrong, none of redux-thunk, redux-saga or redux-loop fit my requirements as they all end-up updating the state with the asynchronous data.

In my case, storing the itineraries in the store doesn't make sense, as itineraries result from the actual data in my store. itineraries = f(store);

The only thing I'd like to control is when they shall be recalculated.

After some deep thought about it, my conclusion is that the logic needs to lie in the component itself: componentDidUpdate will have to check whether itineraries are up-to-date and recalculate those if they need to.

...which felt like this is what reselect does, at least synchronously !

So regarding my case, wouldn't it make sense to handle asynchronous selects? How would you deal with it otherwise?

Cheers

@samiskin
Copy link

samiskin commented Jul 5, 2016

I had a similar use case, and I ended up having to copy the main reselect selector code and alter it for promises.

function memoizeLastPromise(func) {
    let lastArgs = null;
    let lastResult = null;
    return (...args) => {
        if (
            lastArgs !== null &&
            lastArgs.length === args.length &&
            args.every((value, index) => value === lastArgs[index])
        ) {
            return lastResult;
        }
        return func(...args).then((ret) => {
            lastArgs = args;
            lastResult = new Promise((res) => res(ret));
            return ret;
        });
    };
}

I think it makes sense for this to be within the library itself. Rather than being tied to redux and its store, this library also makes sense as simply "save computations by making a function return a precomputed value if given repeated inputs". Within that scope, having a Promise compatible selector makes sense.

@augnustin
Copy link
Author

I'd be curious to have @ellbee feedback on this, but in case he stands on his position, would you consider forking the repo @samiskin with you custom code ?

I don't know enough how the library works yet to understand your code, but it shall become clearer at some point.

@ellbee
Copy link
Collaborator

ellbee commented Jul 6, 2016

If you do async calls in the selector it is not going to play nicely with Redux Devtools, which achieves time travel by replaying a series of actions. As Reselect is primarily a companion library for Redux I don't think it is a good idea to introduce anything that breaks Devtools or encourages non-idiomatic patterns.

That said, createSelectorCreator, with its swappable memoize function, was introduced to the library to make what you are requesting possible without having to maintain a fork. Just create a new selector creator that swaps defaultMemoize for memoizeLastPromise. You could create a module that wraps Reselect to introduce the new selector type, or a module that just exports memoizeLastPromise

@thomasneirynck
Copy link

+1 Agreed that this would be a very useful addition, so it would be nice to see this reconsidered.

@markerikson
Copy link
Contributor

Selectors have never been meant for asynchronous behavior - they're intended for synchronously reading values for a state tree, with memoization of derived results.

@Firenze11
Copy link

Found this project https://github.com/humflelump/async-selector that solves this issue.

@cweise
Copy link

cweise commented Mar 4, 2020

Hey, I made a small hook https://www.npmjs.com/package/use-async-selector that could help.

@dhlolo
Copy link

dhlolo commented Jun 8, 2023

Selectors have never been meant for asynchronous behavior - they're intended for synchronously reading values for a state tree, with memoization of derived results.

I noticed that there is no 'createSelectorCreator' reexport in rtk for 'reselect'. But what if I want to do some lazy loading? Such as when I select 'point1' from store, if there is no valid value, it should fetch for 'point1', update store and give updated value to selectors. In react, there is 'useEffect' could do this, but in other framework, there is no such thing, so how could I do?

@EskiMojo14
Copy link
Contributor

I noticed that there is no 'createSelectorCreator' reexport in rtk for 'reselect'. But what if I want to do some lazy loading? Such as when I select 'point1' from store, if there is no valid value, it should fetch for 'point1', update store and give updated value to selectors. In react, there is 'useEffect' could do this, but in other framework, there is no such thing, so how could I do?

usually with a thunk:

const getPost = createAsyncThunk(
  "posts/getPost",
  async (id: string, { getState }): Promise<Post> => {
    const cachedPost = selectPostById(getState(), id);
    if (cachedPost) return cachedPost;
    return fetch('api/post/' + id).then(r => r.json());
  }
);

const postAdapter = createEntityAdapter<Post>();

const { selectById: selectPostById } = postAdapter.getSelectors(
  (state: RootState) => state.posts
);

const postSlice = createSlice({
  name: "posts",
  initialState: postAdapter.getInitialState({
    loading: {} as Record<string, string>,
  }),
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getPost.pending, (state, action) => {
        state.loading[action.meta.arg] = action.meta.requestId;
      })
      .addCase(getPost.rejected, (state, action) => {
        if (state.loading[action.meta.arg] === action.meta.requestId) {
          delete state.loading[action.meta.arg];
        }
      })
      .addCase(getPost.fulfilled, (state, action) => {
        if (state.loading[action.meta.arg] === action.meta.requestId) {
          delete state.loading[action.meta.arg];
        }
        postAdapter.setOne(state, action);
      });
  },
});

// then later

const myPost = await dispatch(getPost(id)).unwrap();

This is obviously a lot of code, which is why we recommend using RTK Query, which handles all of this caching and loading state logic for you. It's most commonly used with React hooks, but can be used without.

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: "api/" }),
  endpoints: (build) => ({
    getPost: build.query<Post, string>({
      query: (id) => "post/" + id,
    }),
  }),
});

// then
const resultPromise = dispatch(api.endpoints.getPost.initiate(id));
try {
  const post = await resultPromise.unwrap();
} catch (e) {
  console.error(e);
} finally {
  resultPromise.unsubscribe();
}

@dhlolo
Copy link

dhlolo commented Jun 9, 2023

I noticed that there is no 'createSelectorCreator' reexport in rtk for 'reselect'. But what if I want to do some lazy loading? Such as when I select 'point1' from store, if there is no valid value, it should fetch for 'point1', update store and give updated value to selectors. In react, there is 'useEffect' could do this, but in other framework, there is no such thing, so how could I do?

usually with a thunk:

const getPost = createAsyncThunk(
  "posts/getPost",
  async (id: string, { getState }): Promise<Post> => {
    const cachedPost = selectPostById(getState(), id);
    if (cachedPost) return cachedPost;
    return fetch('api/post/' + id).then(r => r.json());
  }
);

const postAdapter = createEntityAdapter<Post>();

const { selectById: selectPostById } = postAdapter.getSelectors(
  (state: RootState) => state.posts
);

const postSlice = createSlice({
  name: "posts",
  initialState: postAdapter.getInitialState({
    loading: {} as Record<string, string>,
  }),
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getPost.pending, (state, action) => {
        state.loading[action.meta.arg] = action.meta.requestId;
      })
      .addCase(getPost.rejected, (state, action) => {
        if (state.loading[action.meta.arg] === action.meta.requestId) {
          delete state.loading[action.meta.arg];
        }
      })
      .addCase(getPost.fulfilled, (state, action) => {
        if (state.loading[action.meta.arg] === action.meta.requestId) {
          delete state.loading[action.meta.arg];
        }
        postAdapter.setOne(state, action);
      });
  },
});

// then later

const myPost = await dispatch(getPost(id)).unwrap();

This is obviously a lot of code, which is why we recommend using RTK Query, which handles all of this caching and loading state logic for you. It's most commonly used with React hooks, but can be used without.

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: "api/" }),
  endpoints: (build) => ({
    getPost: build.query<Post, string>({
      query: (id) => "post/" + id,
    }),
  }),
});

// then
const resultPromise = dispatch(api.endpoints.getPost.initiate(id));
try {
  const post = await resultPromise.unwrap();
} catch (e) {
  console.error(e);
} finally {
  resultPromise.unsubscribe();
}

Appreciate for your patient reply. I've gotton what your said, what you showed above is a 'pull' mode, which means, if I want to use it, I should call a get thunk manually. But actually what I want is a subscribe mode. Which means, when a value in store changed, the thunk should be auto called like a 'effect'. I've gotton an idea inspired by your reply, but don't know if it is acceptable:

const getPost = createAsyncThunk(
  "posts/getPost",
  async (id: string, { getState }): Promise<Post> => {
    const cachedPost = selectPostById(getState(), id);
    if (cachedPost) return cachedPost;
    return fetch('api/post/' + id).then(r => r.json());
  }
);

let res: Post | null = null;
let promise: Promise<Post> | null = null;
const effectWithPostSubscriber = (id) => store.subscribe(() => {
  const postMap = selectAllPost(getState());
  getPost(id).then(post => {
    res = post;
  })
});

const lazyPostAsyncSelector = (id) => {
  if (res) return res;
  return promise | new Error('not subscribe');
}

// unsubscribe if needed
const effectWithPostUnsubscriber = effectWithPostSubscriber(id);

@EskiMojo14
Copy link
Contributor

EskiMojo14 commented Jun 9, 2023

subscription management is exactly what RTKQ does - when you call the initiate thunk (or the hooks) you create a subscription to that data. when there are no subscriptions left to a cache entry, a timer is set and it's removed if there are still no subscriptions to that at the end of the timer.

RTKQ also provides a selector factory so you can use regular selector patterns once a subscription is in place.

it's hard to advise specifically without knowledge of what sort of library you're trying to work with.

const getPost = async (id, callback) => {
  const selectPostEntry = api.endpoints.getPost.select(id);
  const rtkqSubscription = store.dispatch(api.endpoints.getPost.initiate(id));
  await rtkqSubscription;
  let lastPostEntry = undefined;
  const unsubscribeStore = store.subscribe(() => {
    const data = selectPostEntry(store.getState());
    if (lastPostEntry !== data) {
      callback(data);
      lastPostEntry = data;
    }
  });
  return () => {
    rtkqSubscription.unsubscribe();
    unsubscribeStore();
  }
}

const unsubscribe = getPost(1, (postEntry) => doSomething(postEntry.data))

@markerikson
Copy link
Contributor

@dhlolo : the best answer is to have a component that reads a value from the store via useSelector, and then passes that to a query hook.

The second best option is to use the RTK listener middleware and watch for state changes, then have it dispatch the query thunk for the endpoint.

@dhlolo
Copy link

dhlolo commented Jun 13, 2023

subscription management is exactly what RTKQ does - when you call the initiate thunk (or the hooks) you create a subscription to that data. when there are no subscriptions left to a cache entry, a timer is set and it's removed if there are still no subscriptions to that at the end of the timer.

RTKQ also provides a selector factory so you can use regular selector patterns once a subscription is in place.

it's hard to advise specifically without knowledge of what sort of library you're trying to work with.

const getPost = async (id, callback) => {
  const selectPostEntry = api.endpoints.getPost.select(id);
  const rtkqSubscription = store.dispatch(api.endpoints.getPost.initiate(id));
  await rtkqSubscription;
  let lastPostEntry = undefined;
  const unsubscribeStore = store.subscribe(() => {
    const data = selectPostEntry(store.getState());
    if (lastPostEntry !== data) {
      callback(data);
      lastPostEntry = data;
    }
  });
  return () => {
    rtkqSubscription.unsubscribe();
    unsubscribeStore();
  }
}

const unsubscribe = getPost(1, (postEntry) => doSomething(postEntry.data))

Thanks for your advice. Actually, I am working with a non-standard webview env in wechat app called miniprogram . It has different interface with browser. And it is more like a enhanced react-native but without react. Its interface is more like vue 2 as it also has option API like:

Component({ data: {}, methods: {}, lifetimes: {} })

And you have to claim component both in js and xml:

<view>page</view>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants