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

Feature request: Second parameter for atomFamily passed to default function #231

Open
afitiskin opened this issue Jun 1, 2020 · 19 comments
Labels
enhancement New feature or request

Comments

@afitiskin
Copy link

afitiskin commented Jun 1, 2020

First of all, thank you guys for an awesome library!

I checked the latest version (0.0.8) and found your utilities atomFamily and selectorFamily very useful, but currently limited.

From your example default value for an atom in atomFamily may be parameterized:

const myAtomFamily = atomFamily({
  key: ‘MyAtom’,
  default: param => defaultBasedOnParam(param),
});

const myAtom = atomFamily(myAtomKey);

In case above myAtomKey is responsible for both: atom key and it's default value. This behaviour makes atomFamily very limited. What if we add additional parameter to the function which is returned from atomFamily and simply pass this additional parameter to default function?

const myAtomFamily = atomFamily({
  key: ‘MyAtom’,
  default: (key, params) => defaultBasedOnKeyAndParams(key, params),
});

const myAtom = atomFamily(myAtomKey, myAtomSettings);

Now, we may (or may not) pass additional parameters to create atoms with custom default values and this addition will make atomFamily more useful. Same additional parameter should be added to selectorFamily as well:

const myGenericNumberState = atomFamily({
  key: 'MyNumbers',
  default: (key, defaultValue) => defaultValue,
});

const myGenericMultipliedState = selectorFamily({
  key: 'MyMultipliedNumbers',
  get: (key, multiplier) => ({get}) => {
    return get(myGenericNumberState(key)) * multiplier;
  },
  set: (key, multiplier) => ({set}, newValue) => {
    set(myGenericNumberState(key), newValue / multiplier);
  },
});

const xAtom = myGenericNumberState('x', 2); // creates x atom with default value 2
const yAtom = myGenericNumberState('y', 3); // creates y atom with default value 3

const x = myGenericMultipliedState('x', 5); // creates selector which multiplies x atom value by 5
const y = myGenericMultipliedState('y', 10); // creates selector which multiplies y atom value by 10
@afitiskin afitiskin changed the title Feature request: Additional parameters for parameterized default value with atomFamily/selectorFamily Feature request: Additional parameters for parameterized atomFamily/selectorFamily Jun 1, 2020
@acutmore
Copy link

acutmore commented Jun 1, 2020

Hi @afitiskin

Perhaps not as elegant but you can pass more values by using an object or array.

With object:

const myGenericNumberState = atomFamily({
  key: 'MyNumbers',
  default: ({x, y}) => defaultBasedOnXY(x, y),
});

myGenericNumberState({x: 1, y: 2});

With array;

const myGenericNumberState = atomFamily({
  key: 'MyNumbers',
  default: ([x, y]) => defaultBasedOnXY(x, y),
});

myGenericNumberState([1, 2]);

@drarmstr
Copy link
Contributor

drarmstr commented Jun 1, 2020

Yes, we use named parameters by passing in objects as the parameter all the time. Please also note the restrictions for types that may be passed into atomFamily() and selectorFamily

@drarmstr drarmstr closed this as completed Jun 1, 2020
@afitiskin
Copy link
Author

afitiskin commented Jun 1, 2020

@acutmore @drarmstr thank you guys for your answers! Unfortunately passing array or object as a parameter is not an option for my case. I'm working on recoil-form library, and I have the case that atomFamily may solve, but only with my proposed implementation.

Let's say we have a form with ability to set a default value for every field. So, for every field we have name and initialValue. Your suggestion is to use [name, initialValue] or { [name]: initialValue } as a parameter. But this way I'm unable to access the atom by only name without initialValue.

I may achieve my goal with lodash memoization:

export const getFieldInitialValueAtom = memoize((name, initialValue) => {
  key: `form/fields/${name}/initialValue`,
  default: value,
});

In example above atom will be memoized by name only, and may be accessed by name only:

const x = getFieldInitialValueAtom('email', '[email protected]'); // new atom is created with default value '[email protected]';

const y = getFieldInitialValueAtom('email'); // returned existing atom
// currently x and y are linked to the same atom

But with your suggestion this could not be achieved:

const getFieldInitialValueAtom = atomFamily({
  key: 'form/fields/initialValue',
  default: ([name, initialValue]) => initialValue,
});

