Exporting store as a singleton:
// Store.js
import { observable, computed } from 'mobx'
class Store {
@observable numClicks = 0
@computed get oddOrEven() {
return this.numClicks % 2 === 0 ? 'even' : 'odd'
}
}
const store = new Store()
export default store
... enables us to import and use the active store anywhere in the app as many times we want.
// Component.jsx
import store from Store
// use the global store in any component without passing it
// down through the chain of props from the root node
store.numClicks = 4
console.log(store.oddOrEven)
I am using this approach without problems for some time now. Are there any caveats I should look out for?
From a technical perspective there is nothing wrong with this. Quite some people use this approach. Personally I prefer using some dependency injection system or pass stores explicitly, so that you can create fresh store instances for each test you create, instead of having a single global store which needs to be reset after each test. It will help scaling your project.
There is one generic problem with exporting "stateful" objects - their singleton behavior relies on module caching. When you turn off module caching for any reason (hot-reloading etc), you will obtain different instance each time you import a module.
I think that caching in general should work only as an optimization tool, so I am not a fan o this approach.
Also your component is hardwired to a particular instance, so you can't easily switch it's implementation or provide instance with different state, that's usually handy for testing purposes (it can be done by messing up with module loading).
Notice the problem is transitive...if you would apply the same pattern for obtaining dependencies anywhere, you would end up with a system, where everything is a tightly coupled singleton.
In general objects/modules/functions should just define their dependencies and let the client code provide these dependencies, not to obtain them themselves.
EDIT: Btw, is module caching behavior somehow defined in ES6 modules standard?
I would like to use import ... statement to get my datasource into the component. Same way like all other dependencies are imported.
Maybe creating a singleton at the point of default export is really not the optimal place to do it.
How about (when created) automatically linking them to the root component (like <App /> or document.body) which would be garbage collected last?
This way we never lose all memory pointers to our less often used singletons, which could be used only by rare components that exist only 10% of the application runtime, but their state must not be lost to garbage collector in the remaining 90% of the time while application is running and those components do not exist.
// ComponentA
import Store
// first time called, create named singleton 'gui'
const storeToolbar = Store.get('gui')
// ComponentB
import Store
// generates a new singleton
const storeAlerts = Store.get('alerts')
//provides the same singleton that was created in ComponentA
const storeToolbar = Store.get('gui')
// drop links to named singleton stores for easier test setup
// singletons will be unlinked and garbage collected after
// this function ends, next get will create a new store
if (testing) Store.drop('alerts')
if (testing) Store.drop('gui')
This decouples all components as they are _listening to a channel_ they .get(, and are simply _tuning in_ when calling the import by themselves.
We could also call this component interprocess communication, because it acts like a bus for all components interested in the same datasource.
This is basically Service locator pattern
Coincidentally, there was an issue a sec ago about server side rendering (#607), which implies another problem you may encounter when using module caching as a mean for singleton implementation.
When you run your code on the server, your singletons will be shared by all requests. (Note that removing the singleton module from cache at every request woudn't help, because request processing can be asynchronous)
Would storing a pointer to the store to a property of the root nodes ref solve this?
No, it doesn't matter where you store the returned instance of store.
The problem is that import store from Store imports the same instance of store for the whole lifetime of application (or to be precise the whole lifetime of module cache).
That's fine if you run the code on the client side, because the application lifetime is the the same as the intended store lifetime, which is a single page request.
However on the server, the application lifetime and thereby lifetime of imported module, spans over multiple requests.
In other words you can't control a "context" of singleton, the context of such singletons is always the module cache, which lifetime differs on the client and on the server.
Btw refs is for storing references to rendered DOM Elements, you don't want to store anything to the elements, as they came and go as the components are being updated.
We use global singletons on the client side for application and business logic (MC from MVC) but not for React components (V from MVC), we pass it to components via props from root components. We decided that the import of singletons into components greatly complicates their support, debugging and testing.
@urugator I'd use refs only on the root node of the application, to protect the store from garbage collector. This could work only on the client side, because server doesn't have a dom. Maybe storing the state in something similar to a cookie jar would work to bridge sessions on the server.
@VolCh If I understand you correctly, you're saying that all things touching the store are functions existing outside of React components. This way all components become pure functions? Because this is exactly the way how I approach it.
Do you anchor your singletons to any public object (like document.body or the ref of app root node)?
Unfortunately I really don't see how persisting state or garbage collector concerns relate to the problem.
To get a better picture, I will just try to explain what I currently do:
I have a class App representing whole client app, it instantiates and holds all the stores. It's constructor accepts an object state, which is just a serializable object, representing initial state of the client application.
There is a root react component accepting an instance of App as a prop.
So the whole client code looks roughly like this:
const App = require("client/App");
const AppRootCmp = require("client/AppRootCmp");
const app = new App(window.state);
ReactDOM.render(
React.createElement(AppRootCmp, { app: app })
document.getElementById("app")
);
The server side rendering code looks nearly the same, only the state is not taken from window.state and ReactDOMServer.renderToString is used instead.
The stores are simply passed to components through props:
const AppRootCmp = (props) => React.DOM.div(null
,React.createElement(CmpA, { storeA: this.props.app.storeA })
,React.createElement(CmpB, { storeB: this.props.app.storeB, storeC: this.props.app.storeC })
);
You could also simply pass the whole app to every component as a single dependency and use it as a kind of service locator (or use react context). However that's not very clean solution.
I don't know about any way to make something "global" in isomorphic app, so everything must be passed (through props or args).
Maybe with precompiled sources you could have a global namespace, which would be local for the whole compiled "package". However I think it would complicate everything even more.
Closing this question, I think it doesn't really relate to MobX anymore, but more to general app design :)
From a technical perspective there is nothing wrong with this. Quite some people use this approach. Personally I prefer using some dependency injection system or pass stores explicitly, so that you can create fresh store instances for each test you create, instead of having a single global store which needs to be reset after each test. It will help scaling your project.
can you supply an example to this approach?
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions.
Most helpful comment
From a technical perspective there is nothing wrong with this. Quite some people use this approach. Personally I prefer using some dependency injection system or pass stores explicitly, so that you can create fresh store instances for each test you create, instead of having a single global store which needs to be reset after each test. It will help scaling your project.