foxdonut / meiosis Goto Github PK
View Code? Open in Web Editor NEWmeiosis
Home Page: https://meiosis.js.org
License: MIT License
meiosis
Home Page: https://meiosis.js.org
License: MIT License
Hi,
I went through you tutorial and I like how it shows that you don't need much to build an app with modern ideas (view = f(model)). Especially, whatever rendering lib you use, it does not really matter :)
However, I knew meiosis before as a "framework" implementing the SAM pattern (https://foxdonut.gitbooks.io/meiosis-guide/content/sam.html). This part is missing from the tutorial. I'm wondering what's the relation between your previous gitbook and this tutorial. Do you intend to add state + model + next action predicate in that? It would be nice to show how to implement the SAM pattern as an extension to the meiosis pattern.
Just my thoughts, take it or leave it :)
PS: I'm also very eager to see / try to write an example with SAM and Reasonml, as the pattern matching would work really nicely with that, I believe.
Now that we have React Hooks, the usage with meiosis pattern is obsolete?
How can I make use of meiosis + react hooks to get a better state management in general?
I might use map on a stream down in a component that later gets unmounted. How do I stop the callback from continuing to happen?
I may be thinking about it wrong. If so, please correct me. I'm just getting started with this sort of state management.
@foxdonut
I tested a bit more :-)
one thing I noticed is that with the router's toPath
functions all values that are not explicitly defined in routeConfig
as path values or query params will be dropped:
In practice this means that in the routing example this:
// adding-a-router/src/beverage/view.jsx
<a href={router.toPath(routing.childRoute([Route.Brewer({ id })]))}>Brewer</a>
...will result in id === undefined
at the brewer component for route #/coffee/c2/brewer
, as currently specified.
With pick
in convertToPath
we should probably pass the beverage id
down as a prop.
How easy do you think it is to come with meiosis examples using hooks functions from react or solidjs state manager ?
I'm new to Meiosis and have some problem getting the initialRoute work.
I'm using:
"meiosis-routing": "^3.0.0",
"meiosis-setup": "^5.1.2",
My configuration:
import { createRouteSegments, routeTransition } from 'meiosis-routing/state'
import createRouteMatcher from 'feather-route-matcher'
import { createFeatherRouter } from 'meiosis-routing/router-helper'
export const Route = createRouteSegments([
'Home'
, 'Contatti'
, 'NotFound'
])
export const routeConfig = {
Home: '/'
, Contatti: '/contatti'
}
export const navTo = route => ({ nextRoute: () => Array.isArray(route) ? route : [route] })
export const routeService = state => ({
routeTransition: () => routeTransition(state.route, state.nextRoute)
, route: state.nextRoute
})
// ...
export const router = createFeatherRouter({
createRouteMatcher
, routeConfig
, defaultRoute: [Route.NotFound()]
// , getPath: function () { return document.location.pathname }
})
If I open in browser a url:
https://mysite.com
I get the right Route.Home(), but if I open this url:
https://mysite.com/contatti
initialRoute is not set to Route.Contatti() but stay on Route.Home().
I see in createFeatherRouter code the line 317:
const initialRoute = parsePath ? parsePath(getPath()) : undefined;
have this line to get url path from browser url?
After router creation if I console.log(router.initialRoute) I get:
router.initialRoute
[
{
"id": "Home",
"params": {}
}
]
but I think that it have to be:
router.initialRoute
[
{
"id": "Contatti",
"params": {}
}
]
My app config is:
const app = {
initial: Object.assign(
{}
, navTo(router.initialRoute)
, home.initial
, contatti.initial
)
, Actions: function (update) {
return Object.assign(
{ navigateTo: route => update(navTo(route)) }
, home.Actions(update)
, contatti.Actions(update)
)
}
, services: [routeService]
, effects: update => []
}
home.initial and contatti.initial have not navigation function and
, home.Actions(update)
, contatti.Actions(update)
are empty
Have I not understanding the right Meiosis configuration?
Is a bug?
best regards,
Leonardo
Hello!
I read your whole tutorial for mithril, awesome reading!
For example in chapter 12:
If I add console.log(label + ' redraw');
into view()
function (~67th line) inside createTemperature()
I see
Air redraw
Water redraw
every time I click any button!
Is it possible to rewrite meiosis pattern to rebuild vdom only for components dependent of changed data?
For example if I increase water temperature only Water redraw
line must appear at console.
Hey @foxdonut ... as i am getting further along with meiosis, one odd thing to me about the API is the nextAction
function declaration (model, proposal, actions)
.
In most cases, only the model
and actions
would be needed to make a decision on the next Action I think so requiring that the dev declare all three args to get the actions
(which he will surely need for a nextAction) seems to miss out on the benefits of JS where I could just do something like nextAction: (model, actions) => { if(model.foo) { actions.go('foo') }}
I know the proposal can inform the context of the request/loop but as far as I have learned that isnt as common as just reading model state to decide what's next. Is that wrong? I dont want to be frivolous.
Love the introduction here
https://github.com/foxdonut/meiosis/wiki/The-Fundamental-Setup
but found this confusing:
There is just one source stream: update. The view code does not create additional streams. In fact, the view code is not aware of streams at all; views just call the update as a function that was passed as a callback.
When view
is called, it is passed the update stream instead of function:
const update = flyd.stream();
...
models.map(model => ReactDOM.render(view(model, update), element));
The only reason it works here is because of the flyd
api treating streams as functions. But you need to know this in order to write the view correctly, and it would not work when you replace flyd
by another stream library with different api. E.g. if update(val)
were replaced by update.emit(val)
, then the view
must be aware of it.
What seem conceptually happen here is some sort of lifting where the update
was previously defined for functions, but when called, is actually lifted to stream values.
I can see why it doesn't matter in this particular example, but using object.assign()
directly does actually have the side-effect of changing the existing value of the stream, while returning the same mutated object rather than a new one.
The stream isn't strictly a "stream" anymore - it's now a "stream-looking" way of performing continuous operations on mutable state, which is a bit confusing and likely could lead to bugs in more complex scenarios? ๐ค
Likely the appropriate function would be something like:
function(model, value) {
return Object.assign({}, model, value);
}
Which is side-effect free.
I understand it doesn't lead to problems in this particular case, but you are trying to teach people functional programming patterns, where typically functions are kept pure and free of side-effects?
I'm by no means an expert, it just occurred to me that some people might not understand that using object.assign()
in this way, with FRP patterns, in general, isn't really safe?
Currently, when editing the model with the textarea, the view does display correctly but subsequent interactions with the view are inconsistent.
This change makes editing the model with the textarea add to the tracer history, and makes view interactions consistent with the model snapshot from the tracer.
Not so much an issue but more of a question, why have all the react related tutorials been deleted? I actually found them helpful.
@foxdonut Please help
Refer to the example I have created: https://github.com/meepeek/meiosis-react-example , the component can be dependent on their own by the work of react setState and not meiosis itself. In temperature example, the update and models passed thought from root through every components which makes the code so complex.
It would be hard to scale if we have to pass everything down to make it works. I still cannot find the solution.
Can we have 2 updates in a single app without setState ?
Meiosis needs to account for the absence of window
under React-Native.
Thanks to @mnichols for reporting this issue.
When using m.router.prefix = '' it breaks the router implementation, issue is caused by checking for config.prefix truthiness. Condition should be done with hasOwnProperty instead.
Statics/function attributes need to be hoisted onto the returned function from createComponent
so that HOC composition doesnt get buried.
For example, if you have a React component that is like:
function MyView(props) {
return <div>me</div>
}
MyView.route = { styles: {... }}
export default MyView
The route
static is lost (and inaccessible) because it is wrapped by the createComponent
function. (here).
In my opinion the composition of HOC by meiosis is an implementation detail that should be hidden, so statics should be hoisted...something like https://github.com/mridgway/hoist-non-react-statics.
Thots?
Congrats on releasing the new tutorial! The following is my feedback as I read through it. I've written a lot of educational material for programmers (beginners, specifically), so I hope to be of some help.
vdom
variable to be clear on what m()
is returning).var vdom
change in the previous lesson: "instead of directly passing what to render" -> "instead of hardcoding our VDOM"var vdom = view(initial)
Note:
section since it's not relevant to the current concept.createView
. I personally understand the purpose of the abstraction, but in the context of the tutorial, the abstraction provides no tangible benefit to the example code. It'll be more work, but I recommend omitting it here (resulting in something like this) and introducing the concept in a new "components" lesson, where createView
is called to create multiple counter views.That's all I have for now. Great job with this so far. I like meiosis and want it to succeed. I'll be back later with more feedback after you make a round of updates.
I'm trying to start with meiosis-setup with snowpack. But I'm getting Uncaught TypeError: meiosis is not a function and sure enough it is undefined.
import meiosis from "meiosis-setup/mergerino";
import simpleStream from "meiosis-setup/simple-stream";
import merge from "mergerino";
const app = {};
console.log(meiosis);
const { update, states } = meiosis({ stream: simpleStream, merge, app });
What am I missing?
Can I have all models and actions in one place before assign to components ? The example I saw had only model and view contained in a component.
I want to make a develop environment where I can call actions manually. This way I can test all actions before coding the UI. Could you give me an example ?
First of I want to say thank you for writing all of this content, I've found it to be a tremendous resource.
When implementing a system that uses a next-action function I discovered that the system generates many duplicate events given the example implementation.
nap: actions => state => {
if (state.pageId === "DataPage" && !state.data) {
actions.loadData();
}
}
const present = flyd.stream();
const actions = app.actions(present);
const states = flyd.scan(app.acceptor, app.initialState(), present)
.map(app.state);
states.map(app.nap(actions));
Adding additional actions will result in n!
calls to nap, state, and anything downstream as a function of the number of updates
in the actions
. This was easily fixed by tweaking the example using a stream combinator. The previous example then becomes:
nap: actions => state => {
if (state.pageId === "DataPage" && !state.data) {
actions.loadData();
}
}
const present = flyd.stream();
const actions = app.actions(present);
const computed = flyd.scan(app.acceptor, app.initialState(), present)
.map(app.state);
const states = flyd.combine(
(computed) => {
if (!app.nap(actions)(computed())) {
// If nap indicates that there was no action called then create a new
// event. This prevents nap from triggerring the same action
// several times.
return computed();
}
},
[computed],
);
I derived this solution from the sam.js docs. With this small tweak a new state isn't produced and rendered if a next action was triggered.
Thanks again!
Lets assume we have an encapsulated part of software that we're willing to store all if its storage in one central place, lets call it a module. Consider that this module is made of instances of some components. Each component in meiosis is a function that returns a model and a view. In meiosis you're suggesting storing these models in a hierarchical dict. It's good approach and Redux has something similar too: https://redux.js.org/api/combinereducers.
But in my experience with Redux in a super huge web-application with more than 200 view components, more than 40 tables in server's db some having more than 200k rows served by a rest api I found that it'd be much easier if instead of this hierarchical dict my store was just a single level dicts with unique ids as keys and instance storage data as values. These keys can be uuids or in "model:id" format or even in "model:view:id" format, values can be js objects (or immutable objects), single level here doesn't mean values should be flat numbers or strings, it just means each component is in first level of hierarchy in the store dict. For example in my web-application a view component in this path in the store dict: x->y->z->a->b->c would want to have access to this path and use some data from there X->Y->Z->A->B->C. After the app grew I found it much easier if my store was stored the way I described. If c in example above needs data from C then it should have its whole path in hierarchical dict in hierarchical model but in single level dict model it only needs to know the uuid or if it's stored in "model:id" format it only needs to know the id of the instance.
There are some challenges in this storage model such as what if user is modifying an instance stored in "model:1" but when the edit shouldn't apply to whole web app nor it should be synced with server until he presses some "save" button. (not syncing with server is not the challenge but not syncing with whole web app is a challenge.) It can be solved by following a pattern to store editions in a temporary store and sync it with global storage only when user saves the editions, views should read data from the temporary store dedicated for their edits unless it's not present. I can't remember any other challenges write now but I'm sure there were more and I'm sure the solution to these challenges were straight forward.
This model is similar to how you would store your data in your server using a db server. Most of the reasons that database servers don't save data in a big dict and rather store it in tables and give you an api to access your data in "model:id" format (SELECT * FROM "model"_talbe WHERE id="id") and it's obvious to us that this model of saving data is best approach applies here too.
What do you think about it?
Basically, adding this to the common.js before exporting everything;
const bindActions = Object.entries(actions).reduce((acc, [key, func], index) => {
acc[key] = func.bind(actions)
return acc
}, {})
return { update, contexts, states, actions: bindActions }
This way we can call actions from inside actions with simple this.action
. It would be a nice addition since you can include more logic between the actions inside them.
I have something similar, basically :
Actions : {
api : { /* ... */ }
paging: { /* ... */ }
sorting: { /* ... */ }
}
where paging and sorting are internally calling the api action, and so on ...
Hi!
So I follow what's going in with hyperapp development, I'm a big fan of that project. I saw meiosis come up in a discussion and checked it out. I liked it so much I ended up hacking together some tooling to the weather example.
The styling is kind of gross right now, but that's simple enough to fix. Maybe something more polished would be nice for folks to quickly dig in on a local machine
@foxdonut It is really impressive what you've achieved with the TypeScript support, so I have only a few remarks/suggestions/requests.
Currently:
import Stream from 'mithril/stream';
import { toStream } from 'meiosis-setup/common';
import { App, Service, setup } from 'meiosis-setup/mergerino';
import { merge } from '../utils/mergerino';
I would prefer:
import Stream from 'mithril/stream';
import { toStream } from 'meiosis-setup/common';
import { App, Service, setup, merge } from 'meiosis-setup/mergerino';
As we are already using mergerino, it makes little sense (considering it is quite stable) to import it separately.
getCell
do? It has no documentation.export const { states, getCell } = setup({ stream: toStream(Stream), merge, app });
const { states, update, actions } = meiosis({ stream: simpleStream, merge, app });
However, in your code, I do not see the actions
being exported, and instead, you export states
and getCell
? And in the mergerino
version, getCell
also has a nest
function. What is that used for?
setup
from meiosis-setup/mergerino
, it would make sense that the merge
does not have to be supplied separately, i.e. why can't we use:export const { states, getCell } = setup({ stream: toStream(Stream), app });
toStream
first exported, just to convert Stream
to StreamLib
? Can't this be done internally, so instead, passexport const { states, getCell } = setup({ stream: Stream, app });
Where Stream
is of type mithril/stream
or flyd
.
simpleStream
yourself, why not leave it out, so it becomes:import { App, Service, setup } from 'meiosis-setup/mergerino';
export const { states, getCell } = setup({ app });
So the simpleStream
will be the default value for stream
, the same with merge
and mergerino
.
app
, as in:const app: App<IAppModel, IActions> = {
initial: Object.assign({}, appStateMgmt.initial) as IAppModel,
Actions: context => Object.assign({}, appStateMgmt.actions(context)) as IActions,
/** Services update the state */
services: [] as Array<Service<IAppModel>>,
};
Why do you write services
using lower-case letters, versus Actions
and Effects
? I prefer the former, but perhaps there is a specific reason for that.
memory-trainer
, file app-state.ts
, you have the followingactions: context => { // ...
Whereas I prefer, so I don't have to write context.update(...)
everywhere:
actions: ({ update, getState }) => { // ...
function
, you can, by using this
. However, Typescript coding practices do not really like the usage of this
. Isn't it possible to also provide the actions
, like the state
and update
function, as a parameter (in the context
as defined before)? So you can always get to another action. I.e. the above example would become:actions: ({ update, getState, actions }) => { // ...
meiosis/helpers/routing/state/index.js
Lines 47 to 62 in d5c8b95
Dear @foxdonut
I have been taking a look at your routing WIP. Array based, programmable routes are ๐ฅ. Here are some thoughts that may make the concept easier to grok:
Initially I had problems wrapping my head around what route
and routes
mean or refer to in different places of the code.
We have route
in state
: This can be thought of an array of ...routeSegments
that match against components in the view tree and to some extend against url paths as well.
Then there is the route
component prop (object), that gets calculated based on a given route
array in state
: This is where the actual routing or navigation is happening on a per component basis. Confusingly, it contains a routes
value which references the original route
array.
Then on top of this there are various helper functions where it is hard to tell if they work with the component prop route
or the state route
/ routes
or both. (e. g. initRoute
and nextRoute
produce a prop, while parentRoute
, childRoute
, siblingRoute
produce a state route arry.
I think the confusion may be resolved by re-naming the state prop to something different, like routing
or navigation
or something similar.
I would also suggest to better differentiate the two functions nextRoute
and initRoute
that each yield the routing prop from the other functions that yield state route arrays.
Consider the following:
export function Routing (route = [], index = 0) {
return {
route,
index,
local: route[index] || {},
child: route[index + 1] || {},
next () {
return Routing(route, index + 1)
}
}
}
And then in the Root
import { Route, Routing } from "somewhere"
import { Beer, Beverages } from 'somewhere-else'
const componentMap = { Beer, Beverages }
function Root ({ state, actions }) {
const routing = Routing(state.route.current)
const Component = componentMap[routing.local.id]
return <div>
<Button onClick={() => actions.navigateTo([ Route.Beer(), Route.Beverages() ])}>
Show list of beers
<Button>
<Component state={state} actions={actions} routing={routing} />
</div>
}
And then in Beer
import { Beverages } from "../beverages";
import { Beverage } from "../beverage";
const componentMap = {
Beverages,
Beverage
}
function Beer ({ state, actions, routing }) {
const Component = componentMap[routing.child.id]
return (
<div>
<div>Beer Page</div>
<Component
state={state}
actions={actions}
routing={routing.next()}
beverages="beers" />
</div>
)
}
I think the mechanism will be much easier to understand just by virtue of better differentiating its moving parts.
this might also help clarify the signatures of the other helper functions that consume the routing prop to yield a new route array. (So that you don't end up referencing a routes
when in fact there is only route
.
const routing = new Routing([ Route.Beer(), Route.Beverages() ])
function showBeerDetails () {
actions.navigateTo(siblingRoute(routing, [ Route.Beverage({ id: 'b2' }) ])
}
/**
*
* @param {Object} routing - the component routing object / prop
* @param {Array.<{ id: String, params: Object}>} routing.route - original state route ref
* @param {Number} routing.index - current route segment reference
*
* @param {{ id: String, params: Object}} sibling - route segment to inject into route
* @param {Array.<{ id: String, params: Object }>} [children] - optional sibling children
*
* @return {Array.<{ id: String, params: Object }>} - new state route
*/
function siblingRoute (routing = {}, [ sibling, ...children ]) {
return routing.route.slice(0, routing.index).concat(sibling, children)
}
A am about to try your new routing mechanism with an app of mine where I have to deal with ledgers, that contain accounts, and accounts that contain transactions, and transactions that have lots of transaction details and reference different accounts each. So this will be awesome!
Hi is there a performance impact with calling ReactDOM.render again upon changes in the model?
Does it work the same as a normal re-render?
this is causing an error for me, because state.login
is still null
when the login view renders after redirect from the settings page. (only happens on redirect)
Can you reproduce this?
Might be related to the combinator optimisations in the setup script. Not sure, though
Could you explain why do you convert from Patchinko to Mergerino?
Has Patchinko any important disadvantages to avoid using it in Meiosis?
Hello @foxdonut
I tried out simple-stream
with the routing example and ran into an issue.
When entering the app at the initial route path: /beer
or /coffee
or /login
or /settings
, I get an Error at the Root component
For some reason, state
is null
at that point.
I logged state
one level above in the App component
let count = 0
export class App extends Component {
/* ... */
render () {
const state = this.state
const { actions } = this.props
count++
console.log('App state', state, count)
return el(Root, { state, actions })
}
}
and it is in fact null
:
Now the strange thing is, this happens only when 'cold-loading' the the routing example at the /beer/*
, /coffee/*
, /settings
and /login
route paths. The /tea
and /
paths work when cold loading.
Also this issue only occurs when using simple-stream
and mithril/stream
. For some reason flyd
handles this fine.
I have a hard time wrapping my head around this. Can you reproduce this error?
It must me somehow related to the buffered update mechanism in setup
, I think.
Hi foxdonut,
Thanks for your work on meiosis! I wanted to suggest a somewhat-opinionated alternative to how to hook up the state stream to the view in React which differs slightly from the setup and the docs. The following, from the setup, is very useful as a template for getting things set up initially:
export default ({ React, Root }) => ({ states, update, actions }) => {
const [init, setInit] = React.useState(false);
const [state, setState] = React.useState(states());
if (!init) {
setInit(true);
states.map(setState);
}
return React.createElement(Root, { state, update, actions });
};
The main issues I encountered with the above in a larger codebase were the inconvenience of passing state/update/actions as props and, more importantly, the fact that above re-renders the entire tree on every state update. I've read your post on preventing re-renders. I agree with avoiding focusing on performance too early, but these problems do come up, so it's nice to have a way to deal with it if it does. I thought that your suggested solution with shouldComponentUpdate
and an id per component was useful, and this alternative is sort of like that but for a hooks + function component workflow.
export const StatesContext = createContext;
/*
* sliceFn is a pure fn that takes the current state object as a param
* and returns a slice of that state to which a component wishes to subscribe.
*/
export const useStateSlice = (sliceFn) => {
const states = useContext(StatesContext);
const stateSlice = sliceFn(states());
const [slice, setSlice] = useState(stateSlice);
// You can do any sort of comparison you want to decide when to rerender - id check, some deep equals, etc.
// If you wanted to the hook to be more flexible, could allow passing a custom
// comparison function, kind of similar to shouldComponentUpdate
const changed = (a, b) => JSON.stringify(a) !== JSON.stringify(b);
useEffect(() => {
let prevSlice = slice;
const slices = states.map(newState => {
const newSlice = sliceFn(newState);
if (changed(prevSlice, newSlice) {
prevSlice = newSlice;
setSlice(newSlice);
}
});
return () => slices.end(true);
}, []);
return slice;
};
//Top Level
export default ({ Root }) => ({ states, update, actions }) => {
return <StatesContext.Provider value={states}>
<Root update={update} actions={actions} />
</StatesContext.Provider>
};
// Some deeply nested component which only needs a tiny bit of the state
const StatusIndicator = (props) => {
const status = useStateSlice(state => state.status);
return <div>{status}</div>
};
The general idea is to prevent any unnecessary re-rendering by allowing each component which needs some piece of state to create a stream which takes a slice of the current state (kind of like Redux mapStateToProps
) and compares it to the previous slice, updating the component's local state if there has been a change. It's very convenient to use in components that perhaps previously didn't need any state and then require it later. The use of React's context isn't strictly necessary here, as long as the hook has a reference to the state stream.
Just wanted to put this out there, not sure if there was a better place. Maybe this will help someone googling for help with these patterns later on.
Great stuff!, i was wondering it this pattern will be hard to integrate inside an existing redux app, how do i use that pattern only on part of the app on a specific route?
Hi,
What happened in a04e86d ? Why was all the documentation removed?
meiosis/helpers/common/setup.js
Lines 37 to 41 in e38b896
This solves a real headache!
Hello,
I like your work and I try to map some part of my project with meiosis pattern.
I'm using a lot redux and redux-saga with some pro and many cons.
I like the way redux and react-redux use context to propagate store accross component hierarchy.
I'm trying to figure out how to do the same thing with meiosis but I'm stuck.
Do you have any clues or work in progress?
thanks
I create react app with create-react-app command line. How can I integrate meiosis with that ?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.