const x = getFieldInitialValueAtom(['email', '[email protected]']); //  new atom is created with default value '[email protected]';

const y = getFieldInitialValueAtom(['email']); // new atom is created but without default value
// x and y are different atoms in this case!

@afitiskin
Copy link
Author

Another possible solution of my case is to allow to pass serialization function to convert passed object to key manually:

const getFieldInitialValueAtom = atomFamily({
  key: 'form/fields/initialValue',
  default: ([name, initialValue]) => initialValue,
  serialize: ([name, initialValue]) => name,
});

const x = getFieldInitialValueAtom(['email', '[email protected]']); //  new atom is created with default value '[email protected]';

const y = getFieldInitialValueAtom(['email']); // returned existing atom, because atom with key `'email'` is already created in the family

@acutmore
Copy link

acutmore commented Jun 1, 2020

Hi @afitiskin,

That use case makes sense. Maybe @drarmstr will consider it for Recoil.

As you say for now you can build this with your own implementation of AtomFamily that only memoizes based on the first argument.

@afitiskin
Copy link
Author

@acutmore yep, currently we have our own cache/memoization system, but with release of atomFamily I see that we may drop our implementation in favour of built-in recoil solution with proper memory management. I may submit a PR, since I already played with the recoil-code, implementation is very easy and straightforward.

@drarmstr drarmstr added the enhancement New feature or request label Jun 13, 2020
@drarmstr
Copy link
Contributor

drarmstr commented Jun 13, 2020

@afitiskin Ah, yes, I see. One parameter for scoping the atom with the key and another just to pass to the default function.

Note that you can't just trivially transfer the second param to the internal default selector, since that's also part of the memoized atom. So, when you got the atom a second time, the second parameter would actually be ignored. Instead of using a default selector, you could wrap the atom with a selector. At the end of the day you'd probably end up with a pattern similar to the helper we use. I don't think we published it as part of the utils, but here's the idea:

function evaluatingAtom<T, StoredType, P: ?Parameter>({
  get = () => (_, value) => value,
  set = () => (_, value) => value,
  cacheImplementation_UNSTABLE: cacheImplementation,
  ...opt
}: EvaluatingAtomOptions<T, StoredType, P>): P => RecoilState<T> {
  const myAtom = atom<StoredType>(opt);
  const mySelector = selectorFamily<T, P>({
    key: `${opt.key}__evaluation`,
    get: evaluationParams => ({get: getRecoilValue}) =>
      get(evaluationParams)({get: getRecoilValue}, getRecoilValue(myAtom)),
    set: evaluationParams => (
      {set: setRecoilValue, get: getRecoilValue},
      newValue,
    ) =>
      setRecoilValue<StoredType>(
        myAtom,
        newValue instanceof DefaultValue
          ? newValue
          : oldValue =>
              set(evaluationParams)({get: getRecoilValue}, newValue, oldValue),
      ),
    cacheImplementation_UNSTABLE:
      cacheImplementation && (() => cacheImplementation),
    dangerouslyAllowMutability: opt.dangerouslyAllowMutability,
  });
  return evaluationParams => mySelector(evaluationParams);
}

function evaluatingAtomFamily<
  T,
  StoredType,
  ScopeParameter: Parameter,
  EvaluationParameter,
>({
  get = () => (_, value): T => value,
  set = () => (_, value): StoredType => value,
  cacheImplementationForEvaluation_UNSTABLE: cacheImplementationForEvaluation,
  ...options
}: evaluatingAtomFamilyOptions<
  T,
  StoredType,
  ScopeParameter,
  EvaluationParameter,
>): (ScopeParameter, EvaluationParameter) => RecoilState<T> {
  const baseAtom: ScopeParameter => RecoilState<StoredType> = atomFamily(
    options,
  );

  // If there are get/set accessors associated with this atomFamily,
  // then construct a wrapping selector to perform those evaluations whenever
  // the atom is read or written.
  const evaluationSelector = selectorFamily<
    T,
    {scopeParam: ScopeParameter, evalParam: EvaluationParameter},
  >({
    key: `${options.key}__evaluation`,
    get: ({scopeParam, evalParam}) => ({get: getRecoilValue}) =>
      get(scopeParam, evalParam)(
        {get: getRecoilValue},
        getRecoilValue(baseAtom(scopeParam)),
      ),
    set: ({evalParam, scopeParam}) => (
      {set: setRecoilValue, get: getRecoilValue},
      newValue,
    ) =>
      setRecoilValue(
        baseAtom(scopeParam),
        newValue instanceof DefaultValue
          ? newValue
          : oldValue =>
              set(scopeParam, evalParam)(
                {get: getRecoilValue},
                newValue,
                oldValue,
              ),
      ),
    cacheImplementation_UNSTABLE: cacheImplementationForEvaluation,
  });

  return (scopeParam, evalParam) => evaluationSelector({scopeParam, evalParam});
}

