cefn / watchable

Repo for @watchable/store and supporting packages.
MIT License
5 stars 1 forks source link

Path based useSelected #32

Open cefn opened 1 year ago

cefn commented 1 year ago

If the argument passed to useSelected is a list of state paths, it would be very useful if this could compose all the complex elements of a lazy selection.

So the following would return a well-typed lookup and only cause a React re-render when one of the specified paths had varied....

function Component(){
  const paths = useSelectedPaths(gameStore, ["wordToGuess", "guessedLetters", "guessedLetters.length"] );
  return <><p>{paths.wordToGuess}</p><p>{paths["guessedLetters.length"]}</p></>
}

The useSelectedPaths call would be equivalent to something like this maybe...


const paths = useSelected(gameStore, (state) => {
  const items = ["wordToGuess", "guessedLetters", "guessedLetters.length"]).map((path) => lodash.get(path));
  return reselect(items, (items) => Object.fromEntries(paths.map((path, pos) => [path, items[pos]])))
} )
cefn commented 1 year ago

Here's an example manually-tested implementation...

import { createStructuredSelector } from "reselect";

export function createPicker<T, K extends keyof T>(
  ...keys: K[]
): (state: T) => Pick<T, K> {
  const selectorEntries = keys.map((key) => [key, (state: T) => state[key]]);
  const selector = createStructuredSelector<Pick<T, K>>(
    Object.fromEntries(selectorEntries)
  );
  return (value: T) => {
    return selector(value);
  };
}

export function createGameStatePicker<K extends keyof GameState>(...keys: K[]) {
  return createPicker<Immutable<GameState>, K>(...keys);
}

It's used a bit like this...

const selector = createGameStatePicker("guessedLetters", "wordToGuess");

export function HangmanWord(props: GameProps) {
  const { gameStore } = props;
  const { guessedLetters, wordToGuess } = useSelected(gameStore, selector);
cefn commented 1 year ago

Could wrap around https://www.npmjs.com/package/just-safe-get and adopt the typings of https://github.com/cefn/lauf/issues/180

cefn commented 1 year ago

Here's an example of a working inference of Path strings, and of their referents...

export type PathImpl<T, Key extends keyof T> = Key extends string
  ? T[Key] extends Record<string, any>
    ?
        | `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> &
            string}`
        | `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
    : never
  : never;

export type DescendantPath<T> = PathImpl<T, keyof T> | keyof T;

export type Path<T> = keyof T extends string
  ? DescendantPath<T> extends string | keyof T
      ? DescendantPath<T>
      : keyof T
    :never

export type PathValue<
  T,
  P extends Path<T>,
> = P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? Rest extends Path<T[Key]>
      ? PathValue<T[Key], Rest>
      : never
    : never
  : P extends keyof T
  ? T[P]
  : never;

const tree = {
    grandparent:{
        parent:{
            child:{
                hands:{
                    left:{
                        fingers:[0,1,2,3,4]
                    },
                    right:{
                        fingers:[0,1,2,3,4]
                    }
                }
            }
        }
    }
} as const;

const treePath: Path<typeof tree> = "grandparent.parent.child";
type PathReferent = PathValue<typeof tree, typeof treePath>