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

Question: Why can't we support createAsyncSelector? #691

Open
xotahal opened this issue Jan 26, 2024 · 11 comments
Open

Question: Why can't we support createAsyncSelector? #691

xotahal opened this issue Jan 26, 2024 · 11 comments

Comments

@xotahal
Copy link

xotahal commented Jan 26, 2024

Hey folks! 👋 First of all, great work on this library! 👏

I have a question. I'd like to understand where the problem lies. I know that reselect doesn't support async selectors, and if we need to fetch data, we should use a thunk or another approach. That's fine and it works. But what if we need to use, let's say, a 3rd party library inside of a selector that computes something, but does that only in an async way?

For example, something like this:

const asyncSelector = createAsyncSelector(
   (state) => state.a,
   (state) => state.b,
   async (a, b) => {
     return await computeSomethingAsync(a, b);
   }

Also, if I want to use this as the input of another async selector:

const anotherAsyncSelector = createAsyncSelector(
   asyncSelector,
   (state) => state.c,
   async (resultAB, c) => {
     return await computeSomethingAsync(resultAB, c);
   }

I would just like to understand why this is considered an antipattern. Why can't we support this? This is not a case of fetching data; it's deriving data from the Redux store asynchronously.

@markerikson
Copy link
Contributor

That's actually the problem. A selector is, by definition, 100% synchronous and pure (same as a reducer, actually). It gets called synchronously, is expected to return a value immediately, and that value is based solely on the arguments that were passed in (typically the Redux store state, plus optional parameters).

You can write other async function that are similar (in that they take the state as an argument), but they would no longer be selectors.

Where and how would you expect to use one of these "async selectors"?

@xotahal
Copy link
Author

xotahal commented Jan 26, 2024

Appreciate very quick response 🙏

We use https://jsonata.org. It's very simple query language that "unfortunately" works async (in a future version). Here is a simplified example:

const asyncSelector = createAsyncSelector(
  (state) => state.a,
  (state) => state.b,
  async (a, b) => {
     const context = { a, b }
     return await jsonataEvaluator("a+b", context)
  }

Then in React world I would do this:

function PrintAsyncSelector() {
   const result = useSelector(asyncSelector)
   
   return <p>{result}</p>
}

function Component() {
   return (
      <Suspense>
         <PrintAsyncSelector />
      </Suspense>
   )
}

The Component is suspended while evaluating asyncSelector. That would mean the useSelector would need to throw promise.


Another use case would be in "side effects". If you want to get current and derivated data from the store in callback executed on click.

function Button() {
  const onSubmit = async () => {
     const result = await asyncSelector(store.getState())
     await doRestCallWithCurrentValue(result)
  }
  
  return <Button onClick={onSubmit}>Submit</Button>
}

I know I could do the await jsonataEvaluator("a+b", context) directly in the onSubmit callback, but I'd like to re-use the same code from the selector.

Hope it makes sense.

@markerikson
Copy link
Contributor

Yeah, unfortunately that won't work with Redux. useSelector (and the underlying React useSyncExternalStore hook) do expect standard 100% synchronous selector functions.

What are you actually trying to retrieve with JSONata? is it actual data in the Redux store state?

@xotahal
Copy link
Author

xotahal commented Jan 26, 2024

Yes, it is data in the Redux store. It is very complex network of createSelector that collects data from redux and then use jsonata to evaluate the expression.

I guess I could do something like this:

function useHook() {
   const [result, setResult] = useState()
   const context = useSelector(collectDataFromRedux)
   
   useEffect(() => {
      setResult(await jsonataEvaluator("a+b", context))
   }, [context])
   
   return result
} 

But that complicates the code and it feels like unnecessary work-around of React's Suspense.

@EskiMojo14
Copy link
Contributor

you could try using the suspense util library for it?

@markerikson
Copy link
Contributor

Part of the issue is it seems like the entire state is needed as an argument. Normally to get that you'd have to use useSelector(state => state), which would force a re-render 100% of the time and is bad.

@xotahal
Copy link
Author

xotahal commented Jan 27, 2024

Sorry, maybe it is just a lack of knowledge about all these libraries. But if I am not wrong there is no problem of making createSelector work async, right? We can discuss what exactly pure function means and how strict we need to be about it but for the sake of conversation let's say that the following would still be pure action.

We use await/async and the jsonata (in our case) evaluates the same inputs to the same outputs without any side effects, I think we can consider such a function as pure.

The problem is the connection between React and Redux, where useSelector doesn't wait for the reselect's selector to be done. But that shouldn't be a problem to support with a few adjustments, right?

I put together this code sandbox that might better illustrate what I mean - https://codesandbox.io/p/sandbox/use-external-storage-forked-yflsy3

Please let me know if I am missing something

@kyranjamie
Copy link

IMO async selectors are the missing key for Redux state management when it comes comparisons of atom-based state libraries. They'd would be a huge upgrade, even if implemented somewhat separately, given the reasons @markerikson mentions here

@markerikson
Copy link
Contributor

@kyranjamie : can you give an example of what a notional "async selector" API would look like, how it would be used, and thoughts on it would work?

@kyranjamie
Copy link

Say I'm working on an app that has to perform expensive cryptographic operations on an underlying key in a Redux store. Assume a library exposes some logic to derive child key information via an async method.

const rootKey = createSelector(state => state.key);
const childKey = createAsyncSelector(rootKey, async key => await deriveChildKey(key));

function LayoutComponent () {
  const childKey = useAsyncSelector(childKey); // suspends or has default value of `undefined`
  return <KeyView childKey={key} />;
}

This is super easy in a library like Jotai. I've actually refactored an app to Redux from Jotai and the lack of an API like this is the biggest pain point.

@markerikson
Copy link
Contributor

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

4 participants