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

Proposal: Suport createSelector #39

Open
aralroca opened this issue Nov 9, 2021 · 7 comments
Open

Proposal: Suport createSelector #39

aralroca opened this issue Nov 9, 2021 · 7 comments
Assignees
Labels
enhancement New feature or request
Projects
Milestone

Comments

@aralroca
Copy link
Collaborator

aralroca commented Nov 9, 2021

Create hooks for calculated store values:

Example:

const usePriceWithIva = createSelector(
  () => useStore.cart.price, 
  (val) => val + (val * 0.21)
);

The val + (val * 0.21) calc only will be executed once for all the components that use the usePriceWithIva(); hook. It only will be calculated again when cart.price change and is going to do a rerender for all these components that use this hook.

For this simple calculation, it does not make any sense to use the createSelector, but it can be useful for very complex calculations.

And is also possible to do the same with helpers:

const getPriceWithIva = createSelector(
  () => getStore.cart.price, 
  (val) => val + (val * 0.21)
);

Example of complete example:

import createStore from "teaful";
import createSelector from "teaful/createSelector";

const initialState = {
  username: "Aral",
  age: 31,
  cart: {
    price: 3,
    items: []
  }
};

export const { useStore, getStore, withStore } = createStore(initialState);

const usePriceWithIva = createSelector(
  () => useStore.cart.price, 
  (val) => val + (val * 0.21)
);

export default function CartPriceWithIva() {
  const price = usePriceWithIva();
  const price2 = usePriceWithIva();
  const price3 = usePriceWithIva();
  const price4 = usePriceWithIva();
  const price5 = usePriceWithIva();

  return (
    <ul>
      <li>{price}</li>
      <li>{price2}</li>
      <li>{price3}</li>
      <li>{price4}</li>
      <li>{price5}</li>
    </ul>
  );
}

And an example of createSelector:

export default function createSelector(getProxy, calc) {
  let last;

  return () => {
    const [value] = getProxy()();

    if (!last || last.value !== value) {
      last = {
        calculated: calc(value),
        value
      };
    }

    return last.calculated;
  };
}
@aralroca aralroca added the enhancement New feature or request label Nov 9, 2021
@aralroca aralroca added this to the 0.8.0 milestone Nov 9, 2021
@Chetan-unbounce
Copy link

this seems to be a good idea if I want to customize some basic primitive types, some uses cases I can think of are like formatting a store value for UI, like showing price with currency or adding discount oven name concatenation. But one thing which is not clear is what's the bigger use case, since teaful already gives us fragmented access to store and we can do the memoization in React easily(assuming its same for other framework), why do this selector thing which is just basic primitive value check

@aralroca
Copy link
Collaborator Author

aralroca commented Nov 9, 2021

@Chetan-unbounce In fact it would be very similar to useMemo with a calculated store value but with a small difference: the hook that returns the createStore makes only 1 calculation even if this hook is at the same time in many components. With useMemo it would do 1 calculation per component.

An example:

const useExpensiveCalc = createSelector(
  () => useStore.items, 
  (val) => expensiveCalc(val)
);

function Component1() {
  const val = useExpensiveCalc()
  // ...
}

function Component2() {
  const val = useExpensiveCalc()
  // ...
}

function Component3() {
  const [items] = useStore.items()
  const val = useMemo(() => expensiveCalc(items), [items])
  // ...
}

function Component4() {
  const [items] = useStore.items()
  const val = useMemo(() => expensiveCalc(items), [items])
  // ...
}

export default function App() {
    return (
     <>
        <Component1 />
        <Component2 />
        <Component3 />
        <Component4 />
     </>
    )
}

At the first render:

  • with createStore:
    • Component 1: It's doing the expensive calc
    • Component 2: It's using a cached value. It's not executing the expensive function.
  • with useMemo:
    • Component 3: It's doing the expensive calc
    • Component 4: It's doing the expensive calc

In other rerenders for other things:

  • All Components (with createStore and with useMemo) are using the cached expensive value. No calculation in this step.

