Recoil: Providing an external context to atoms / selectors

Created on 31 May 2020  路  3Comments  路  Source: facebookexperimental/Recoil

Currently, atoms and selectors can be initialized asynchronously and that works like a charm in simple cases like fetching a post list from some URL. But what if there will be different backend implementations? Like this:

interface AudioplayerBackend {
   getTrack(trackID: string) => Promise<Track>;
   search(searchString: string) => Promise<Track[]>;
   //...so on
}

class FilesystemBackend implements AudioplayerBackend {
....
}

class YoutubeBackend implements AudioplayerBackend {
....
}

const searchResults = atom({
    key: 'searchResults',
    // how do I use some custom AudioplayerBackend implementation here without 
    // directly requiring it?
    get: ({get}) => ???
}

So, can we add a custom Context to Recoil hooks so it was passed to atoms / selectors, like this?

const searchResults = atom({
    key: 'searchResults',
    get: ({get, customContext}) => customContext.search(....)
});



const MyApp = () => {
   const [backend] = useState(() => new YoutubeBackend());
  // so we pass an instance of YoutubeBackend through custom context to the atoms here
   return <RecoilRoot customContext={backend|>.....</RecoilRoot>;
}

const MySearchComponent = () => {
     /// we get an instance of YoutubeBackend here somehow
    const customContext = useYoutubeBackend();
    /// And pass it to the hook to provide it as custom context
    const atomValue = useRecoilValue(searchResults, customContext);
}

question

Most helpful comment

Hi @karevn

You can have multiple <RecoilRoot>s in an app. This would give you a way to have atoms/selector with different values in different parts of your app.

const backendToUse = atom({
  key: "backendToUse",
  default: FilesystemBackend,
});

const initaliseResults = selector({
  key: "initaliseSearchResults",
  get({ get }) {
    const backend = get(backendToUse);
    return backend.read();
  },
});

const results = atom({
  key: "results",
  default: initaliseResults,
});

function App() {
  return (
    <>
      <RecoilRoot
        initializeState={({ set }) => {
          set(backendToUse, YoutubeBackend);
        }}
      >
        <Component1 />
      </RecoilRoot>
      <RecoilRoot
        initializeState={({ set }) => {
          set(backendToUse, AnotherBackend);
        }}
      >
        <Component2 />
      </RecoilRoot>
    </>
  );
}

All 3 comments

Hi @karevn

You can have multiple <RecoilRoot>s in an app. This would give you a way to have atoms/selector with different values in different parts of your app.

const backendToUse = atom({
  key: "backendToUse",
  default: FilesystemBackend,
});

const initaliseResults = selector({
  key: "initaliseSearchResults",
  get({ get }) {
    const backend = get(backendToUse);
    return backend.read();
  },
});

const results = atom({
  key: "results",
  default: initaliseResults,
});

function App() {
  return (
    <>
      <RecoilRoot
        initializeState={({ set }) => {
          set(backendToUse, YoutubeBackend);
        }}
      >
        <Component1 />
      </RecoilRoot>
      <RecoilRoot
        initializeState={({ set }) => {
          set(backendToUse, AnotherBackend);
        }}
      >
        <Component2 />
      </RecoilRoot>
    </>
  );
}

Thanks for your idea, that should work fine for this illustrative example. But IMO it looks more like a workaround and would create an additional atom derived from some other state, which increases a surface for bugs.

@karevn - From your example, it appears that you are making this decision based on variables at the callsite. Could you create two selectors for each backend?

const FilesystemSearchQuery = selector({
  key: 'Search/Filesystem',
  get: ({get}) => FilesystemBackend.search(get(searchState)),
});

const YouTubeSearchQuery = selector({
  key: 'Search/YouTube',
  get: ({get}) => YouTubeBackend.search(get(searchState)),
});

Or, use a selectorFamily to parameterize it for the search string and your interface is of type string => RecoilValueReadOnly<Array<Track>>

const FilesystemSearchQuery = selectorFamily<string, Array<Track>>({
  key: 'Search/Filesystem',
  get: searchStr => () => FilesystemBackend.search(searchStr),
});

const YouTubeSearchQuery = selectorFamily({
  key: 'Search/YouTube',
  get: searchStr => () => YouTubeBackend.search(searchStr),
});

Or, could you use the selectorFamily to parameterize which back-end to use (though note the restrictions on what you can use in parameters)

const seachQuery = selectorFamily({
  key: 'SearchQuery',
  get: ({searchStr, backend}) => () => backends[backend].search(searchStr),
});

The backend parameter could be obtained from React props, another atom, React context, &c.

Was this page helpful?
0 / 5 - 0 ratings