Context

Context #

Props (and Attributes and Fields) are the default way to pass data down the component tree. Sometimes, you may also want to get data from components above the current component, or in other words, understand what the Context of the current component is.

An Mvui Context works pretty much the same as in React. It can also be seen as a kind of dependency injection, but that is a whole other discussion.

Basic Example #

The following example should be seen as a bit of a toy. You could achieve the same end result in an easier and more idiomatic way using events. However, the code does illustrate how a Context works on a basic level.

import { Component, rx, h } from '@mvuijs/core'; const myCtx = new rx.Context<rx.State<string>>(); @Component.register class CtxProvider extends Component { render() { const ctx = this.provideContext(myCtx, new rx.State('initial')); return [ h.span(ctx.derive(v => `Value: ${v}`)), h.div(h.slot()), ] } } @Component.register class CtxConsumer extends Component { render() { const ctx = this.getContext(myCtx); return [ h.button({ events: { click: _ => ctx.next('changed')} }, 'change ctx val'), ] } } @Component.register export default class App extends Component { render() { return [ CtxProvider.t([ CtxConsumer.t() ]) ]; } }

Again, this is not the most useful example. However, imagine now that the CtxConsumer sits somewhere deeper in the component tree and itself needs to know the value of the provided myCtx. The only alternative to using a Context in that scenario would be “prop drilling”, meaning you continually pass the data in question to every child component.

A Custom <select> and <option> #

Another example of how Contexts can be useful is writing a custom implementation of <select> and <option>. This is because the <option> element needs to know what the current value is if you want to style it accordingly. If all <option>s were styled the same, you could just have them emit events. But if you do want custom styling for the active option, a Context is arguably the most elegant way to handle this.

import { Component, rx, h } from '@mvuijs/core'; const selectCtx = new rx.Context<rx.State<string>>(); @Component.register class Select extends Component { props = { selected: rx.prop<string>() } render() { const ctx = this.provideContext(selectCtx, new rx.State('')); this.subscribe(this.props.selected, v => ctx.next(v)); const hideChildren = new rx.State(true); this.subscribe(ctx, _ => hideChildren.next(true)); return [ h.span(ctx.derive(v => `Value: ${v}`)), h.button({ events: { click: _ => hideChildren.next(v => !v) }}, 'pick'), h.div({ style: { display: hideChildren.ifelse({if: 'none', else: 'block'}) } }, [ h.slot(), ]), ] } } @Component.register class Option extends Component { props = { value: rx.prop<string>() } render() { const ctx = this.getContext(selectCtx); const { value } = this.props; const selected = ctx.derive(v => v === value.value); return [ h.button({ events: { click: _ => ctx.next(value.value) }, style: { outline: selected.ifelse({ if: '3px solid blue', else: 'none' }) }, }, value), ] } } @Component.register export default class App extends Component { render() { return [ Select.t({ props: { selected: 'val1' }}, [ Option.t({ props: { value: 'val1' }}), Option.t({ props: { value: 'val2' }}), Option.t({ props: { value: 'val3' }}), ]) ]; } }