dyo / dyo

Dyo is a JavaScript library for building user interfaces.

Home Page:https://dyo.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Remarks on the return type of "useContext"

mcjazzyfunky opened this issue · comments

TypeScript is a an awesome programming language, especially due to its great automatic type inference.
One awesome thing about the React hook API is that it supports type inference in a very good way.
Same for Dyo, with one single exception: The return type of "useContext" is any, unfortunately not the real type of the context value.
That means in TypeScript (some day in future when Dyo will have a index.d.ts file) you will always have to write
const someValue = useContext(SomeProvider) as SomeType
which is tbh not really that nice.
I do not know a nice and simple way to handle this in Dyo's future index.d.ts file. Of course the type of Dyo components could be something like Component<Props = {}, Ctx = undefined>, instead of just Component<Props = {}> - or something similar), so that additional context type information could be used with useContextbut that does not seem to be a very elegant solution, does it?

@thysultan
This is just a remark. With the current context API this may be just part of the game.
I don't think there's something to do here. Feel free to close this issue whenever you like.

I've noodled around a bit with Dyo's context API today:
Isn't the context API just a bit too powerful (while in the same time it does not even support default context values out of the box)? This dyo.Context thingy adds quite a lot of flexibility, but I frankly do not even know, where this additional flexibility is actually needed in the real world, while it causes a lot of trouble for possible type inference in TypeScript.

Please have a look at the following demo:
https://codesandbox.io/s/cool-black-i0d0c
In this example it would be impossible for a TS compiler to automatically infer the type of the result value of useContext(Localize).

Maybe a bit naive, but wouldn't it be better to get completely rid of that dyo.Context API thing and replace it by a function dyo.createContext<T>(defaultValue: T) which returns a canonical context Provider of type (props: { value: T, key?, children? }) => VirtualNode and can be used as input argument for the useContext hook?

Type inference would not be a problem anymore and default context values would also be supported.
Or am I just missing something?

PS: The following two lines in the current alpha version of index.d.ts will for example not work for this Localize context provider in my above demo:

interface Context<Value> extends Component<{value: Value, children: Collection}> {}

function useContext<Value>(context: Context<Value>): Value

Type inference would not be a problem anymore and default context values.

You can definitely create a React-like createContext API with useContext and Context thingy.

TS compiler to automatically infer the type of the result value of useContext(Localize).

That's what i was trying to figure out in the comment, intuitively it should be possible, though i maybe wrong.

For example if we could pick the props of the return type of the Localize component, and from that pick the type of the 'value" property:

function useContext<Value extends Component>(context: Value): Pick<Pick<ReturnType<Value>, 'props'>, 'value'>

My demo was obviously not "evil" enough ... I've just made it a bit uglier, to show why I think that context API is a just too powerful => You cannot just take the return type of that Localize function now and try to infer the Value type in a general way:
https://codesandbox.io/s/relaxed-darwin-vj07g
(.... and btw: why should anyone ever want to implement a context provider like this?)

You can definitely create a React-like createContext API with useContext and Context thingy.

Yes you can, but then you always have to use your own useContext counterpart function if you want for example default context value support.
Maybe I'm just wrong - but currently I think, if I wrote an app based on Dyo I would first implement a helper function in userland to bypass the above mentioned issues with useContext:

// default context value is easy to implement here
const [LocaleProvider, useLocale] = defineContext('en') 

... and I am not really sure whether this is really ideal.

My demo was obviously not "evil" enough...

The component could also be written as:

function Localize({ country = "US", language = "en", children }) {
  const locale = `${language}_${country}`;

  return h(Context, {value: locale},
    h('div', null, 
      h('div', 'The following content will be localized:'), 
      children
    )
  )
}

why should anyone ever want to implement a context provider like this

It's important to note that Localize assumes the mantle of a "Provider". So in the context of good typescript inference, it is up to it to uphold this contract, i.e by returning a Context root as far as the typescript contract is concerned, beyond that – yes it is probably impossible for typescript to infer a type outside of your own custom hook.

Outside of Typescript and Typescript's automatic type inference is this an issue?

  • The fact that useContext does not support default values is really, really a pity
  • Moreover, I still cannot see what really useful things can be done with Dyo's context API which cannot also be done with React much simpler and easily typeable context API
  • In React a context can have multiple providers (for example FormCtx as context and the components Form, BasicForm, AdvancedForm, DataForm all as FormCtx providers) where none of those providers is more important than the other providers (the canonical Provider SomeCtx.Provider is actually also not really that special). In contrast, with Dyo a context can also have multiple providers per context but there is this ONE SPECIAL Provider that is more important than the others due to the fact that this one can be used as input for useContext and the others cannot. Why shall one provider be "special"? I think it really makes sense to distinct the concept of a context from the concept of a context provider (like it done in React).

