Skip to content

Instantly share code, notes, and snippets.

@ryedin
Last active December 11, 2017 18:36
Show Gist options
  • Save ryedin/6efcc8c0883bb2b1baf4d9a2f19ef105 to your computer and use it in GitHub Desktop.
Save ryedin/6efcc8c0883bb2b1baf4d9a2f19ef105 to your computer and use it in GitHub Desktop.
Example of single "app" constructed from multiple `Html.React` calls
@using React.Web.Mvc
@using SomeFramework.Mvc.AmazingHTMLHelpers
@model MyNamespace.Partials.SomeSpecialViewModel
<!-- since this partial relies on magic "SomeFramework" mechanisms to get its data,
the path of least resistance to get all of this working is to just use the Razor
engine as "SomeFramework" expects it to work, call its special Html helper extension,
and plug the resulting data into our `RegisterViewData` component. This means that
1. future calls to Html.React (within THIS REQUEST) will have this data available and our SSR will be accurate
2. this data will get nicely hydrated on the client side to setup the initial view state
-->
@Html.React("Components.RegisterViewData", new {
viewName = "SomeComponentOneViewData",
data = Html.WizzBangHolySmokesAmazingHtmlHelperMethodThatReliesOnASpecificViewModelInterface(Model)
})
import React, { Component } from 'react'
import { Provider, observer } from 'mobx-react'
import { viewDataStore } from './stores'
import { SomeComponentOne } from './SomeComponentOne'
@observer
class App extends Component {
render() {
return (
<Provider viewDataStore={viewDataStore}>
//The SomeComponentOne component will render, grab the store from context (via the injection semantics)
//and potentially see no data the first render cycle. This is normally (i.e. on the client) OK, because
//our store is "reactive", and will auto-re-render any components that care about its data. This of course
//falls down on the server because the context across Html.React calls is destroyed. There is no concept
//of a "re-render", either, because the components are not considered "live"; they are simply mounted and rendered
//out as a static string with no real notion of being inside a larger app context, with potentially injected
//data.
//Also, it helps if you imagine that SomeComponentOne is comprised of many levels of nested
//components which would make it really unwieldy to force all props to be defined and pushed
//down from this level (which would mean you'd need to expose every piece
//of data that you want to be available for your entire app and all of its children components
//if you want to achieve accurate SSR and gain SEO benefits, etc...)
<SomeComponentOne />
</Provider>
)
}
}
export { App }
import React, { Component } from 'react'
import { viewDataStore } from './stores'
class RegisterViewData extends Component {
/**
* when this component is mounting, grab the view data props and add them
* to the ViewDataStore singleton instance. once this happens, other components
* will be able to respond as needed to render the data.
*/
componentWillMount() {
const { viewName, data } = this.props
viewDataStore.setViewData(viewName, data)
}
render() {
//this is a non-visual component so simply render nothing
return null
}
}
export { RegisterViewData }
import React, { Component } from 'react'
import { inject, observer } form 'mobx-react'
/**
* Again, it helps to imagine that this component is actually composed of many levels
* of nested children, or that it is actually itself nested deeply within a larger
* component hierarchy. One in which the data that it needs is really only needed to be
* known at the top-level and this level (and having to push the data down all the levels
* of children is basically untenable), AND the data may or may not be present the first time this component
* is mounted and rendered. On the client, this is just fine (some stores might have the data right away
* and some might require async fetching to get populated). On the server, we'd _like_ for this to be fine as
* well, because it would greatly increase the flexibility of how we compose the higher level application.
* As of now, we can only approximate this by turning off engine pooling and making sure our Html.React calls
* happen in the right order (ordinality of these calls also wouldn't make a difference, if we were dealing in
* "live" components that have a chance to re-render before the final HTML string is created)
*/
@inject('viewDataStore')
@observer
class SomeComponentOne extends Component {
render() {
const { viewDataStore } = this.props
const myViewData = viewDataStore.views.get('SomeComponentOneViewData')
if (myViewData) {
return (
<div>
some data from the store: {myViewData.aPieceOfDataLivesHere}
</div>
)
} else {
//data isn't here yet, no worries, just render nothing.
//NOTE: if a re-render never happens before the final string building happens,
//our SSR rendering is broken (we lose SEO and other benefits for this view).
return null
}
}
}
export { SomeComponentOne }
@using React.Web.Mvc
@model MyNamepace.Pages.SomePageModel
<!-- blah blah blah, meta, head, bundles, etc... -->
<!-- ok here's where it gets interesting. We are using a CMS Framework,
we'll call it "SomeFramework", that has its own Html helper extension methods,
and relies heavily on the Razor engine to recursively expand html fragments and
other data. Those helper methods require to be run within the context of a view
or partial whose model may vary wildly from the model I am in right now.
No worries, we're in a single request, and that means a single js execution context,
which means those other partials can simply make calls to our `RegisterViewData`
react component, and then our top-level `App` component can still nicely be composed
without having to manually maintain a huge list of props and push them down the chain.
The "maintain" word is key here, as if that _were_ the case, it would be a nightmare
any time we wanted to re-structure pieces of our application -->
<!-- use normal partial semantics to get view data render calls without trying to go against
the grain of "SomeFramework" -->
@Html.Partial("Some/Path/To/_MyPartial")
<!-- finally, render our page-level react "App" -->
@Html.React("Components.App", new {
someTopLevelPropThatOnlyAppCaresAbout = "hello, world"
})
import { action, ObservableMap } from 'mobx'
class ViewDataStore {
views = new ObservableMap()
@action setViewData(viewName, data) {
this.views.set(viewName, data)
}
}
//right now, this store is going to be completely empty, which is _fine_ normally
//(client side, thanks to mobx, as data arrives any components that care will be
// automatically re-rendered... yay!)
const viewDataStore = new ViewDataStore()
//export the singleton instance because we want multiple calls to `Html.React` to share this
//store so that the mobx reactions can take place as expected, _even_ if our "app" is comprised
//of multiple mounted react "roots".
//NOTE: this is where the default engine pooling semantics fall down for us.
//Also, since there is not "live"ness to the components on the server, we need to make sure we
//render out all calls to the "RegisterViewData" component _before_ the call to the app-level component
//that wants to consume this data. It sort of defeats at least one reason for using reactive stores and
//dependency injection, but that's what we have to do to make this work for now.
export { viewDataStore }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment