S
SolidJS•5w ago
Erfan

attaching dynamic component to render pipeline via shared state

Hi there 🙏 — I’m working on a setup where I want to dynamically “plug in” components at runtime, controlled by shared state, without tight coupling between the components themselves. I have a central state (e.g. { current: "StepA" | "StepB", data: {…} }) Multiple components (StepA, StepB) are mostly independent – they don’t import or reference each other The parent component should mount the appropriate Step* into the render tree automatically whenever the state updates I tried this minimal working example using Solid’s <Dynamic> component
import type { JSX } from "solid-js";
import { Dynamic, createComponent } from "solid-js/web";
import { getApp } from "./app";

export function ActiveComponentView(element: JSX.Element) {
const component = createComponent(() => element, {});
return <Dynamic component={component} />;
}

export function inActiveComponentView() {
return <div>waiting to select component</div>;
}

export default function View() {
let app = getApp();

return app.activeComponent
? ActiveComponentView(app.activeComponent)
: inActiveComponentView();
}
import type { JSX } from "solid-js";
import { Dynamic, createComponent } from "solid-js/web";
import { getApp } from "./app";

export function ActiveComponentView(element: JSX.Element) {
const component = createComponent(() => element, {});
return <Dynamic component={component} />;
}

export function inActiveComponentView() {
return <div>waiting to select component</div>;
}