BTW: I'm strongly of the opinion that in most cases where defining proper typing is practically and theoretically not really 100% possible there is a really good chance that using a slightly different but typeable API (in most cases even simpler API) may be a much better solution.

The fact that useContext does not support default values

Yes i was trying to move away from the concept of allowing default values, that is that the Provider -> Consumer contract should be strict enough where you can with some level of certainty know where the context value is coming from(not from union of either the provider/default value): In which case useContext is still not strict enough and should probably explicitly throw in such cases.

there is this ONE SPECIAL Provider that is more important than the others due

What do you mean by this? All providers are equally special, that is they are not special. A canonical "provider" to a consumer is any function passed to useContext that provides context. In React a provider is Context.ProviderForm, BasicForm, AdvancedForm, DataForm would be "equivalent" providers in their notion of returning the canonical provider Context.Provider. In React there's only every one single provider per context createContext(T): {Consumer, Provider}.

Yes i was trying to move away from the concept of allowing default values [...]

Ah okay, thanks for informing about the idea behind it ... especially that "should probably explicitly throw in such cases" part.
But nevertheless, aren't there a lot of use cases where components shall also work without explicitly using corresponding providers?

  • Shouldn't a date picker not out of the box just use the default theme and English as default language and ISO date format even if ThemeProvider and LocaleProvider have not been used?

  • Shouldn't an input control not also work if no FormCtx value has been set by a provider (like <input .../> also works even if it is not somehow embedded in a <form .../> element)?

  • If some complex business components use some logging functionality, shouldn't they not just use a null-logger by default (without even knowing that this default logger is a null-logger - could also be a console logger)? In most apps this build-in logging functionality will not even be used - so why should each of the apps use that null-logger provider explicitly all the time?

What do you mean by this? All providers are equally special, that is they are not special.

https://codesandbox.io/s/confident-wright-kfor2

seems that LocaleProvider1 is kinda "special"

The component LocaleProvider2 is not a provider in the same way that the component Demo is not one as well.

Shouldn't a date picker...

A date picker would provide it's own provider, in what scenario would/should this come from outside?

function DatePicker (props) {
    return h(Context, {value: '...'}, ...etc)
}

If a third party Dyo library only consists of LocaleProvider1 and LocaleProvider2 (nobody would do that in real-world of course, but anyway), it would not be clear without additional information which of those providers you have to be passed to Dyo's useContext hook function.
In React you would have LocaleProvider1, LocaleProvider2 and LocaleCtx and everyone would know what has to be passed to React's useContext hook function (this is what I meant by saying a distinction between "Context" and "Context Provider" could make sense).

[...] in what scenario would/should this come from outside?

funtion DatePicker({ value, label .... }) {
  const
     theme = useContext(ThemeCtx),
     locale = useContext(LocaleCtx),
     form = useContext(FormCtx),

     // I do not know what a date picker should log, but anyway....
     logger = useContext(LoggerCtx),

     // this is to determine whether the label
     // shall be placed above the input control
     // or on the left side (which may not be easily done
     // by CSS) - not very common but also not necessarily an anti-pattern
     formViewMode = useContext(FormViewModeCtx),

  ...
}

I would assume it is what whatever it is documented as either explicitly in documentation or implicitly in what is exported and how one names it.

In fact i'd argue in what that example is trying to do you shouldn't need two:

function LocaleProvider (props) { return h(Context, {value: ...}, ...) }

That instead of the consumer needing to juggle two references Context and Provider they can hold onto just on reference that serves the purpose of both: in both useContext(Provider) and in h(Provider).

theme = useContext(ThemeCtx)

Why the indirection, why is the theme coming from a single outside context provider and not from props?

Why the indirection, why is the theme coming from a single outside context provider and not from props?

Because then you would have to pass that theme property to each single themeable component explictly ... isn't theming THE standard example for the use of contexts?

Anyway, I do not want to steal too much time from you ... this seems to be one of the cases where it's fine to agree to disagree ;-)

Thanks a lot for the discussion and sharing your thoughts and ideas.
I will close this issue now.

Because then you would have to pass that theme property to each single themeable component explictly

Yes, but the theme provider can provide the default value.

function ThemeProvider ({value = {...defaultTheme}, children}) {
    return h(Context, {value}, children)
}

render(h(ThemeProvider, {}, h(Indirection, {}, h(DatePicker))), document)