Having The Same Context Transfer Different Properties

im trying to make a base widget, where it has common stuff amongst the widget elements(canvas for now, there will be the reference panel as well)
import {Context, createContext, JSXElement, useContext} from "solid-js";

export const BaseWidgetContext: Context<BaseCanvasPropertiesInterface | undefined> = createContext();

export function useBaseWidgetContext(): BaseCanvasPropertiesInterface {
const context: BaseCanvasPropertiesInterface | undefined = useContext(BaseWidgetContext);
if (!context) throw new Error("useBaseWidgetContext must be used within a BaseWidgetProvider");
return context;
}

export function BaseWidgetProvider(props: { properties: BaseCanvasPropertiesInterface; children: JSXElement }) {
return (
<BaseWidgetContext.Provider value={props.properties}>
{props.children}
</BaseWidgetContext.Provider>
);
}
import {Context, createContext, JSXElement, useContext} from "solid-js";

export const BaseWidgetContext: Context<BaseCanvasPropertiesInterface | undefined> = createContext();

export function useBaseWidgetContext(): BaseCanvasPropertiesInterface {
const context: BaseCanvasPropertiesInterface | undefined = useContext(BaseWidgetContext);
if (!context) throw new Error("useBaseWidgetContext must be used within a BaseWidgetProvider");
return context;
}

export function BaseWidgetProvider(props: { properties: BaseCanvasPropertiesInterface; children: JSXElement }) {
return (
<BaseWidgetContext.Provider value={props.properties}>
{props.children}
</BaseWidgetContext.Provider>
);
}
note: BaseCanvasPropertiesInterface is a placeholder on the context here i want to supply BaseWidgetPropertiesInterface however, i can't just supply it like so bc my other widgets will inherit from this interface and expand with their own properties the naive approach would be making another context but ideally i want to handle this case from this context
28 Replies
McBrincie212
McBrincie212OP20h ago
here is the creator function code:
export type BaseWidgetInterface<T extends BaseWidgetPropertiesInterface> = RenderableComponent<T>;

export interface BaseWidgetPropertiesInterface extends RenderableComponentProperties {
getWidth: () => number,
getHeight: () => number,
getOffset: () => [number, number],
isWidgetPicked: () => boolean,
setIsWidgetPicked: (val: boolean) => void,
setOffset: (val: [number, number]) => void,
setHeight: (val: number) => void,
setWidth: (val: number) => void,
getTitle: () => string,
setTitle: (val: string) => void,
getIsActive: () => boolean,
setIsActive: (val: boolean) => void,
getContent: () => () => JSXElement,
setContent: (value: () => JSXElement) => void,
isSelectable: boolean
}

export default function baseWidgetCreator(
x: number,
y: number,
width: number,
height: number,
title: string,
isSelectable: boolean,
content: () => JSXElement,
): BaseWidgetInterface<BaseWidgetPropertiesInterface> {
const [getWidth, setWidth] = createSignal(width);
const [getHeight, setHeight] = createSignal(height);
const [getTitle, setTitle] = createSignal(title);
const [getOffset, setOffset] = createSignal<[number, number]>([x, y]);
const [isWidgetPicked, setIsWidgetPicked] = createSignal(false)
const [getIsActive, setIsActive] = createSignal(false);
const [getContent, setContent] = createSignal(content);

const properties: BaseWidgetPropertiesInterface = {
getWidth,
getHeight,
getOffset,
getIsActive,
setIsActive,
isWidgetPicked,
setIsWidgetPicked,
setOffset,
setHeight,
setWidth,
getTitle,
setTitle,
isSelectable,
getContent,
setContent,
id: createUniqueId()
}

return {
properties,
element: () => <BaseWidgetProvider properties={properties}>
<BaseCanvas />
</BaseWidgetProvider>,
};
}
export type BaseWidgetInterface<T extends BaseWidgetPropertiesInterface> = RenderableComponent<T>;

export interface BaseWidgetPropertiesInterface extends RenderableComponentProperties {
getWidth: () => number,
getHeight: () => number,
getOffset: () => [number, number],
isWidgetPicked: () => boolean,
setIsWidgetPicked: (val: boolean) => void,
setOffset: (val: [number, number]) => void,
setHeight: (val: number) => void,
setWidth: (val: number) => void,
getTitle: () => string,
setTitle: (val: string) => void,
getIsActive: () => boolean,
setIsActive: (val: boolean) => void,
getContent: () => () => JSXElement,
setContent: (value: () => JSXElement) => void,
isSelectable: boolean
}

