Recoil: [Request] move key field from public API to optional argument

Created on 7 Jul 2020  路  9Comments  路  Source: facebookexperimental/Recoil

During of Recoil usage I faced with unnecessary boilerplate in key field for atom or selector for state with static root.

For the project it looks like creating of additnional constants.
image

From API-usage point it looks like: "I need to define API unique required filed as public for private API goals".
In other words for common api usage the key is redundant.

The request is next:
Move key field out and setup it within the Recoil. For dynamic usage like dynamicKey_${id} it could be placed to the second argument as optional.
For other metadata and params not related to default the same operation could be performed

In case it it could be he result could be next:

// before
const atomState = atom({
  key: 'uniqueAtomState',
  default: 'defaultStateValue'
});

const selectorState = selector({
  key: 'uniqueSelectorState',
  get: ({get}) => get(atomState)
});

// after
const atomState = atom('defaultStateValue');
const selectorState = selector({
  get: ({get}) => get(atomState)
});

in case if I key is need to be used for dynamic atoms we can move it out to the second argument as optional.

const atomState = atom('defaultStateValue'); // without public defined key

const atomState = atom('defaultStateValue', 'uniqueAtomState'); // with public defined key as string
const atomState = atom('defaultStateValue', {key: 'uniqueAtomState'}); // with public defined key as an object (good way if API will be extended with more option values)

For the last case for implementation the adding of private isPrivateKey: boolean field to each atom/selector could be a good way to define how key was defined.

All 9 comments

request is updated to dynamic option of the keys from remove/replace to move to optional arguments
based on Recoil presentation video: https://www.youtube.com/watch?v=_ISAA_Jt9kI&feature=youtu.be&t=595

Agreed, forcing the user to set and remember strings for each atom is too much to ask, in addition the user is going to have to remember all of the magic strings used for keys in their application since it has to be unique, which is going to cause mad problems once the user's application grows beyond a todo list. Recommend use guid for key and then add optional name / label field if desired for debugging purposes

I disagree with a number of points here.

First, a string constant barely meets the specification what I would consider "unnecessary boilerplate".

Your point that

I need to define API unique required filed as public for private API goals

is misleading, as there's no reason as to why you should be exporting private API in your public modules. This library gives no exception to this rule.

Using a key (or unique identifier) is a very common pattern for state management libraries. See: useReducer (react), actions from redux, or types.identifier from mobx-state-tree. Using a generated id for these unique identifiers is not an option - say you add a duplicate object to your storage tree, and then the old object now has a different ID. If you use something like codepush to update your app and rely upon the shape of state staying consistent between versions, this would break that consistency.

the user is going to have to remember all of the magic strings used for keys in their application since it has to be unique

I may be misinformed but my understanding is that you operate on the created atoms and selectors of this library, not the unique keys used to create them. This being the case, there's no reason to "remember" (import/export) string constants to use this library. It's up to you to determine how you manage constants/types in your application.

@immackay hi, related to the first point. Recoil and Redux are pretty different to compare them and their approaches. Moreover redux is independent 3rdparty-library which means its API is over flexable and open to integrate with different libs (of course it increase amount of boilerplates will be used).

About Redux - I hope you mentioned actions in reducers with their unique type. If so yes, it's their common patter to split the changes and maintain the reducer as pure function.
And most important what in reducers the unique action's type defines the way how exactly state will be changed.

In recoil case in mostly cases user will only subscribe to the state (or calculation between delived state for selectors) and each state itself is unique.

// redux
const reducer  = (state, {type, payload}) => getActionHandler(type)(state, payload); 

// recoil
useRecoilValue(recoilState) // components 
//or 
get: ({get}) => get(recoilState) // selectors

In other words I did not see any point to compare different approaches and link to redux as good sample (All of us know how many boilerplate it contains e.g. their type);

Only one way it could be for recoil - is dynamic keys like const getStateById = (id) => atom({key: id, default: false}) but for this case moving keys to the optional args looks pretty native const getStateById = (id) => atom(false, id)

// existed API
const myUniqKey = 'myUniqKey';
const myState = atom({key: myUniqKey, default: false})

// proposed
const myState = atom(false);

// and just to clarify how it could be out of the box
const atomWithoutKey: <T> = (default: T) => {
  const key: string = soomehowGenerateUniquePrvateKey(); // handled by recoil
  return atom<T>({key, default})
}

