We have a Webpack bundle exposing React components. The exposed components contain nested components from the same bundle.
We want to allow users to replace components with their own specific version.
For example, the following component structure;
class A extends React.Component {
render() {
return (
<B/>
);
}
}
const C = require('./C');
class B extends React.Component {
render() {
return (
<C/>
);
}
}
from which the top-level component A is exposed in the Webpack bundle:
const A = bundle.components.A;
Our server-side web framework relies on the exposed bundle interface to invoke the React components.
From this structure, an example use-case would be to replace the nested component C from component A.
Current options that we see for this are;
add a method to the class B (and A) to replace component C
create a registration mechanism, e.g. by using the filename or logical name of C, and a generic method to the bundle to replace a component. For example: bundle.component.replace('./C', './custom-C');
...
What would be the best way, i.e. common practice, to replace a (nested) React component in a Webpack bundle?
Joost, I've got the same problem and the way that I'm been handling this is creating an Block component which decides what component should take place.
This is a working project importing webpack bundles and deciding which one should take place. If they are defined in the template scope I switch the render method.
https://github.com/wallynm/react-template-system
In the end we chose to use render-props, HOCs, and adding some replace functions for specific components.
Related
I have a typical Glimmer "base" component:
import Component from '#glimmer/component';
export default class BaseComponent extends Component { ... }
It has a template like normally, but the actual implementations of that component are child componenents, that override some of the template getters and parameters so that it works with various different data types.
export default class TypeAComponent extends BaseComponent { ... }
export default class TypeBComponent extends BaseComponent { ... }
etc.
My question is: How do I specify that all the child components should use the parent class template, so I don't have to duplicate the same fairly complex HTML for all child components? Visually the components are supposed to look identical so any changes would have to be replicated across all child component types. Therefore multiple duplicated templates isn't ideal.
In Ember Classic components there was layout and layoutName properties so I could just do:
layoutName: 'components/component-name'
in the base component and all child components did automatically use the defined template.
Now that I'm migrating to Glimmer components I can't seem to figure out how to do this. I have tried:
layout property
layoutName property
template property
Using the child components without a template in hope that they would automatically fall back to the parent class template.
Only thing that seems to work is creating Application Initializer like this:
app.register('template:components/child1-component', app.lookup('template:components/base-component'));
app.register('template:components/child2-component', app.lookup('template:components/base-component'));
But that feels so hacky that I decided to ask here first if there is a proper way to do this that I have missed?
How to specify template for Glimmer Component?
tl;dr: you should avoid this.
There are two answers to two, more specific, questions:
What is the recommended way to manage complex components with shared behaviors?
Typically, you'll want to re-work your code to use either composition or a service.
Composition
<BaseBehaviors as |myAPI|>
<TypeAComponent #foo={{myAPI.foo}} #bar={{myAPI.bar}} />
<BaseBehaviors>
Where BaseBehaviors' template is:
{{yield (hash
foo=whateverThisDoes
bar=whateverThisBarDoes
)}}
Service
export default class TypeAComponent extends Component {
#service base;
}
and the service can be created with
ember g service base
then, instead of accessing everything on this, you'd access everything on this.base
Ignoring all advice, how do I technically do the thing?
Co-located components (js + hbs as separate files), are combined into one file at build time, which works like this:
// app/components/my-component.js
import Component from '#glimmer/component';
export default class MyComponent extends Cmoponent {
// ..
}
{{! app/components/my-component.hbs }}
<div>{{yield}}</div>
The above js and hbs file becomes the following single file:
// app/components/my-component.js
import Component from '#glimmer/component';
import { hbs } from 'ember-cli-htmlbars';
import { setComponentTemplate } from '#ember/component';
export default class MyComponent extends Cmoponent {
// ..
}
setComponentTemplate(hbs`{{! app/components/my-component.hbs }}
<div>{{yield}}</div>
`, MyComponent);
So this means you can use setComponentTemplate anywhere at the module level, to assign a template to a backing class.
Why is this not recommended over the other approaches?
All of this is a main reason the layout and related properties did not make it in to Octane.
Formally supported Component inheritance results in people getting "clever"
this in of itself, isn't so much of a problem, as it is what people can do with the tool. Bad inheritance is the main reason folks don't like classes at all -- and why functional programming has been on the rise -- which is warranted! Definitely a bit of an over-correction, as the best code uses both FP and OP, when appropriate, and doesn't get dogmatic about this stuff.
Component Inheritance is harder to debug
Things that are a "Foo" but are a subclass of "Foo" may not actually work like "Foo", because in JS, there aren't strict rules around inheritance, so you can override getters, methods, etc, and have them provide entirely different behavior.
This confuses someone who is looking to debug your code.
Additionally, as someone is trying to do that debugging, they'll need to have more files open to try to under stand the bigger picture, which increases cognitive load.
Component inheritance allows folks to ignore boundaries
This makes unit testing harder -- components are only tested as "black boxes" / something you can't see in to -- you test the inputs and outputs, and nothing in between.
If you do want to test the in-between, you need to extract either regular functions or a service (or more rendering tests on the specific things).
I would say this is the classic case for a composition, where TypeAComponent and TypeBComponent use the BaseComponent.
So you have your BaseComponent with all the HTML, that basically is your template. I think its important here to think a bit more of Components also as possible Templates, not only full Components. So lets call this the TemplateComponent.
So you have your TemplateComponent which could also be a template-only component. Then you have as template for TypeAComponent and TypeBComponent:
<TemplateComponent
#type={{#title}}
#title={{#title}}
#onchange={{#onchange}}
#propertyThatIsChanged={{this.propertyThatIsChanged}}
...
/>
this allows you to have a getter propertyThatIsChanged to overwrite pieces. Common behaviour can also be placed on the TemplateComponent, or, if its common code, maybe on a BaseCodeComponent, that only contains shared code, while I would rather not do this.
For areas you want to replace this also opens the possibility to use Blocks.
The TemplateComponent, for example, could use has-block to check if a :title exists, use this block then ({{yield to="default"}}), and if not just use {{#title}}.
So, to the only obvious downside of this: you have to proxy all params. This seems ugly at first, but generally I think its better for components not to have too many arguments. At some point an options or data argument could be better, also because it can be built with js when necessary. It should also be mentioned that there is an open RFC that would address this issue. With the upcoming SFCs, I think this is the much more future-proof solution overall.
To make e2e testing easier I would like to add to each react component data-component= attribute with component name. I would like to have it done "automatically" (without adjusting render() functions everywhere).
Anyone knows how to do it reliably for both class and function based components?
Component name is set via static property displayName for each component. You need to set it manually.
Create hoc (higher order component), to wrap component with div (or any other html tag) which will have required attribute.
const withComponentName(WrappedComponent) => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'UnnamedComponent';
return props => (
<div data-component={displayName}><WrappedComponent {...props} /><div>
)
}
Wrap all component export statements with created hoc.
export default withComponentName(YourShinyComponent)
Another option is to use this Webpack plugin to do it for you:
https://github.com/lemonmade/babel-plugin-react-component-data-attribute
If you're using create-react-app:
If you've ejected already then just follow the docs on the plugin's page ☝️
However, if you have not ejected already, then you can use either of these solutions to customize your build for a create-react-app project:
https://github.com/arackaf/customize-cra
https://github.com/timarney/react-app-rewired/
Is it possible to dynamically import precompiled svelte components or whole svelte apps.
And when, how do I compile a single component in svelte 3. I found this approach, but nothing in the docs:
https://github.com/sveltejs/svelte/issues/1576
I want to combine several independent (hosted) Svelte apps on one page to one bigger svelte-app (microfrontend). The goal is, that every sub app can be independent deployed and hosted wherever (may be an own docker container). And any change should be visible in the aggregator app without recompiling it.
I think I wat to do something like this:
https://single-spa.js.org/docs/separating-applications.html
but with no other framework, that is blowing my app and components.
I don't want to use custom components, because of the inflexible styling of the Shadow DOM. I must be able to change css over a global stylesheet.
Has anyone an idea?
Thank you :)
You can take a look at Ara Framework. It has a standalone component named Nova Bridge.
This approach consists of a component that renders a placeholder micro-frontend view will be mounted. Then the component emits a DOM event named NovaMount that is listened by the JavaScript bundle of the micro-frontend to render it and mount it in run-time.
Here an example of the entry point for your micro-frontend.
import { mountComponent, load, loadById } from 'hypernova-svelte'
import Example from './components/Example.svelte'
const render = (name, { node, data }) => {
if (name === 'Example') {
return mountComponent(Example, node, data)
}
}
document.addEventListener('NovaMount', ({ detail }) => {
const { name, id } = detail
const payload = loadById(name, id)
if (payload) {
render(name, payload)
}
})
load('Example').forEach(render.bind(null, 'Example'))
The microfrontend uses hypernova-svelte. You can take a look in this article I wrote for implementing Svelte in Nuxt.
https://ara-framework.github.io/website/blog/2019/08/27/nuxt-js
Ive got a modular .NET application, which loads controllers, views and components from assemblies as plugins.
Going to learn React (totally new to it) - and got a question. Does React support splitting its components into multiple assemblies and loading them at runtime?
Yes, it does.
I suggest you to read React official documentation.
Almost every React component will produce some HTML markup for browser to show. The general idea of component splitting is to split HTML markup between several components like so
// App.js
class App extends React.Component {
render () {
return <div>
My first component
<Component1/>
</div>
}
}
// Component1.js
export default class Component1 extends React.Component {
render () {
return <div>
This is internals of component
</div>
}
}
If I have an ES6 component like so:
component OldThing extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.someProp}</div>;
}
}
an ES6 container like so:
const OldThingContainer = connect(mapStateToProps, mapDispatchToProps)(OldThing);
and a new typescript component like so:
component NewThing extends React.Component<Props, State> {
render() {
return <OldThingContainer someProp={someValue} />
}
}
I currently get an IntrinsicAttributes error about someProp not existing on OldThing. How do I get rid of that error without adding types/typedefs to/for OldThing and/or OldThingContainer? I tried the #augments solution on the OldThingContainer where it's defined but that doesn't seem to help. Neither did adding the comment on the component itself.
The reason I want to be able to import JS without types is that we are adding typescript to a very large, multiple years old code base and it's currently difficult to get people to write typescript components at all, let alone if they also have to type out every existing component their component wants to import.
Is there an existing elegant solution to this problem or am I going to have to go through ahead of everyone and manually type every existing JS component (hundreds) in some typedef file to get rid of/prevent these errors?
I tried your example and it seemed some issue with the type connect inferred for your component.
One quick'n'dirty fix would be to annotate your JS component with:
/**
* #type {React.ComponentType<any>}
*/
const OldThingContainer = connect(mapStateToProps, mapDispatchToProps)(
OldThing
);
One more elegant fix (but maybe not possible) would be trying to dive into the react-redux typings. The connect function requires you to supply some type arguments for it to properly work. When not supplied, those arguments are given {}, not any (because any can silently eat your types).
Comment: From the few I've worked with react+redux+typescript, lib typedefs are one of the biggest pain points. React typedefs can get really complicated. (flow just will hang up your editor with "waiting for flow server", which is even worse). As TS+Redux gets more popular and more support is added, this may get a lot better in a few months...