This isn't a bug report, but something I wanted to discuss because I am not happy with getInitialData in real apps.
Let's consider a simple blog/post app, where routes looks like this:
export default {
path: '/blog',
component: Blog,
children: [
{ path: '', component: BlogIndex },
{ path: ':post_id', component: BlogPost }
]
}
so /blog renders the blog index, and /blog/123 renders a post, and they both share the same layout.
In a prototype, Blog doesn't even need getInitialData, BlogIndex will have something like:
export default {
async getInitialData () {
return { posts: await axios.get(`/api/posts`) }
}
}
and BlogPost will have:
export default {
async getInitialData ({ route }) {
return { post: await axios.get(`/api/posts/${route.params.post_id}`) }
}
}
So far so good. Now let's see how the prototype scales to a real world scenario:
Blog needs to asynchronously pull blog config, which includes data that it needs to render and something that child routes will need. For example, { name: 'My Blog', postsPerPage: 10 }
BlogPost needs to know the value of postsPerPage in its getInitialData to construct the API request: await axios.get(..., { ... perPage })
BlogPost needs to tell Blog which post title to render in <h1> (or which category to mark as "current", etc.)
Generalizing, the components in parent/child chain need to push/pull data along the chain. Let's outline the 4 possible scenarios:
- Access data coming from parent
getInitialData in child getInitialData
- Access data coming from child
getInitialData in parent getInitialData
- Synchronously access data from parent's
getInitialData in child template
- Synchronously access data from child's
getInitialData in parent template
1 and 2 are not currently possible as the two getInitialData for parent and child run in parallel with Promise.all:
https://github.com/ream/ream/blob/874aa8ef9968b8c77480f3084ef30a99f03e9005/app/server-entry.js#L63 (in theory it can be achieved by using something like a shared promise in context, with looped setTimeout to deal with the race condition, but it's unacceptable architecture-wise).
3 is possible with {{ $parent.$parent.xxx }}, not very elegant, but acceptable.
4 is possible with this hack in BlogPost:
export default {
async getInitialData ({ route }) {
const post = await axios.get(...)
route.params.title = post.title
return { post }
}
}
and this in Blog:
export default {
beforeCreate () {
if (process.server) {
// Save route params set by a child route in SSR context
// this.$initialData will be pushed to this.$data by Ream
this.$initialData.title = this.$route.params.title
} else {
// Do nothing.
// On client, this.$initialData has already been restored from window.__REAM__, and custom $route.params.xxx are NOT set!
}
}
}
which is quite cumbersome.
What I'm thinking (I didn't do much thinking though) is that instead of doing Promise.all, the children getInitialData's could be called sequentially, and include all parents' data in context or as additional parameters. For the blog example, that would be:
Blog:
export default {
async getInitialData () {
const config = await axios.get('config')
return { config }
}
}
and then - sequentially - BlogPost which may both access parent data and push something else to it:
export default {
async getInitialData ({ route }, blogData) {
const post = await axios.get(..., { perPage: blogData.config.postsPerpage } )
blogData.title = post.title
return { post }
}
}
This will fix data flow scenarios 1, 4, and possibly simplify 3. Scenario 2 will still not be possible but it is the least natural data flow anyway.
If BlogPost had its own child component, that component's getInitialData would be called as getInitialData(context, blogData, blogPostData).
The proposed improve will be fully backwards compatible.
Thoughts?
This isn't a bug report, but something I wanted to discuss because I am not happy with
getInitialDatain real apps.Let's consider a simple blog/post app, where routes looks like this:
so
/blogrenders the blog index, and/blog/123renders a post, and they both share the same layout.In a prototype,
Blogdoesn't even needgetInitialData,BlogIndexwill have something like:and
BlogPostwill have:So far so good. Now let's see how the prototype scales to a real world scenario:
Blogneeds to asynchronously pull blog config, which includes data that it needs to render and something that child routes will need. For example,{ name: 'My Blog', postsPerPage: 10 }BlogPostneeds to know the value ofpostsPerPagein itsgetInitialDatato construct the API request:await axios.get(..., { ... perPage })BlogPostneeds to tellBlogwhich post title to render in<h1>(or which category to mark as "current", etc.)Generalizing, the components in parent/child chain need to push/pull data along the chain. Let's outline the 4 possible scenarios:
getInitialDatain childgetInitialDatagetInitialDatain parentgetInitialDatagetInitialDatain child templategetInitialDatain parent template1 and 2 are not currently possible as the two
getInitialDatafor parent and child run in parallel withPromise.all:https://github.com/ream/ream/blob/874aa8ef9968b8c77480f3084ef30a99f03e9005/app/server-entry.js#L63 (in theory it can be achieved by using something like a shared promise in
context, with loopedsetTimeoutto deal with the race condition, but it's unacceptable architecture-wise).3 is possible with
{{ $parent.$parent.xxx }}, not very elegant, but acceptable.4 is possible with this hack in
BlogPost:and this in
Blog:which is quite cumbersome.
What I'm thinking (I didn't do much thinking though) is that instead of doing
Promise.all, the childrengetInitialData's could be called sequentially, and include all parents' data in context or as additional parameters. For the blog example, that would be:Blog:and then - sequentially -
BlogPostwhich may both access parent data and push something else to it:This will fix data flow scenarios 1, 4, and possibly simplify 3. Scenario 2 will still not be possible but it is the least natural data flow anyway.
If
BlogPosthad its own child component, that component'sgetInitialDatawould be called asgetInitialData(context, blogData, blogPostData).The proposed improve will be fully backwards compatible.
Thoughts?