With this you could use call-site-specific defaults:

const fieldInitialValueState = evaluatingAtomFamily({
  key: 'Field',
  default: undefined,
  get: (field, initialValue) => (_, value) => value ?? initialValue,
});

useRecoilValue(fieldInitialValueState('email', '[email protected]'));

@drarmstr drarmstr reopened this Jun 13, 2020
@drarmstr drarmstr changed the title Feature request: Additional parameters for parameterized atomFamily/selectorFamily Feature request: Second parameter for atomFamily passed to default function Jun 13, 2020
@cybervaldez
Copy link

I second this so we can compose more dynamically. Another idea is to allow us to get an atom/selector by key.

@cybervaldez
Copy link

```js-jsx
function evaluatingAtom<T, StoredType, P: ?Parameter>({
  get = () => (_, value) => value,
  set = () => (_, value) => value,
  cacheImplementation_UNSTABLE: cacheImplementation,
  ...opt
}: EvaluatingAtomOptions<T, StoredType, P>): P => RecoilState<T> {
  const myAtom = atom<StoredType>(opt);
  const mySelector = selectorFamily<T, P>({
    key: `${opt.key}__evaluation`,
    get: evaluationParams => ({get: getRecoilValue}) =>
      get(evaluationParams)({get: getRecoilValue}, getRecoilValue(myAtom)),
    set: evaluationParams => (
      {set: setRecoilValue, get: getRecoilValue},
      newValue,
    ) =>
      setRecoilValue<StoredType>(
        myAtom,
        newValue instanceof DefaultValue
          ? newValue
          : oldValue =>
              set(evaluationParams)({get: getRecoilValue}, newValue, oldValue),
      ),
    cacheImplementation_UNSTABLE:
      cacheImplementation && (() => cacheImplementation),
    dangerouslyAllowMutability: opt.dangerouslyAllowMutability,
  });
  return evaluationParams => mySelector(evaluationParams);
}

function evaluatingAtomFamily<
  T,
  StoredType,
  ScopeParameter: Parameter,
  EvaluationParameter,
>({
  get = () => (_, value): T => value,
  set = () => (_, value): StoredType => value,
  cacheImplementationForEvaluation_UNSTABLE: cacheImplementationForEvaluation,
  ...options
}: evaluatingAtomFamilyOptions<
  T,
  StoredType,
  ScopeParameter,
  EvaluationParameter,
>): (ScopeParameter, EvaluationParameter) => RecoilState<T> {
  const baseAtom: ScopeParameter => RecoilState<StoredType> = atomFamily(
    options,
  );

  // If there are get/set accessors associated with this atomFamily,
  // then construct a wrapping selector to perform those evaluations whenever
  // the atom is read or written.
  const evaluationSelector = selectorFamily<
    T,
    {scopeParam: ScopeParameter, evalParam: EvaluationParameter},
  >({
    key: `${options.key}__evaluation`,
    get: ({scopeParam, evalParam}) => ({get: getRecoilValue}) =>
      get(scopeParam, evalParam)(
        {get: getRecoilValue},
        getRecoilValue(baseAtom(scopeParam)),
      ),
    set: ({evalParam, scopeParam}) => (
      {set: setRecoilValue, get: getRecoilValue},
      newValue,
    ) =>
      setRecoilValue(
        baseAtom(scopeParam),
        newValue instanceof DefaultValue
          ? newValue
          : oldValue =>
              set(scopeParam, evalParam)(
                {get: getRecoilValue},
                newValue,
                oldValue,
              ),
      ),
    cacheImplementation_UNSTABLE: cacheImplementationForEvaluation,
  });

  return (scopeParam, evalParam) => evaluationSelector({scopeParam, evalParam});
}

This looks perfect! Any chance we can have this in the next release? I have no idea how to add this to my current project.

@drarmstr
Copy link
Contributor

@cybervaldez - you don't need this fancy helper, just wrap a selectorFamily around the atomFamily to take the default parameter.

@xuzhanhh
Copy link

xuzhanhh commented Jul 21, 2020

@drarmstr hey. I follow the document and split atom(todoList) into atom(todoIdList) and atomFamily(todo). But <TodoList /> will still use atomFamily(todo) when I try to add <TodoListFilters/>. That will cause rerender again! How can i avoid that rerender in <TodoList /> ?

demo: https://codesandbox.io/s/condescending-sinoussi-d3ck0?file=/src/App.js:2020-2035

@drarmstr
Copy link
Contributor

@xuzhanhh - <TodoList /> should re-render when you change the set of todo's, since it needs that to render each one. But, the individual <TodoItem />'s should only render changed ones based on the keys. Which re-render are you trying to avoid? Is this related to the second parameter request?

@xuzhanhh
Copy link

xuzhanhh commented Jul 23, 2020

@xuzhanhh - <TodoList /> should re-render when you change the set of todo's, since it needs that to render each one. But, the individual <TodoItem />'s should only render changed ones based on the keys. Which re-render are you trying to avoid? Is this related to the second parameter request?

@drarmstr I hope stop rerendering <TodoList /> when I changing text in <TodoItem /> because filteredTodoIdListState only subscribe todoIdlistFilterState todoIdListState and todoDoneState. And both of these atoms/selectors value are not change. Does it a duplicate of #314 ?

@drarmstr
Copy link
Contributor

@xuzhanhh - In your example <TodoList /> depends on filteredTodoIdListState, which depends on the actual todo item to know if it is done or not when filtering. So, it will re-render when that changes. #314 would help avoid some re-renders as an optimization if a todo is updated without changing the done state, but filteredTodoIdListState would still need to be recomputed. You can avoid this as well as <TodoList /> re-renders today by splitting the done state into a separate atomFamily from the todo content. If you have further issue, though, please open a new issue as this is unrelated to #231.

@devarsh
Copy link

devarsh commented Sep 2, 2020

@drarmstr could you please provide a working example to play with, I've looked through the sample code above but I'm not able to fully understand, how it is achieved. BTW awesome library eliminated so many issues I had working with Forms where context had to be updated frequently and always felt hacky.

@drarmstr
Copy link
Contributor

drarmstr commented Sep 3, 2020

Example usage of a parameter used for a default might look something like this:

const fieldState = evaluatingAtomFamily({
  key: 'MyState',
  get: (field, defaultValue) => ({get}, fieldValue) => fieldValue ?? defaultValue,
});


const x = useRecoilValue(fieldState('email', '[email protected]'));    

@brandoncc
Copy link

First off, thanks for this really cool library, it has great use cases!

This is proving to be the largest headache in my implementation of this library. I have a need for a dynamic default value for my family of atoms. My use case is a bunch of text editors on a page, each of which have their own content. My only other option is setting my state in a useEffect, which results in a double render on mount. It also requires an extra useEffect in my file, and an extra optional assignment in every render:

  let [textDocument, setTextDocument] = useRecoilState(data.id);
  if (textDocument === null) textDocument = data.content;

  useEffect(() => {
    setTextDocument(data.content);
  }, [data.content, setTextDocument]);

@drarmstr
Copy link
Contributor

drarmstr commented Jan 7, 2021

The evaluatingAtomFamily example above provides a way to use dynamic call-site-specific defaults for an atom family.

@iweurman
Copy link

iweurman commented Oct 9, 2021

const comp = (...args: any) => [...args].join('|');
const expand = (key: string) => key.split('|');

const someFunc = (a, b, c, d) => { }

const myState = atomFamily({
  key: 'myState',
  default: (args: string) => someFunc(...expand(args),
});

const [data, setData] = useRecoilState(myState(comp('hello', 123, 'nice', true);

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
None yet
Development

No branches or pull requests

8 participants