reatom icon indicating copy to clipboard operation
reatom copied to clipboard

Add optional second parameter to useCreateCtx with custom ctx creation function

Open artalar opened this issue 1 year ago • 6 comments

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.

artalar avatar Jan 20 '25 17:01 artalar

So, useCreateCustomCtx at @reatom/npm-react?

MOZGIII avatar Jan 21 '25 05:01 MOZGIII

@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
}

artalar avatar Jan 21 '25 09:01 artalar

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 avatar Jan 21 '25 10:01 MOZGIII

@MOZGIII it is required for better type safety, but you can try another implementation

artalar avatar Jan 22 '25 08:01 artalar

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.

MOZGIII avatar Jan 22 '25 11:01 MOZGIII

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.

MOZGIII avatar Jan 22 '25 11:01 MOZGIII