-
-
Notifications
You must be signed in to change notification settings - Fork 926
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add React Context API similar feature #2148
Comments
I agree that the context API is a nice pattern, but IMHO this is a userland concern. Enforcing an opinionated structure of this kind is antithetical to much of Mithril's basic philosophy. |
I don't think this can be fully implemented with Mithril as it is today. There's no way for a vnode to look up the ancestors hierarchy... We could add a That's probably the simplest way to make it possible to implement that feature. |
Just a couple of thoughts on this. React "needs" the context API (or some 3rd party wiring like Redux) because redraws are tied to component state, while Mithril redraws are not. Would a vnode parent reference affect garbage collection by adding circular references within the tree? |
I'm against the idea. It can be implemented but shouldn't: it's unnecessary and drastically adds to the surface complexity of Mithril's vnode API. As @pygy points out React needs this as a workaround for initial API design that Mithril solved the first time round. In practice Mithril can and should achieve the same practical effects through the |
It's just an idea to lookup from, not meant to be same as context API, just see what's mithril can do for parent lookup.
Yes! but also maybe as a global method if m.closest ( vnode, parentVnode => isProvider(parentVnode) ) // return provider
How the vnode parent |
FWIW, domvm's vnodes have also, i think react's context api auto-redraws relevant components that use a consumer whenever a provider's value is updated. so it's a bit more than just getting a value from an ancestor in the vtree, it's also a micro pub/sub. |
@leeoniya In what browser are there leaks in such circumstances? I remember IE6 having two GCs and DOM <=> JS circular refs causing leaks, but I was not aware of similar circumstances with modern browsers. |
probably worth re-testing again but it was a doozy to narrow down at the time [1] as none of the old vtree should have been reachable, though it's difficult to say with absolute certainty. i tried a lot of things but dereferencing the old vnode from the dom element is the only thing that worked. [1] domvm/domvm#164 |
React "needs" the context API (or some 3rd party wiring like Redux)
because redraws are tied to component state, while Mithril redraws are not.
Can you describe this?
|
Mithril performs a diff/redraw on route changes, ajax resolutions and user events, irrespective of state changes. |
When regard redraw, react have no manually option like The Mithril's good part is very integrated with JS itself, that's good, but vnode data source can only from Think I'm making a button component, that anyone can include: //export ThemedButton
class ThemedButton {
oninit(vnode){
this.theme = getTheme(vnode)
}
view() {
return m("button", {class:this.theme}, `CLICK`)
}
}
function getTheme(vnode){
// how we know the root theme?
} Currently we can pass The idea is, if root theme is This may help this component |
The other idea is to give each Since Below demo code as inspiration: DEMO HERE function queryVnode (vnode, query, depth, store) {
store = store || []
depth = depth || 0
if(Array.isArray(vnode)) {
vnode.forEach(function(v){
queryVnode(v, query, depth, store)
})
return store
}
if(!vnode || !vnode.tag) return
if(depth >= (query.from|0)) {
if(query.test(vnode) && store.indexOf(vnode)<0) store.push(vnode)
} else {
console.log(depth, 'skip')
}
if(query.depth > depth) {
queryVnode(vnode.children, query, depth+1, store)
}
return store
}
var rootVnode = m('div', m("button", {className:'abc'}, `CLICK`))
var find = queryVnode(rootVnode, {
depth: Infinity,
test: vnode=>vnode.attrs && vnode.attrs.className==='abc'
})
console.log(find[0]) |
@futurist for the theme thing, at some point you'll be able to rely on CSS variables that are dynamically scoped according to the position of an element in the DOM. If you want to use it today (with fallback to the default theme in browsers that don't support variables), it is a bit more complex though. color: #f00;
color: var(--someVar, #f00); IIRC the support for CSS variables in vdom could be improved in Mithril. Your
You can then use The Big O complexity is not pretty though (O(N) where N is the total number of vnodes in the tree un to |
@pygy not for theme thing, just because React Context API doc page use Helper is ok for me, but for the above example I suggest mithril can add a |
@futurist for encapsulation, you could inject the // myComponent.js
export default function(vnodeQuery) {
return {view(){...}}
}
// app.js
import vnodeQuery from "..."
import myComponentFactory from "./myComponent"
const myComponent = myComponentFactory(vnodeQuery) |
@futurist I still need to do a write-up, but FWIW, with Meiosis I have a React Context example which simply uses a designated property on the model ( N.B. If you scroll down the page, you will find the Mithril version. |
One pattern that the context API enables is Compound Components. They make for very reusable components, and they are somewhat difficult to achieve without something akin to the context API. If it is not too difficult to do, I would be very much in favor of adding a reference to the parent on the vnode, to enable such a pattern. A typical example of a Compound Component is a Tabs component, with associated TabList, Tab, TabPanels and TabPanel components. I have made several examples for tabs in mithril using different patterns: Example 1: Manual tabs. Tabs are created "manually", by setting an activeTabIndex at the App level. I can build everything I want like this, but there is no abstraction, and I have to wire everything by hand. Example 2: Simple Tabs component. This abstracts the logic of managing the active tab index. This is great, but fixes the structure of the tabs, e.g., if I want to have the tabs below the panels, I have to write an alternative Tabs component. Example 3: Tabs compound component (no context API). This allows more flexibility, since I can choose whether I want to put the tabs on top or on the bottom. However, I am restricted to using a fixed structure for the elements of the compound components, e.g., TabList and TabPanels must be direct children of Tabs, otherwise everything breaks. Example 4: Tabs compount component (with context API). This is the most flexible solution, as it allows any level of nesting, and the compound component still works. I had to create and use an alternative mc function instead of the usual m to add a parent to the vnodes, and this still required some hacks. I don't know whether it would work in all cases. Other examples of compound components might be :
I don't think that a "root" or global context would work that well for compound components, since these might appear in several places within a page, or even be nested inside each other, and each would need to keep its own state. Also, building these examples made me sceptical that the problem can be fully solved in userland only, given the hacks I had to do to make it work, and I am sure that I didn't cover all edge cases. |
@esrch thanks for your explanation of compound components - until now I hadn't really grokked why they were structured the way they're structured. Here's an alternative implementation using child functions (aka 'render props').. I think this is better than the other implementations for a few reasons:
'Compound components' don't need context, it's just that React culture has the context API sitting there so it feels 'idiomatic' to make use of it (presumably this is more 'specific' to the use case than higher-order components or render props). The scenarios that aren't feasible without context are those in which you want to pick up on shared state in separate scopes with no explicit references between the two (for example, you call The class of problems introduced by context are similar to CSS inheritance, inasmuch as the DOM tree structure becomes a model unto itself, a way of avoiding explicit references at the cost of imprecise and increasingly difficult to manage 'declarations' of state. Atomic CSS & functional views via virtual DOM nominally enable us to step out of that problem space by offering total flexibility & explicit, granular precision. EDIT: expanded the Tabs component functionality to allow named tab references and initial tab state declaration. |
@barneycarroll Your solution is indeed better, and the reasons you give make sense to me. As a side note, it was very interesting to read how you wrote your components, I learned a lot from it, thank you. |
@barneycarroll that is brilliant! |
I find react's context very useful exactly for what is mentioned in the original issue here: i18n and theming. Compound components or render props are great for almost anything else, but they offer little help when you need access to the current language or theme somewhere deep down. I tried to create a copy of the react context api here, just to see how far I could get: https://github.com/benmerckx/mithril-context/blob/master/index.js |
i kinda forgot about this thread until recently and basically ended up with the same solution in domvm as @futurist proposed above in #2148 (comment) - a simple vtree crawler that can be implemented in userland (domvm/domvm#202). https://jsfiddle.net/s9cz2wu3/ a nice property it has over @barneycarroll's solution is that the shared stuff can live as high or low as necessary without imposing a specific compund component api (one that provides scoped Tab and TabPanel components) on all contents. certainly, each strategy has its trade-offs, but the vtree crawler is more generalized and simple, i think. EDIT: a drawback I see to the vtree crawler is the same as you would have with createContext - it has to be done a priori rather than ad-hoc as in @barneycarroll's solution, which essentially means free state construction/destruction. for i18n and theming, imperative construction is usually ok, since they tend to be singletons, whereas there can be many child components that need shared but insulated state. so definitely valid use-cases for both strategies. EDIT 2: maybe the vtree crawler is actually sufficient: https://jsfiddle.net/kvd1a954/ |
BTW, this came up independently when I was looking to come up with a better I'm not 100% sold on the
Keys would be object keys, so it's pretty easy to implement. And the context itself would be something like this, mod allocation-avoiding optimizations: var rootContext = null
// Called when processing `m.context.set`
function pushContext(parent, keys) {
if (keys == null) return parent.context
return Object.freeze(
Object.assign(Object.create(parent.context), keys)
)
}
// Called when processing `m.context.get`
function extractContext(callback, keysList, parent) {
var keys = Object.create(null)
for (var i = 0; i < keysList.length; i++) {
keys[keysList[i]] = parent.context[keysList[i]]
}
return callback(keys)
} |
I'm committed to this once #2219 gets implemented and I clean up the excessive arguments passing in the renderer (to reduce stack space - I'll benchmark it first). |
I'm a little late to this. The beauty of mithril is that can take many design paradigms and use it in mithril. Context API is not a generic solution that applies to the common app. Sure, it'd be great to have a library 'm.context' like any other. The Context idea is easily implemented in mithril (if you have in mind at start of projects. I believe an immutable key/value structure that is passsed down the hierarchy provides the same function. It would be nice to have a global immutable object that gets passed to all descendants vnodes "automatically" |
BS"D Possibly Mithril can allow something like |
BTW, that's literally what I'm proposing, just with a catch: I'd rather avoid doing too much allocation in the process. A previous rendition was this, but it would've made it unusable for Meiosis users and others who don't really use Mithril's internal component functionality:
I may still choose to expose it that way, but by abstracting it out completely, I'm free to just allocate and manage context while rendering. It also remains less opinionated on how you structure your views. What we're wanting to do to the router falls under a similar boat - making it accessible to those who don't use Mithril's component functionality.
I'd have to open a separate channel either way - either I do it while normalizing (for your idea) or while rendering (my idea). It might seem simpler to implement, but it'd easily triple the size of this code, and that's a pretty performance-sensitive area. And if I'm going to open a separate channel, I'd rather not waste more memory and CPU cycles than I need, and it's not like it's that much more code to just do a bunch of |
BTW, just a status update on this. @barneycarroll and I discussed this in depth in Gitter (in a private admin-only channel) and we came to the conclusion that we'd like to look into alternate design patterns and abstractions that avoid the need to have context in core. Here's some of these ways to work around it:
Here's some code samples detailing how each of those would work.For the first, passing a model attribute, it's actually quite easy. The snippet below is for something more traditional MVC, but it's easily adapted to something targeting Redux. // Definition
class UserModel {
constructor(request) {
this.request = request
this.list = []
}
loadList() {
return this.request({
url: "https://rem-rest-api.herokuapp.com/api/users",
withCredentials: true,
})
.then(result => {
this.list = result.data
})
}
}
const UserList = ({attrs: {model: User}}) => ({
oninit: User.loadList,
view: () => m(".user-list", User.list.map(user =>
m(".user-list-item", user.firstName, " ", user.lastName)
)),
})
// Usage
const User = new UserModel(m.request)
m.mount(document.body, {
view: () => m(UserList, {model: User}),
}) For the second, it's a little more complicated, but it's still doable. The general shape looks like this, although there are obvious optimizations that could be made for some specialized cases (like in the theoretical router): // Definition
const ComponentWithContext = {
// Lifecycle crap...
view(vnode) {
const Foo = {
// A component closing over certain `vnode.attrs` and `vnode.state` state
}
return vnode.attrs.children({Foo, ...otherRelevantData})
}
}
// Usage
m(ComponentWithContext, {view: context =>
m("div", [
m(context.Foo, ...),
m(SubComponent, {context}),
context.currentWhatever,
])
}) And in general, it's better and more idiomatic in Mithril to keep context thin and easily traced, and we prefer explicit over implicit - we don't like hiding the cost of things. In addition, not all components need to know how routing works or have access to the global model state. So it really keeps abstraction boundaries much stronger, and doing this kind of thing has other benefits, too. Despite all this, we're still not against this feature, just we want to first verify there really does exist problems solved by context that couldn't be easily solved without excessive boilerplate in userland through the use of functional programming, dependency injection, higher order component composition, and other design patterns and methodologies. And as it stands, we've yet to see any problem that is legitimately harder or significantly more tedious to solve. |
I'm against this feature, but I implemented it anyway. The surface isn't quite the same as React's API, and notably it doesn't change the vnode per se, but it should fulfill all the same requirements. For the sake of unique references as keys, consider Symbols. This is a composite component pattern and doesn't require extending Mithril in any way. |
For those who are interested, I've written more extensively about how and why to avoid the context pattern, and understanding context as an API artifact of the kind Mithril shouldn't seek to emulate (same thread). |
I’ve got a partial implementation of context in Mithril Machine Tools. I’m closing this long inactive thread for the purpose of cutting down the backlog but conversation is still welcome. |
Expected Behavior
Mithril now still conformed to the
data is passed top-down
way, since the React Context API come to live, it's possible to grab some idea.Consider the same scenario of locale preference, UI theme etc., for a mithril predefined component lib, this feature is important to apply vnode/state into arbitrarily level of deeply nested sub-components.
Current Behavior
Currently, it's commonly 2 ways to do so:
Break apart from the whole vnode tree system, and using a global
model
, imported then applied to sub-component manually.Store the
top level/desired level
vnode references, and consume data from sub-component using these references, which is a bit anti-pattern.Possible Solution
The React Context API is a good design pattern to consider, it's
Provider/Consumer
pair that the Consumer will lookup the closest Parent Provider for vnode data, and invoke a children as function as the result.This similar way, if there's a context concept or similar thing specific to mithril, and that thing is managed by mithril system compared to current solutions (which is manually managed by user), will make good for better
encapsulated
components, and good for component development from community.The text was updated successfully, but these errors were encountered: