Weird Context Behaviour

I'm currently setting up a tip tap editor with solid and wanted to create a context to pass the editor around the components.
import type { SolidEditor } from "@vrite/tiptap-solid";
import { createContext, useContext } from "solid-js";

const EditorContext = createContext<SolidEditor | null>(null);

export const EditorProvider = EditorContext.Provider;

export function useCurrentEditor() {
const context = useContext(EditorContext);
if (!context) {
throw new Error("useEditor must be used within an EditorProvider");
}
return context;
}
import type { SolidEditor } from "@vrite/tiptap-solid";
import { createContext, useContext } from "solid-js";

const EditorContext = createContext<SolidEditor | null>(null);

export const EditorProvider = EditorContext.Provider;

export function useCurrentEditor() {
const context = useContext(EditorContext);
if (!context) {
throw new Error("useEditor must be used within an EditorProvider");
}
return context;
}
I have a root component that creates the editor and then multiple sub components that want to consume the editor.
export const EditorRoot: Component<Partial<EditorOptions> & EditorRootProps> = (
props,
) => {
const tunnelInstance = tunnel();
const { children, ...editorProps } = props;

const editor = useEditor({
...props,
extensions: editorProps.extensions ?? [StarterKit],
});

return (
<EditorProvider value={editor()}>
<EditorCommandTunnelContext.Provider value={tunnelInstance}>
<div class={props.className}>
<SolidEditorContent editor={editor()}>
{props.children}
</SolidEditorContent>
</div>
</EditorCommandTunnelContext.Provider>
</EditorProvider>
);
};
export const EditorRoot: Component<Partial<EditorOptions> & EditorRootProps> = (
props,
) => {
const tunnelInstance = tunnel();
const { children, ...editorProps } = props;

const editor = useEditor({
...props,
extensions: editorProps.extensions ?? [StarterKit],
});

return (
<EditorProvider value={editor()}>
<EditorCommandTunnelContext.Provider value={tunnelInstance}>
<div class={props.className}>
<SolidEditorContent editor={editor()}>
{props.children}
</SolidEditorContent>
</div>
</EditorCommandTunnelContext.Provider>
</EditorProvider>
);
};
4 Replies
NotLuksus
NotLuksusOP3mo ago
import { isNodeSelection } from "@tiptap/core";
import {
BubbleMenuWrapper,
type BubbleMenuWrapperProps,
} from "@vrite/tiptap-solid";
import { createEffect, createMemo, onCleanup, onMount } from "solid-js";
import { useCurrentEditor } from "../utils/context";


export const EditorBubble = (props: EditorBubbleProps): JSX.Element => {
const editor = useCurrentEditor();
let instanceRef: Instance<Props> | null = null;

createEffect(() => {
if (!instanceRef || !props.tippyOptions?.placement) return;

instanceRef.setProps({ placement: props.tippyOptions.placement });
instanceRef.popperInstance?.update();
});

// const bubbleMenuProps: Omit<BubbleMenuWrapperProps, "children">

const memo = createMemo(() => {
const shouldShow: BubbleMenuWrapperProps["shouldShow"] = ({
editor,
state,
}) => {
const { selection } = state;
const { empty } = selection;

// don't show bubble menu if:
// - the editor is not editable
// - the selected node is an image
// - the selection is empty
// - the selection is a node selection (for drag handles)
if (
!editor.isEditable ||
editor.isActive("image") ||
empty ||
isNodeSelection(selection)
) {
return false;
}
return true;
};

return {
shouldShow,
tippyOptions: {
onCreate: (val: Instance<Props>) => {
instanceRef = val;
},
moveTransition: "transform 0.15s ease-out",
...props.tippyOptions,
},
...props,
};
});

const bubbleMenuProps: Omit<BubbleMenuWrapperProps, "editor" | "children"> =
memo();

if (!editor) return null;

return (
// We need to add this because of https://github.com/ueberdosis/tiptap/issues/2658
<div>
<BubbleMenuWrapper {...bubbleMenuProps} editor={editor}>
{props.children}
</BubbleMenuWrapper>
</div>
);
};

export default EditorBubble;
import { isNodeSelection } from "@tiptap/core";
import {
BubbleMenuWrapper,
type BubbleMenuWrapperProps,
} from "@vrite/tiptap-solid";
import { createEffect, createMemo, onCleanup, onMount } from "solid-js";
import { useCurrentEditor } from "../utils/context";


export const EditorBubble = (props: EditorBubbleProps): JSX.Element => {
const editor = useCurrentEditor();
let instanceRef: Instance<Props> | null = null;

createEffect(() => {
if (!instanceRef || !props.tippyOptions?.placement) return;

instanceRef.setProps({ placement: props.tippyOptions.placement });
instanceRef.popperInstance?.update();
});

// const bubbleMenuProps: Omit<BubbleMenuWrapperProps, "children">

const memo = createMemo(() => {
const shouldShow: BubbleMenuWrapperProps["shouldShow"] = ({
editor,
state,
}) => {
const { selection } = state;
const { empty } = selection;

// don't show bubble menu if:
// - the editor is not editable
// - the selected node is an image
// - the selection is empty
// - the selection is a node selection (for drag handles)
if (
!editor.isEditable ||
editor.isActive("image") ||
empty ||
isNodeSelection(selection)
) {
return false;
}
return true;
};

return {
shouldShow,
tippyOptions: {
onCreate: (val: Instance<Props>) => {
instanceRef = val;
},
moveTransition: "transform 0.15s ease-out",
...props.tippyOptions,
},
...props,
};
});

const bubbleMenuProps: Omit<BubbleMenuWrapperProps, "editor" | "children"> =
memo();

if (!editor) return null;

return (
// We need to add this because of https://github.com/ueberdosis/tiptap/issues/2658
<div>
<BubbleMenuWrapper {...bubbleMenuProps} editor={editor}>
{props.children}
</BubbleMenuWrapper>
</div>
);
};

export default EditorBubble;
However when I render <EditorRoot> <EditorBubble ...> </EditorRoot> I get "useEditor must be used within an EditorProvider", which I defined in the context.ts, but I don't get why
Brendonovich
Brendonovich3mo ago
You're destructing children out of props, which is causing EditorBubble to be evaluated before EditorProvider Also you're spreading props into useEditor which will do the same thing Step 1 would be to use splitProps to separate the editor props from children and class(Name) safely Step 2 you'd probably want to make useEditor take an accessor so that it can read the editor props reactively
NotLuksus
NotLuksusOP3mo ago
Alright, using split props fixed it. Why is that tho? Is a page evaluated from bottom to top / children to parents in Solid? Which now that I think about would make sense
Brendonovich
Brendonovich3mo ago
Kinda - the way i'd describe it is that components evaluate top to bottom, and then as each parent component renders its children the html elements are constructed from bottom to top The important thing is that children in most cases is implemented as get children() {}, so if you do props.children or spread outside of the component body the children will be evaluated before the parents
Want results from more Discord servers?
Add your server