Closed mark0978 closed 4 years ago
This is the best I've come up with so far, but I'm not really all that thrilled with the component dealing with the fetch....
function _StoryLinkFromId(props) {
// When displaying inspired_by or next_chapter, sometimes all we have is a link, since these
// are likely destinations we will go ahead and try to load them so they are ready for the
// user. In the meantime we just show the url since it is probably below the fold anyway
// and once we have the actual story in the cache, we replace that link with the normal one
const story = props.stories.cache[props.id];
if (story) {
return <StoryLink story={story} />;
}
props.doFetchStory(props.id);
const url = makeStoryUrl(props.id);
return (
<NavLink to={url} className="story__link_id">
{props.alternate_text || url}
</NavLink>
);
}
const StoryLinkFromId = connect(
'selectStories',
'doFetchStory',
_StoryLinkFromId
);
This is on top of exposing the structure of the story cache to the component......
Hey @mark0978!
This is a variation of a common question and yeah, it's a bit hairy to do this in a way that lets you keep fetching logic out of the component. But, personally, I think it's worth it. Especially when combined with caching you can make it so that you have an growing cache of fetched stories. Additionally, if you've already fetched and added items to your story cache you'd already have them by the time a user clicked 'em.
The main story ID is in the URL already, it sounds like. So first you need a selector that grabs the story Id from the URL, and one that grabs the corresponding story ID from cache. Something like this:
selectActiveStoryId: createSelector(
'selectUrlParams',
params => params.storyId
),
// i often make one that returns the "raw" story that object including metadata about
// whether I'm currently fetching it or not, it may not have data yet:
// it may return something like: {loading: true, data: null}
// this can be used in a reactor to determine whether or not we need to
// fetch it.
selectActiveStoryRaw: createSelector(
'selectCachedStoriesById', // something that returns an object of cached stories keyed by story id
'selectActiveStoryId',
(stories, id) => stories[id]
),
// now we can have a selector that only returns the data of the active story if we have it
selectActiveStoryData: createSelector(
'selectActiveStoryRaw'
storyObjectRaw => storyObjectRaw.data
),
// extract other stories we may need to fetch
selectAllActiveStoryIds: createSelector(
'selectActiveStoryData',
(data, storiesById) => {
if (!data) {
return null
}
return data.subStories.map(item => item.id)
}
),
// now we can write a reactor that fetches the ones
// we don't already have and aren't already loading
reactFetchSubStories: createSelector(
'selectAllActiveStoryIds',
'selectCachedStoriesById'
(ids, storyObjects) => {
if (!ids || !ids.length) {
return
}
const idsToFetch = ids.filter(id => {
const matchingStoryObj = storyObjects[id]
if (!matchingStoryObject) {
return true
}
return !matchingStoryObject.data && !matchingStoryObject.loading
})
if (idsToFetch.length) {
// it's just important that the action creator here immediately dispatches a
// "started" action that creates an entry in the reducer keyed by id with "loading: true"
return { actionCreator: 'doFetchStories', args: [idsToFetch] }
}
}
)
// at the point where all this exists you can write a single selector for the
// view that just combines all the available data (including sub stories)
selectActiveStory: createSelector(
'selectActiveStoryRaw',
'selectCachedStoriesById',
(activeStoryRaw, cachedStories) => {
if (!activeStoryRaw) {
return null
}
// you could do this to show a loading state
// for the root story if you wanted
if (activeStoryRaw.loading) {
return {loading: true}
}
// extract all the sub stories
// note that some of these may be .loading === true too
// so if you want you should show that in the UI
// they would just pop in once loaded
const subStories = activeStoryRaw.data.subStories.map(id => cachedStories[id])
return Object.assign({}, activeStoryRaw.data, {subStories})
)
After all that, now you can write a stateless view component that just uses selectActiveStory
to render all this stuff.
I'm going to close this one. Just doing some cleanup
I'd like to be able to use the id to grab the story from the state, and at the same time trigger a load of that story if it does not exist locally. I'm having a hard time figuring out how to do this without passing the
prop.id
down into a selector either as a prop or an argument. How do I cause an auto fetch based on theprop.id
in this case?Stories are fetched based on the URL, but they contain links to other stories/chapters and those links might need to fetch a story as well to make a prettier link (using the story title). I'm trying to keep all the auto fetch stuff in the store and NOT in the components but I'm having a devil of time. I've done things like:
and then I can do
What I want is the detection down in the store that stories[id] is undefined and a reaction within the store (similar to what loads the story to display) would cause it to load, but since I can never get
props.id
down into the select mechanism I don't have a good way to trigger this auto loading...