After a re-render for items update:

  • with createStore:
    • Component 1: It's doing the expensive calc again
    • Component 2: It's using a cached value. It's not executing the expensive function.
  • with useMemo:
    • Component 3: It's doing the expensive calc again
    • Component 4: It's doing the expensive calc again

This is a very specific case and only makes sense for very complex calculations and heavy applications. So it does not make the createStore totally necessary. In order not to increase the size of the library since many people would not use it if anything I would upload it in a separate package.

However, for the moment it's only a proposal, it would be necessary to do several tests and to be totally sure of the results.

@aralroca
Copy link
Collaborator Author

aralroca commented Nov 9, 2021

Perhaps the createSelector could be renamed to memoStoreCalc or something like that.

@aralroca
Copy link
Collaborator Author

I've modified it a little bit to accept as many proxy entries as you want to consume:

Examples:

const useExpensiveCalc = createSelector(
  () => useStore.items,
  (items) => expensiveCalc(items)
);
const useExpensiveCalc = createSelector(
  () => useStore.items,
  () => useStore.user,
  (items, user) => expensiveCalc(items, user)
);
const useExpensiveCalc = createSelector(
  () => useStore.items,
  () => useStore.user,
  () => useStore.cart.price,
  (items, user, price) => expensiveCalc(items, user, price)
);

Example of createSelector (or with another name):

function createSelector(...proxies) {
  const calc = proxies.pop();
  const cache = new Map();
  let calculated;

  return () => {
    let inCache = true;

    const values = proxies.map((p) => {
      const [value] = p()();
      inCache &= cache.has(p) && cache.get(p) === value;
      return value;
    });

    if (!inCache) {
      calculated = calc(...values);
      proxies.forEach((p, index) => cache.set(p, values[index]));
    }

    return calculated;
  };
}

@Chetan-unbounce
Copy link

Chetan-unbounce commented Nov 10, 2021

The example works, because we are using the same instance of the function returned from createSelector. But if we try to make the logic generic and move it to a new file like

const useSelector = (stateFun, cb) => {
  return createSelector(stateFun, cb); // same function we have just takes in custom state val and callback
};

export default useSelector;

now if we try to import this in two different files we would get different instance of the returned function and the lexical scope will be different and the caching won't work.

@aralroca can u add an example with the above use case too, where we make it generic and import it from a different file into different components in different files.

@aralroca aralroca modified the milestones: 0.8.0, Before 1.0.0 Nov 14, 2021
@aralroca
Copy link
Collaborator Author

aralroca commented Nov 14, 2021

@Chetan-unbounce I did a little modification to support to define the stateFun & cb to useSelector.

I hope this works for you, if not we will look for a better way to implement it. At the moment it is just a draft.

Way 1

export const useCalculatedValue = createSelector(
    () => useStore.cart.price,
    () => useStore.username,
    (price, username) => `${username} - ${price * 3}`
);

And to use the selector:

const calculatedValue = useCalculatedValue()

Way 2

export const useSelector = createSelector();

And to use the selector:

const calculatedValue = useSelector(
    () => useStore.cart.price,
    () => useStore.username,
    (price, username) => `${username} - ${price * 3}`
 );

createSelector:

function createSelector(...proxiesCreateSelector) {
  const cache = new Map();
  let calculated;

  return (...proxiesSelector) => {
    const proxies = proxiesCreateSelector.length
      ? proxiesCreateSelector
      : proxiesSelector;

    const calc = proxies.pop();
    let inCache = true;

    const values = proxies.map((p) => {
      const [value] = p()();
      inCache &= cache.has(p) && cache.get(p) === value;
      return value;
    });

    if (!inCache) {
      calculated = calc(...values);
      proxies.forEach((p, index) => cache.set(p, values[index]));
    }

    return calculated;
  };
}

@aralroca
Copy link
Collaborator Author

I think it is better to create the hooks directly with the createSelector. Why do you need a separate useSelector?

@aralroca aralroca added this to Progress in 1.0 Dec 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
1.0
Progress
Development

No branches or pull requests

3 participants