I think you missed the point of my reply.

Using a generated identifier can cause state inconsistency between different codebase versions.

Here's a contrived example:

Say you've got a function that generates an id. For sake of simplicity we'll use the hash of the passed object, with an integer suffix indicating the count of the given object in the state tree.

const objectCountMap = new Map<string, number>();

const getID = (something: any) => {
  const objectHash = hash(JSON.stringify(something)); // any hash function
  let count = objectCountMap.get(objectHash) ?? -1;
  objectCountMap.set(objectHash, ++count);

  return `${objectHash}${count}`;
};

And with this getID function, you instantiate some state in your app like so:

const exampleObject = { hello: 'world' }

const state1 = atomWithoutKey(exampleObject); // => { key: 'somehash0' }
const state2 = atomWithoutKey(exampleObject); // => { key: 'somehash1' }

const App = () => {
  const [foo, setFoo] = useRecoilState(state1);
  const [bar, setBar] = useRecoilState(state2);

  return ...
}

Using this example, you would have state1 with a key of somehash0, and state2 with a key of somehash1. Now, imagine your application state has been saved in some way, let's say you're using this in a codepush-enabled react-native app and you've serialized your state to JSON and saved it to AsyncStorage.

What happens if you want to update your app with another atom... but you modify the order of creation?

Eg:

const exampleObject = { hello: 'world' }

const state1 = atomWithoutKey(exampleObject); // => { key: 'somehash0' }
const breakingObject = atomWithoutKey(exampleObject); // => ?
const state2 = atomWithoutKey(exampleObject); // => ?

const App = () => {
  const [foo, setFoo] = useRecoilState(state1);
  const [bar, setBar] = useRecoilState(state2);

  const [breaking, setBreaking] = useRecoilState(breakingObject);

  return ...
}

Obviously the method used to generate an identifier could be much more intelligent, but it doesn't matter.

The point is good and extra complicated. As I mentioned early, common usage means work with recoil as a state-management system like init states / subscribe and update them during app's live.

Anyway if order of atoms are changed it could be e.g. like snapshot testing (update affected deps).

In AsyncStorage example - it could be transformed to default task like comparation of version to reset/update/seed necessary data.

And always for your particular case you could still use keys as I proposed for dynamic atoms const atomState = atom('defaultStateValue', 'uniqueAtomStateKey');

The request is just to let end-user chooses how to use recoil. And it will match to already implemented way.
you need only value ? useRecoilValue
you need only setter ? useSetRecoilState
you need both ? useRecoilState

and the same for atoms/selectors
do you expect to use atom identifiers ? atom
do you expect to use atom as state only ? atomWithoutKey

__

btw recoil uses key in Map storage what means default value could be used instead like Symbol(default)

I think perhaps a solution I would be comfortable with is allowing keyless atom instantiation a maximum of once for a given hash, then erroring and enforcing a defined key for duplicate objects after that hash is encountered again.

Eg:

const foo = {}
const state1 = atomWithoutKey(foo);
const state2 = atomWithoutKey(foo); // Error: key `foo` is already defined in the state tree
const state1 = atomWithoutKey(foo);
const state2 = atom('bar', foo); // works

This would effectively remove the ordering problem I previously described (I believe).

Edit: this would however greatly decrease the benefits of this suggestion, as using the same default value for multiple states across an app would require unique keys

Edit 2: see #32 and #378 for additional discussion on this issue

hey, my friend, maybe you can have a look and try concent, 鉂わ笍 build-in dependency collection, a predictable銆亃ero-cost-use銆乸rogressive銆乭igh performance's react develop framework.

here is a js online example.

@immackay What if Recoil's default API did not require a key at all (and generated one underneath as others have suggested), and then you created another function atomWithKey(...) that matches your current API, and then a build time plugin could be created to convert calls of atom to atomWithKey with a build time generated key based on a hash of some kind. You could also add a displayName value that is set at build time to the name of the variable declaration to be used in dev tooling.

This could at the very least maintain key names between reloads for the same build, and maybe longer, depending on what hash is used, for those using the build time plugin.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamiebuilds picture jamiebuilds  路  3Comments

Etherum7 picture Etherum7  路  3Comments

karevn picture karevn  路  3Comments

thegauravthakur picture thegauravthakur  路  3Comments

ibnumusyaffa picture ibnumusyaffa  路  4Comments