export default function baseWidgetCreator(
x: number,
y: number,
width: number,
height: number,
title: string,
isSelectable: boolean,
content: () => JSXElement,
): BaseWidgetInterface<BaseWidgetPropertiesInterface> {
const [getWidth, setWidth] = createSignal(width);
const [getHeight, setHeight] = createSignal(height);
const [getTitle, setTitle] = createSignal(title);
const [getOffset, setOffset] = createSignal<[number, number]>([x, y]);
const [isWidgetPicked, setIsWidgetPicked] = createSignal(false)
const [getIsActive, setIsActive] = createSignal(false);
const [getContent, setContent] = createSignal(content);

const properties: BaseWidgetPropertiesInterface = {
getWidth,
getHeight,
getOffset,
getIsActive,
setIsActive,
isWidgetPicked,
setIsWidgetPicked,
setOffset,
setHeight,
setWidth,
getTitle,
setTitle,
isSelectable,
getContent,
setContent,
id: createUniqueId()
}

return {
properties,
element: () => <BaseWidgetProvider properties={properties}>
<BaseCanvas />
</BaseWidgetProvider>,
};
}
RenderableComponent just contains element(the thing u see) and properties(the properties u can change for the element) and RenderableComponentProperties contains only the ID as a common parameter so in other words ideally i want to expose the nesscarry properties, not just BaseWidgetPropertiesInterface in this one useBaseWidgetContext without creating a lot of contexts
Brendonovich
Brendonovich20h ago
wanna delete the messages in #general now that you're here
McBrincie212
McBrincie212OP20h ago
ig so there
Brendonovich
Brendonovich20h ago
i think you either have to have 1 context with a shared type like the above, or a bunch of type-specific contexts, since you can't have a generic context
McBrincie212
McBrincie212OP20h ago
so that means if i have say BaseCanvasPropertiesInterface that inherits from BaseWidgetPropertiesInterface i have to make a context for that
Brendonovich
Brendonovich20h ago
you don't have to, you'd just lose a bit of typesafety. you could make a useCanvasWidgetContext that just calls useBaseWidgetContext and overwrites the type
McBrincie212
McBrincie212OP20h ago
oh but for the BaseWidgetProvider?
Brendonovich
Brendonovich20h ago
if you want to keep the base and canvas properties in separate contexts then you'd need more than one, i think i was assuming you take the canvas proeprties and merge them with the base properties
McBrincie212
McBrincie212OP20h ago
tbh i could try to inherit from the BaseWidgetPropertiesInterface
Brendonovich
Brendonovich20h ago
i think personally i wouldn't have the canvas properties inherit from the widget properties
McBrincie212
McBrincie212OP20h ago
and add my own params ah ok
Brendonovich
Brendonovich20h ago
let the canvas-specific properties exist on their own, and have the widget properties in another area then you can pull each of them where necessary
McBrincie212
McBrincie212OP20h ago
so i can't escape 2 contexts at once
Brendonovich
Brendonovich20h ago
unless you merge the properties objects no
McBrincie212
McBrincie212OP20h ago
ok
Brendonovich
Brendonovich20h ago
also unless you don't need to get the widget context in the canvas
McBrincie212
McBrincie212OP20h ago
tbh im thinking of making this a factory method
import {Context, createContext, JSXElement, useContext} from "solid-js";
import {BaseWidgetPropertiesInterface} from "./BaseWidgetCreator.tsx";

export const BaseWidgetContext: Context<BaseWidgetPropertiesInterface | undefined> = createContext();

export function useBaseWidgetContext(): BaseWidgetPropertiesInterface {
const context: BaseWidgetPropertiesInterface | undefined = useContext(BaseWidgetContext);
if (!context) throw new Error("useBaseWidgetContext must be used within a BaseWidgetProvider");
return context;
}

export function BaseWidgetProvider(props: { properties: BaseWidgetPropertiesInterface; children: JSXElement }) {
return (
<BaseWidgetContext.Provider value={props.properties}>
{props.children}
</BaseWidgetContext.Provider>
);
}
import {Context, createContext, JSXElement, useContext} from "solid-js";
import {BaseWidgetPropertiesInterface} from "./BaseWidgetCreator.tsx";

export const BaseWidgetContext: Context<BaseWidgetPropertiesInterface | undefined> = createContext();

export function useBaseWidgetContext(): BaseWidgetPropertiesInterface {
const context: BaseWidgetPropertiesInterface | undefined = useContext(BaseWidgetContext);
if (!context) throw new Error("useBaseWidgetContext must be used within a BaseWidgetProvider");
return context;
}

