useContext from function dynamically imported

I'm building a keyboard shortcut management system and wanted to be able to dynamically import the action associated with a keybinding. Some of these keybindings may use a context so I've intentionally put the keyboard shortcut manager provider inside of all other providers. action: () => import("../actions/switchColorScheme") and in my index file:
<I18nProvider>
<ColorSchemeProvider>
<KeyboardShortcutManagerProvider>
<Router root={App}>{routes}</Router>
</KeyboardShortcutManagerProvider>
</ColorSchemeProvider>
</I18nProvider>
<I18nProvider>
<ColorSchemeProvider>
<KeyboardShortcutManagerProvider>
<Router root={App}>{routes}</Router>
</KeyboardShortcutManagerProvider>
</ColorSchemeProvider>
</I18nProvider>
I'm getting an error when trying to useContext() in the action file's default function. Is that expected or is there a correct way of doing this? I thought that intentionally nesting the contexts where they'd normally work would be fine.
27 Replies
flippyflops
flippyflopsOP5mo ago
For reference, this is the function I'm trying to execute:
export default () => {
const { colorScheme, setColorScheme } = useColorScheme();

const foundIndex = COLOR_SCHEME_OPTIONS.findIndex(
(cs) => cs === colorScheme()
);

if (typeof foundIndex === "number") {
setColorScheme(
COLOR_SCHEME_OPTIONS[(foundIndex + 1) % COLOR_SCHEME_OPTIONS.length]
);
}
};
export default () => {
const { colorScheme, setColorScheme } = useColorScheme();

const foundIndex = COLOR_SCHEME_OPTIONS.findIndex(
(cs) => cs === colorScheme()
);

if (typeof foundIndex === "number") {
setColorScheme(
COLOR_SCHEME_OPTIONS[(foundIndex + 1) % COLOR_SCHEME_OPTIONS.length]
);
}
};
I have also verified useColorScheme works in components, as well as the KeyboardShortcutManagerProvider Also trying this with a normal function attached to the action ends up with the same error so maybe this is just a bad pattern? 🤔
Brendonovich
Brendonovich5mo ago
it depends where you're calling the function, you need to be doing so in a place that has access to the context like in a component body
flippyflops
flippyflopsOP5mo ago
ah, I thought as long as the call site was within the context it would work SomeContext -> KeyboardContext -> calls action() -> action impl -> useSomeContextContext
Brendonovich
Brendonovich5mo ago
depends how the context calls it
flippyflops
flippyflopsOP5mo ago
hmm, could you elaborate please?
Brendonovich
Brendonovich5mo ago
can you show how KeyboardContext calls action?
Madaxen86
Madaxen865mo ago
And: What’s the error message?
flippyflops
flippyflopsOP5mo ago
function onKeyDown(e: KeyboardEvent) {
if (e.repeat) return;
const upperCode = e.code.toUpperCase();
const upperKey = e.key.toUpperCase();
let finalKey: string;
if (validUniqueKeys.has(upperKey as ValidUniqueKey)) {
finalKey = upperCode.slice(3);
} else if (validUniversalKeys.has(upperKey as ValidUniversalKey)) {
finalKey = upperKey;
} else {
return;
}

const outSet = new Set(keysDown());
outSet.add(finalKey);

for (const [, { shortcut, action }] of IMC_Default) {
if (isSubsetOf(shortcut, outSet)) {
action();
return;
}
}

setKeysDown(() => outSet);
}
function onKeyDown(e: KeyboardEvent) {
if (e.repeat) return;
const upperCode = e.code.toUpperCase();
const upperKey = e.key.toUpperCase();
let finalKey: string;
if (validUniqueKeys.has(upperKey as ValidUniqueKey)) {
finalKey = upperCode.slice(3);
} else if (validUniversalKeys.has(upperKey as ValidUniversalKey)) {
finalKey = upperKey;
} else {
return;
}

const outSet = new Set(keysDown());
outSet.add(finalKey);

for (const [, { shortcut, action }] of IMC_Default) {
if (isSubsetOf(shortcut, outSet)) {
action();
return;
}
}

setKeysDown(() => outSet);
}
there is some business logic in here but essentially it will find the appropriate action based on the keys that are down in the keydown event this is inside of a provider, which is nested under the context that it needs to use within the action
Brendonovich
Brendonovich5mo ago
this is calling action inside an event handler, which is executed outside of the ownership scope even though the handler is defined inside a context, it depends where it's called
flippyflops
flippyflopsOP5mo ago
ah, so can i bind the scope with an arrow function in this situation?
Brendonovich
Brendonovich5mo ago
nah the equivalent would be getting the Owner of the context component with getOwner and using runWithOwner so the handler has access to the context, but personally i'd try find a way to not have to do that
flippyflops
flippyflopsOP5mo ago
@Madaxen86 FWIW this is the error
No description
flippyflops
flippyflopsOP5mo ago
I guess I'll need to think on this a bit more but I was trying to decouple the impl of action from the keyboard shortcut provider
Brendonovich
Brendonovich5mo ago
i'd probably just pass the values into the action handler
flippyflops
flippyflopsOP5mo ago
the value of getOwner?
createEffect(() => {
const keysToCheck: Set<string> = keysDown();
for (const [, { shortcut, action }] of IMC_Default) {
if (isSubsetOf(shortcut, keysToCheck)) {
runWithOwner(getOwner(), action);
return;
}
}
});
createEffect(() => {
const keysToCheck: Set<string> = keysDown();
for (const [, { shortcut, action }] of IMC_Default) {
if (isSubsetOf(shortcut, keysToCheck)) {
runWithOwner(getOwner(), action);
return;
}
}
});
this works fine but idk if it is "proper" so to speak
Brendonovich
Brendonovich5mo ago
nah i'd just put the result of useColorScheme into the arguments of action itself
flippyflops
flippyflopsOP5mo ago
I was thinking about something like that but then I'm in the position of the keyboard shortcut provider having to know about every possible context/arg of each action For some additional context I have things laid out like this atm: This naming is probs not great but I'm drawing inspriation from Unreal Engine's Enhanced Input System so 🤷🏼‍♂️ idk for now.
export enum InputAction {
SwitchColorScheme = "SwitchColorScheme",
}
export type InputMapping = {
/** The user-facing name of the input mapping. */
name: string;

/** A description of what the input mapping does. */
description: string;

/** The default set of keys that trigger the action. */
shortcut: Set<ValidShortcutKey>;

/** The user-defined set of keys that trigger the action. */
userShortcut: ValidShortcutKey[];

/**
* This method is called when the defined shortcut keys are pressed.
*/
action(): unknown;
};
export type InputMappingContext = Map<InputAction, InputMapping>;
export enum InputAction {
SwitchColorScheme = "SwitchColorScheme",
}
export type InputMapping = {
/** The user-facing name of the input mapping. */
name: string;

/** A description of what the input mapping does. */
description: string;

/** The default set of keys that trigger the action. */
shortcut: Set<ValidShortcutKey>;

/** The user-defined set of keys that trigger the action. */
userShortcut: ValidShortcutKey[];

/**
* This method is called when the defined shortcut keys are pressed.
*/
action(): unknown;
};
export type InputMappingContext = Map<InputAction, InputMapping>;
the KeyboardShortcutManagerProvider is just a binding between the user's keyboard inputs and the InputMappingContext (again probs a bad name for JS world) I guess I'm just not seeing how I could call any of the actions which may or may not use any context within the app from the keyboard shortcut provider without somehow coupling every argument type to the provider Maybe basing this loosely on game development practices was a bad idea too lol 🤷🏼‍♂️
REEEEE
REEEEE5mo ago
I have something like this for my project and I just made a global shortcut manager
flippyflops
flippyflopsOP5mo ago
Would you mind sharing your implementation or some details? Or if you're up for it I could hop into a call and share what I have too
REEEEE
REEEEE5mo ago
I'm just exporting my manager from a file like this
export const keyMapper = createRoot((dispose) => {
onCleanup(dispose)
return useKombos()
})
export const keyMapper = createRoot((dispose) => {
onCleanup(dispose)
return useKombos()
})
then I can just do
keyMapper.addCombo([...list of keys], () => {
// my action callback
})
keyMapper.addCombo([...list of keys], () => {
// my action callback
})
flippyflops
flippyflopsOP5mo ago
And are you able to use other contexts in the function without issue? I'm not familiar with using multiple createRoots yet as I'm pretty new to solid
keyMapper.addCombo([...list of keys], () => {
const something = useSomeOtherContext();

})
keyMapper.addCombo([...list of keys], () => {
const something = useSomeOtherContext();

})
REEEEE
REEEEE5mo ago
hmm I haven't tested it but in general doing useContext inside an event handler doesn't work :P let me test nope does not work
flippyflops
flippyflopsOP5mo ago
That's what @Brendonovich was saying which makes sense after his explanation but now it is forcing me to rethink everything or couple things tightly TL;DR; I want to run arbitrary code within the shortcut manager and have access to any of the contexts
REEEEE
REEEEE5mo ago
Hmm
flippyflops
flippyflopsOP5mo ago
Thanks to @Brendonovich for bringing up the concept of owners, this is now working but IDK if it is "proper" or "best practice" to do something like this
createEffect(() => {
const keysToCheck: Set<string> = keysDown();
for (const [, { shortcut, action }] of IMC_Default) {
if (isSubsetOf(shortcut, keysToCheck)) {
runWithOwner(getOwner(), action);
return;
}
}
});
createEffect(() => {
const keysToCheck: Set<string> = keysDown();
for (const [, { shortcut, action }] of IMC_Default) {
if (isSubsetOf(shortcut, keysToCheck)) {
runWithOwner(getOwner(), action);
return;
}
}
});
REEEEE
REEEEE5mo ago
Yeah just tested it by adding something similar I'm doing the runWithOwner(getOwner(), action) as part of the addCombo except I'm reading getOwner outside so like
const addCombo = (keys, action) => {
const owner = getOwner()

setCombos((p) => [...p, {keys, action: () => runWithOwner(owner, action)}])
}
const addCombo = (keys, action) => {
const owner = getOwner()

setCombos((p) => [...p, {keys, action: () => runWithOwner(owner, action)}])
}
fwiw I think this is fine to do
flippyflops
flippyflopsOP5mo ago
nice, I think in order to decouple the arbitrary code, it needs this behavior thanks to everyone for talking this through with me 😃
Want results from more Discord servers?
Add your server