export default function View() {
let app = getApp();

return app.activeComponent
? ActiveComponentView(app.activeComponent)
: inActiveComponentView();
}
https://playground.solidjs.com/anonymous/a28ef95d-a10e-4976-9ba0-5d910a10454e
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
9 Replies
bigmistqke
bigmistqke•5w ago
fixed version: https://playground.solidjs.com/anonymous/f28d7289-9f50-4196-8399-2eed79ca9bd2 changes are: Adjust <View/>-component in solid components run only once! it's also wisest to use solid's control flow components instead of ternaries. Your code
export default function View() {
let app = getApp();

return app.activeComponent
? ActiveComponentView(app.activeComponent)
: inActiveComponentView();
}
export default function View() {
let app = getApp();

return app.activeComponent
? ActiveComponentView(app.activeComponent)
: inActiveComponentView();
}
my code
export default function View() {
return (
<Show when={getApp().activeComponent} fallback={<InActiveComponentView />}>
{component => <ActiveComponentView element={component()} />}
</Show>
);
}
export default function View() {
return (
<Show when={getApp().activeComponent} fallback={<InActiveComponentView />}>
{component => <ActiveComponentView element={component()} />}
</Show>
);
}
replaced functions with components I would advice to use components instead of calling functions directly for templating. <Dynamic/> also expects an (props) => JSX.Element (aka a Component) and not a JSX.Element. There is no benefit in using solid's createComponent. your code
export function ActiveComponentView(element: JSX.Element) {
const component = createComponent(() => element, {});
return <Dynamic component={component} />;
}
export function ActiveComponentView(element: JSX.Element) {
const component = createComponent(() => element, {});
return <Dynamic component={component} />;
}
my code
export function ActiveComponentView(props: { element: () => JSX.Element }) {
return <Dynamic component={props.element} />;
}
export function ActiveComponentView(props: { element: () => JSX.Element }) {
return <Dynamic component={props.element} />;
}
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke•5w ago
Register the proper components your code
export function registerComponents() {
addComponent("login", greeting);
addComponent("login", login);

const app = getApp();
app.activeComponent = login;
}
export function registerComponents() {
addComponent("login", greeting);
addComponent("login", login);

const app = getApp();
app.activeComponent = login;
}
my code
export function registerComponents() {
addComponent("login", greeting.GreetingComponent);
addComponent("login", login.default);

const app = getApp();
app.activeComponent = login.default;
}
export function registerComponents() {
addComponent("login", greeting.GreetingComponent);
addComponent("login", login.default);

const app = getApp();
app.activeComponent = login.default;
}
Fix types in app.tsx your code
type AppContext = {
components: Map<string, JSX.Element>;
activeComponent?: JSX.Element |  undefined;
data?: {
user: string;
};
};
type AppContext = {
components: Map<string, JSX.Element>;
activeComponent?: JSX.Element |  undefined;
data?: {
user: string;
};
};
my code As mentioned before: <Dynamic/> expects a (props) => JSX.Element
type AppContext = {
components: Map<string, () => JSX.Element>;
activeComponent?: (() => JSX.Element) | undefined;
data?: {
user: string;
};
};
type AppContext = {
components: Map<string, () => JSX.Element>;
activeComponent?: (() => JSX.Element) | undefined;
data?: {
user: string;
};
};
Create a new reference for the signal when signals are updated they are referentially checked, if you want to trigger effects to re-run you will have to spread it. another option would be to use a store, or set the equals-option to false (then it will update regardless of if the value changed or not) setSignal(..., {equals: false}) your code
export function setActiveComponentByName(componentName: string) {
const currentState = state();
const component = currentState.components.get(componentName);
if (!component) return;
currentState.activeComponent = component;
setState(currentState);
}
export function setActiveComponentByName(componentName: string) {
const currentState = state();
const component = currentState.components.get(componentName);
if (!component) return;
currentState.activeComponent = component;
setState(currentState);
}
my code
export function setActiveComponentByName(componentName: string) {
const currentState = state();
const component = currentState.components.get(componentName);
if (!component) return;
currentState.activeComponent = component;
console.log("active component is ", component);
setState({ ...currentState });
}
export function setActiveComponentByName(componentName: string) {
const currentState = state();
const component = currentState.components.get(componentName);
if (!component) return;
currentState.activeComponent = component;
console.log("active component is ", component);
setState({ ...currentState });
}
I didn't touch it but
const components = new Map<string, () => JSX.Element>();
const components = new Map<string, () => JSX.Element>();
is a code-smell and could cause bugs because components is not a reactive datastructure:
export function addComponent(name: string, component: () => JSX.Element) {
components.set(name, component);
}
export function addComponent(name: string, component: () => JSX.Element) {
components.set(name, component);
}
will not cause reactive updates. it currently works, but it is very error-prone i would advice to use a single store to keep all your data in, and to use an object and not a map (stores only do fine-grained updates on arrays/objects, not Map/Set/...) a better way to write this would be
export function setActiveComponentByName(componentName: string) {
setState((state) => {
const component = state.components.get(componentName);
if (!component) return state;
return {
...state,
activeComponent: component
}
})
}
export function setActiveComponentByName(componentName: string) {
setState((state) => {
const component = state.components.get(componentName);
if (!component) return state;
return {
...state,
activeComponent: component
}
})
}
because doing
createEffect(() => {
const state = getState()
setState({...state, ...})
})
createEffect(() => {
const state = getState()
setState({...state, ...})
})
will cause an infinite loop and
createEffect(() => {
setState(state => {...state, ...})
})
createEffect(() => {
setState(state => {...state, ...})
})
will not. I did not touch it because it did not come up in the demo, but:
export function GreetingComponent() {
const app = getApp();

return app.data ? greeting(app.data.user) : noUser();
}
export function GreetingComponent() {
const app = getApp();

return app.data ? greeting(app.data.user) : noUser();
}
will not be reactive (see my first comment) . Components only run once is the most important concept to follow in solid. also the same comment
export default function LoginComponent() {
const app = getApp();
const [user, setUser] = createSignal<string>("");

return (
<div>
<input type="text" on:change={(event) => {
setUser(event.target.value);
}}/>
<button type="button" on:click={(event) => {
app.data = {
user: user()
}
}}>Login</button>
</div>
)
}
export default function LoginComponent() {
const app = getApp();
const [user, setUser] = createSignal<string>("");

return (
<div>
<input type="text" on:change={(event) => {
setUser(event.target.value);
}}/>
<button type="button" on:click={(event) => {
app.data = {
user: user()
}
}}>Login</button>
</div>
)
}
const app = getApp(); is not reactive because Components only run once!
bigmistqke
bigmistqke•5w ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke•5w ago
notice how - app.activeComponent is a derivation: get activeComponent() { return this.components[this.activeComponentName]; }, this way you only have to set the component-name. - I shallow-merge the store in addComponent: setApp("components", { [name]: component }) this will add [name]: component to app.components (see docs)
Erfan
ErfanOP•5w ago
Thanks so much for your effort 🙏 I really appreciate it — I’ll try to implement your solution 🙂
bigmistqke
bigmistqke•5w ago
you are very welcome!
Erfan
ErfanOP•5w ago
The application’s Context object is also a standalone App object that holds all the static data (Maps, config, etc.). For state that needs to be reactive (e.g. user: string or other dynamic values), what do you recommend? Should I initialize a central createStore in the Context provider and expose it to all components? Or should each component create its own local signals (or even a local store) based on data pulled from Context? Or perhaps maintain individual signals directly in a global context file? In other words: static data lives in the shared Context App object, but where should the reactivity layer ideally be built—from your experience?
bigmistqke
bigmistqke•5w ago
Should I initialize a central createStore in the Context provider and expose it to all components?
yes, that's a good approach. something like:
const context = createContext<{
store: {...},
addComponent(...): void,
setActiveComponent(name: string): void
>()
const context = createContext<{
store: {...},
addComponent(...): void,
setActiveComponent(name: string): void
>()
Or should each component create its own local signals (or even a local store) based on data pulled from Context?
syncing state is a code-smell
Or perhaps maintain individual signals directly in a global context file?
using individual signals or a store is the same. i would advice for you to use a store. they are very handy, especially in the beginning.
Erfan
ErfanOP•5w ago
thanks i will build it in today, i have played around with your example, and i think i can build the app the same way 🙂

Did you find this page helpful?