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

Can selectors be supported? #68

Open
MinJieLiu opened this issue Mar 23, 2020 · 4 comments
Open

Can selectors be supported? #68

MinJieLiu opened this issue Mar 23, 2020 · 4 comments

Comments

@MinJieLiu
Copy link

e.g.

const count = Counter.useSelector(state => state.count)

Doing this only triggers rerender when count changes.

It would be perfect if you supported this.

@MinJieLiu
Copy link
Author

MinJieLiu commented Apr 6, 2020

I implemented one myself with use-context-selector

import React from 'react';

export type SelectorFn<Value, Selected> = (value: Value) => Selected;

export interface ContainerProviderProps<State = void> {
  initialState?: State;
}

export function createContainer<Value, State = void>(useHook: (initialState?: State) => Value) {
  const Context = React.createContext<Value | null>(null, () => 0);
  const listeners: Set<(value: Value) => void> = new Set();

  const Provider: React.FC<ContainerProviderProps<State>> = React.memo(
    ({ initialState, children }) => {
      const value = useHook(initialState);

      if (process.env.NODE_ENV !== 'production') {
        // eslint-disable-next-line react-hooks/rules-of-hooks
        React.useLayoutEffect(() => {
          listeners.forEach(listener => {
            listener(value);
          });
        });
      } else {
        listeners.forEach(listener => {
          listener(value);
        });
      }
      return <Context.Provider value={value}>{children}</Context.Provider>;
    },
  );

  function useSelector<Selected = any>(selector: SelectorFn<Value, Selected>): Selected {
    const [, forceUpdate] = React.useReducer(c => c + 1, 0);
    const value = React.useContext(Context);
    if (value === null) {
      throw new Error();
    }
    const selected = selector(value);
    const ref = React.useRef<{
      selector: SelectorFn<Value, Selected>;
      value: Value;
      selected: Selected;
    } | null>(null);

    React.useLayoutEffect(() => {
      ref.current = {
        selector,
        value: value!,
        selected,
      };
    });

    React.useLayoutEffect(() => {
      const callback = (nextValue: Value) => {
        try {
          if (!ref.current) {
            return;
          }
          const refValue = ref.current;
          if (refValue.value === nextValue) {
            return;
          }
          const nextSelected = refValue.selector(nextValue);
          if (Object.is(refValue.selected, nextSelected)) {
            return;
          }

          if (typeof refValue.selected === 'object' && typeof nextSelected === 'object') {
            if (
              Object.entries(refValue.selected).every(([k, v]) => Object.is(v, nextSelected[k]))
            ) {
              return;
            }
          }
        } catch (e) {
          // ignore
        }
        forceUpdate();
      };
      listeners.add(callback);
      return () => {
        listeners.delete(callback);
      };
    }, []);
    return selected;
  }

  function useContainer(): Value {
    return useSelector(x => x);
  }

  return {
    Provider,
    /**
     * Usage `useSelector(state => state.count)`
     * @param selector
     */
    useSelector,
    useContainer,
  };
}

@StarpTech
Copy link

StarpTech commented May 20, 2020

Isn't a selector regarding this "framework" just an useEffect, useCallback, ...inside the container hook?

import { useCallback } from "react";
import { User } from "lib/user-container";
import { Products } from "lib/products-container";

export default function useShoppingCart(initialState = 0) {
  let [products] = useState([]);
  let currentUser = User.useContainer();
  let products = Products.useContainer();

  const selectShoppingCartItems = useCallback(
    () => currentUser.shoppingCart.itemIds.map((id) => products[id]),
    [products, currentUser.shoppingCart.itemIds]
  );

  return { selectShoppingCartItems };
}
  • We have memorization
  • We can pick state selectively

Example from https://daveceddia.com/redux-selectors/

@MinJieLiu
Copy link
Author

@StarpTech I ’m lazy and I do n’t want to optimize manually

Your example still renders through

@StarpTech
Copy link

I ’m lazy and I do n’t want to optimize manually

I got your point but this is something you will do once while writing your selector.

Your example still renders through

But the expensive parts aren't re-executed the same as with selectors. Keep containers small and focused.

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

2 participants