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);
}
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.
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.