export function BaseWidgetProvider(props: { properties: BaseWidgetPropertiesInterface; children: JSXElement }) {
return (
<BaseWidgetContext.Provider value={props.properties}>
{props.children}
</BaseWidgetContext.Provider>
);
}
since this pattern is used commonly
Brendonovich
Brendonovich20h ago
since you could overwrite the outer canvas properties context with the base widget context
McBrincie212
McBrincie212OP20h ago
mhm
Brendonovich
Brendonovich20h ago
but idk how much sense that would make if the canvas properties inherit from the base properties in the first place
McBrincie212
McBrincie212OP20h ago
i mean i got it, its inevitable for the above tho, is it reccomended if i do something like have createContextBoilerplate<T>
Brendonovich
Brendonovich20h ago
yeah sure
McBrincie212
McBrincie212OP20h ago
to not have to write the same thing basically over and over again
Brendonovich
Brendonovich20h ago
though i think i'd use createContextProvider from solid primitives
McBrincie212
McBrincie212OP19h ago
lets reinvent the wheel wait a minute
export interface BaseWidgetPropertiesInterface extends RenderableComponentProperties {
getWidth: () => number,
getHeight: () => number,
getOffset: () => [number, number],
isWidgetPicked: () => boolean,
setIsWidgetPicked: (val: boolean) => void,
setOffset: (val: [number, number]) => void,
setHeight: (val: number) => void,
setWidth: (val: number) => void,
getTitle: () => string,
setTitle: (val: string) => void,
getIsActive: () => boolean,
setIsActive: (val: boolean) => void,
getContent: () => () => JSXElement,
setContent: (value: () => JSXElement) => void,
isSelectable: boolean
}
export interface BaseWidgetPropertiesInterface extends RenderableComponentProperties {
getWidth: () => number,
getHeight: () => number,
getOffset: () => [number, number],
isWidgetPicked: () => boolean,
setIsWidgetPicked: (val: boolean) => void,
setOffset: (val: [number, number]) => void,
setHeight: (val: number) => void,
setWidth: (val: number) => void,
getTitle: () => string,
setTitle: (val: string) => void,
getIsActive: () => boolean,
setIsActive: (val: boolean) => void,
getContent: () => () => JSXElement,
setContent: (value: () => JSXElement) => void,
isSelectable: boolean
}
what if on this i had like subProperties field where it would also be a object turn it maybe to something like this
export interface BaseWidgetPropertiesInterface<T> extends RenderableComponentProperties {
// <...>
subProperties: T
}
export interface BaseWidgetPropertiesInterface<T> extends RenderableComponentProperties {
// <...>
subProperties: T
}
i don't think tbh it would work out and as u said its better to have the contexts seperate i might have to embrace this
peerreynders
peerreynders13h ago
Have you considered implementing the context value as a discriminated union? Right up front, it's not as convenient as polymorphism where you can arbitrarily add new morphs, as it means that you have to modify the union whenever you add a new kind of context value but I wonder if that could be mitigated by somehow injecting the union as a type parameter into the overall context. The kinds of context values would be designed for consumer needs while "provider components" would simply chose the most appropriate kind of context to provide. As all members of the discriminated union would implement the core properties no further narrowing would be needed to access those. Those consumers who do need access to extended properties could use the value kind to narrow the context value to gain access. Remove subclass refactoring In many ways it's accepting early that if you are going to be needing instanceof (or type predicates) for type narrowing later anyway, just accept it and make it a discriminated union. Though there is the difference that kinds are distinct, while interfaces can overlap (though you could add narrowing functions that are maintained together with the discriminated union).
MDN Web Docs
instanceof - JavaScript | MDN
The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. The return value is a boolean value. Its behavior can be customized with Symbol.hasInstance.
Documentation - Narrowing
Understand how TypeScript uses JavaScript knowledge to reduce the amount of type syntax in your projects.
McBrincie212
McBrincie212OP12h ago
like have a sort of type property that has the discriminated union of canvas and base-widget? i've tried that, tbh it feels wrong to me on many levels]
peerreynders
peerreynders12h ago
The thing is if you are going to use just one type of context you'll need to use (runtime) type narrowing anyway to get access to any extended properties if they are present. Also you are still thinking in terms of “kind of provider”. Context consumers really don't care about that. They care about “do you have the type of properties that I need?”

Did you find this page helpful?