easy-peasy
easy-peasy copied to clipboard
[Proposal] Lazy models
The addModel and removeModel API doesn't feel as integrated as the rest of the APIs. This proposal will detail an inline approach of declaring your code-split/lazy-loaded models.
Details coming soon.
My proposal for this would be "wrapping" those 2 functions as in:
instead of
import { StoreProvider } from 'easy-peasy'
we would do
import { Store } from 'easy-peasy'
<Store.Provider store={store}>
...
</Store.Provider>
and on a code-splitted module deep in the react tree....
import { Store } from 'easy-peasy'
<Store.Model model={model}>
...
</Store.Model>
where model would be something as
{
[modelName] : {
...modelDefinition
}
}
Store.Model would have a useEffect hook that runs only once, calling "store.addModel" for each key of "model" prop, and on the cleanup of the useEffect, calling "store.removeModel" for each key of the "model" prop :)
if you dont like "Store" with Provider and Model components
we can keep StoreProvider and add StoreModel
@ctrlplusb what do you think?
Very interesting API @kamikazePT 💜
Microsoft did something similar to @kamikazePT 's suggestion for redux: https://github.com/microsoft/redux-dynamic-modules
There's also redux-injectors that seems like it will be easy enough to port https://github.com/react-boilerplate/redux-injectors
So here is the API I imagined.
Defining a lazy model
import { createStore, lazy } from 'easy-peasy';
// 👆
import todosModel from './models/todo';
const store = createStore({
todos: todosModel,
// We define a lazy model similar to how you define lazy react components 😊
// 👇
products: lazy(() => import('./models/products')),
});
Loading a lazy model
import { LoadLazyModel } from 'easy-peasy';
function ProductsSection() {
return (
<LoadLazyModel target={model => model.products}>
<ProductsList />
</LoadLazyModel>
)
}
Using a loaded lazy model
Once it is loaded you can use it as normal.
import { useStoreState } from 'easy-peasy';
function ProductDetails({ id }) {
const product = useStoreState(state => state.products.byId[id], [id]);
return (
<>
<h1>{product.title}</h1>
{/* ... */}
</>
)
}
What I really like about this design is that it keeps the model within the actual store model definition. It becomes a lot easier to imagine and reason about the store state. It also opens up nice opportunity for me to maintain types.
@kamikazePT @dan-dr
@ctrlplusb
The only thing I dont like that much about that approach is that you are still obligated to know at the root level about the entire app state model
I would like to be able to further down the react tree be able to mount/unmount slices of state
what about this?
import { lazy } from 'easy-peasy';
const ProductsModel = lazy(() => import('./models/products'))
// and then
function ProductsSection() {
return (
<ProductsModel>
<ProductsList />
</ProductsModel>
)
}
// or
function ProductsSection() {
return (
<ProductsModel keepMounted>
<ProductsList />
</ProductsModel>
)
}
the flag keepMounted would keep the state model even if it was unmounted, otherwise it would wipe and re-create on mount/unmount of ProductsSection
EDIT: But now the lazy call seems worthless, my initial draft was something like
import { Store } from 'easy-peasy'
import model from './model'
function ProductsSection() {
return (
<Store.Model model={model}>
<ProductsList />
</Store.Model>
)
}
// or
function ProductsSection() {
return (
<Store.Model model={model} keepMounted>
<ProductsList />
</Store.Model>
)
}
It would be great if EP could handle nested/name-spaced models. In my current project we have over a hundred models. To make this scale manageable, every model is categorized by a namespace, like devices.groups, devices.freeze, devices.data, devices.geoFence, devices.data, devices.dataPoints, etc.
I'd like to be able to dynamically add models because some product features are user-specific, but currently it's impossible because EP cannot handle adding a model with a nested path. I have mentioned this before but it wasn't considered a priority then. However if working on updating the dynamic model handling, please consider handling 'model paths' like devices.groups in addition to simple model names in the root of the store.
@allpro definitely something to keep in mind 👍
My proposal above would allow for this, however, it does require declaration against the model itself. I believe @kamikazePT is wanting something to be more disjoint to the actual store.
Btw, 💜💜💜💜💜. Thanks so much for your financial contribution. I wish OC would allow me to send messages of thanks. Your continued support means so much. 💜
@ctrlplusb exactly,something disjoint from the store, I would like to have the store created with just mandatory models for it to work and on demand based on user specific roles or other factors, attach New models that I didnt know even existed when I created the store
Also I quite dig the idea of namespaced path models 👍
Keep up the good work! 😄
I haven't given this syntax much thought, but some off-the-cuff feedback on the approaches discussed...
I agree it would be ideal if models could be added without needing to specify them in advance. This is the most decoupled approach and allows new models to be developed and implemented without modifying the base-models specified when the store is created. With such a pattern, adding models on-the-fly might become the norm!
I don't like requiring Provider syntax, or any React syntax, to extend the store. The store is a stand-alone data object, not a React component, so I'd prefer to interact with it as such. This allows model-loading code to be contained within helpers that are separate from React components, which is most likely how I'd handle it.
I use a getStoreModel() helper that allows accessing the store from anywhere, including outside the render tree. We have many complex data helpers that require store data for logic. For example, when generating menus we need to check permissions and feature-flags to determine what items to include. The point is that it is useful to interact directly with the store object, which should include adding models. There could also be an HOC and/or Hook component as alternatives, but these can wrap the lower-level method. This would be the most versatile combination, allowing any kind of system to be built around it.