Add optional second parameter to useCreateCtx with custom ctx creation function
Discussed in https://github.com/artalar/reatom/discussions/994
Originally posted by MOZGIII December 30, 2024 I've been doing some development with reatom, and figured we need a way to create a test ctx on a per-page basis. We have some dev-routes with pages that need mocking - sort of like storybooks but at the same app code as the rest of the app.
import { Fn } from "@reatom/framework";
import { createTestCtx, TestCtx } from "@reatom/testing";
import { useRef } from "react";
export const useCreateTestCtx = (extension?: Fn<[TestCtx]>) => {
const ctxRef = useRef<null | TestCtx>(null);
if (!ctxRef.current) {
ctxRef.current = createTestCtx();
extension?.(ctxRef.current);
}
return ctxRef.current;
};
It would also be fine to have a just more generic way to create and destroy the contexts, so that useCreateCtx doesn't invoke the the createCtx itself but rather allows the user to do so.
import { Ctx, Fn } from "@reatom/framework";
import { useRef } from "react";
export const useCreateCustomCtx = <T extends Ctx>(create: Fn<[], T>) => {
const ctxRef = useRef<null | T>(null);
if (!ctxRef.current) {
ctxRef.current = create();
}
return ctxRef.current;
};
The second variant should work better as it does not have direct dependency on both react and a @reatom/testing - which would make it quite puzzling where to place this code.
So, useCreateCustomCtx at @reatom/npm-react?
@MOZGIII there is no need for additional API, base hook would work fine with new parameter. I think, it should be like this:
export const useCreateCtx = <T extends typeof createCtx = typeof createCtx, C extends Ctx = ReturnType<T>>(
extension?: Fn<[C]>,
createFn: T = createCtx as T,
): C => {
const ctxRef = React.useRef(null as null | C)
if (!ctxRef.current) {
ctxRef.current = createFn() as C
extension?.(ctxRef.current)
}
return ctxRef.current
}
Too much generics imo, I'd say we need either T or C generics, not both. Or is there a good reason to have them?
@MOZGIII it is required for better type safety, but you can try another implementation
type CreateCtxFn = <C extends Ctx>() => C;
export const useCreateCtx = <T extends CreateCtxFn = typeof createCtx>(
extension?: Fn<[ReturnType<T>]>,
createFn: T = createCtx as T,
): C => {
const ctxRef = React.useRef<null | ReturnType<T>>(null)
if (!ctxRef.current) {
ctxRef.current = createFn()
extension?.(ctxRef.current)
}
return ctxRef.current
}
Ok, how about this? Might need to test it, just wrote it here... But the idea is similar, yet different. Here we disallow separating the C type from T by forcing a generic type. It should also allow us to eliminate the as type pinnings and allow TypeScript to derive the code.
The only thing I'm still worried about is this createFn: T = createCtx as T - it is a soundness hole. Although thus is a niche thing, and will likely be fine, I still don't like it. We could probably define this as an overload instead of an optional parameter for better safety if we want this kind of behavior.
Also, why have this signature in the first place, if we could just do this:
type CreateCtxFn = <C extends Ctx>() => C;
export const useCreateCtx = <T extends CreateCtxFn = typeof createCtx>(
createFn?: T = createCtx as T,
): C => {
const ctxRef = React.useRef<null | ReturnType<T>>(null)
if (!ctxRef.current) {
ctxRef.current = createFn()
}
return ctxRef.current
}
The extension seems kind of redundant.
Plus, it looks like we could just do const ctx = useMemo(createCtx, []); with the same effect.
In our case that createCtx would be our own fn that allows us using test ctx and mocks in development if a certain env var is set.