Skip to content

Add new RenderProps-style Context from React 16.3#24509

Merged
johnnyreilly merged 4 commits intoDefinitelyTyped:masterfrom
scally:react-16.3-context
Apr 16, 2018
Merged

Add new RenderProps-style Context from React 16.3#24509
johnnyreilly merged 4 commits intoDefinitelyTyped:masterfrom
scally:react-16.3-context

Conversation

@scally
Copy link
Copy Markdown
Contributor

@scally scally commented Mar 25, 2018

React 16.3, the latest version, has a new recommended API for Context, with Context, Provider, and Consumer typings.

These changes are merged in this PR to React:
facebook/react#11818

Please fill in this template.

  • Use a meaningful title for the pull request. Include the name of the package modified.
  • Test the change in your own code. (Compile and run.)
  • Add or edit tests to reflect the change. (Run with npm test.)
  • Follow the advice from the readme.
  • Avoid common mistakes.
  • Run npm run lint package-name (or tsc if no tslint.json is present).

Select one of these and delete the others:

If changing an existing definition:

@scally scally requested a review from johnnyreilly as a code owner March 25, 2018 06:16
@typescript-bot typescript-bot added Popular package This PR affects a popular package (as counted by NPM download counts). Awaiting reviewer feedback labels Mar 25, 2018
@typescript-bot
Copy link
Copy Markdown
Contributor

typescript-bot commented Mar 25, 2018

@scally Thank you for submitting this PR!

🔔 @johnnyreilly @bbenezech @pzavolinsky @digiguru @ericanderson @morcerf @tkrotoff @DovydasNavickas @onigoetz @theruther4d @guilhermehubner @JoshuaKGoldberg @jrakotoharisoa - please review this PR in the next few days. Be sure to explicitly select Approve or Request Changes in the GitHub UI so I know what's going on.

If no reviewer appears after a week, a DefinitelyTyped maintainer will review the PR instead.

Copy link
Copy Markdown

@rhmoller rhmoller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ComponentType instead of Component and add support for bitmasks.

An example of the alternative defitnion can be seen in this CodeSandbox:
https://codesandbox.io/s/l3j167ljq?module=src/react-context.d.ts

...children: ReactNode[]): ReactElement<P>;

// Context via RenderProps
type Provider<T> = () => Component<{
Copy link
Copy Markdown

@rhmoller rhmoller Mar 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type Provider<T> = ComponentType<{

value: T,
children?: ReactNode
}>;
type Consumer<T> = () => Component<{
Copy link
Copy Markdown

@rhmoller rhmoller Mar 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type Consumer<T> = ComponentType<{

}>;
type Consumer<T> = () => Component<{
children: (value: T) => ReactNode,
}>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing field:

unstable_observedBits?: number;

Provider: Provider<T>;
Consumer: Consumer<T>;
}
function createContext<T>(defaultValue: T): Context<T>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createContext has an optional second argument:

calculateChangedBits?: (prev: T, next: T) => number

@typescript-bot typescript-bot added the Revision needed This PR needs code changes before it can be merged. label Mar 25, 2018
@typescript-bot
Copy link
Copy Markdown
Contributor

typescript-bot commented Mar 25, 2018

@scally Unfortunately, this pull request currently has a merge conflict 😥. Please update your PR branch to be up-to-date with respect to master. Have a nice day!

@scally
Copy link
Copy Markdown
Contributor Author

scally commented Mar 26, 2018

@rhmoller Thank you for the review! I have addressed your feedback and rebased.

@typescript-bot typescript-bot added The Travis CI build failed and removed Revision needed This PR needs code changes before it can be merged. labels Mar 26, 2018
@scally
Copy link
Copy Markdown
Contributor Author

scally commented Mar 26, 2018

The travis build failure looks spurious, although I don't see a way to retry it.

Copy link
Copy Markdown

@rhmoller rhmoller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. The Travis build seems to originate from the react-flatpickr project, so unrelated to your changes.

@typescript-bot typescript-bot added the Other Approved This PR was reviewed and signed-off by a community member. label Mar 26, 2018
@xogeny
Copy link
Copy Markdown
Contributor

xogeny commented Mar 26, 2018

FYI, I wrote up an article on using the new Context API with TypeScript. I used the current definitions in the PR (linking back here) in the article. I'll probably start publicizing the article tomorrow but I just wanted to circulate it here in case anybody had any constructive feedback on it:

https://medium.com/@mtiller/react-16-3-context-api-intypescript-45c9eeb7a384

function createContext<T>(
defaultValue: T,
calculateChangedBits?: (prev: T, next: T) => number
): Context<T>;
Copy link
Copy Markdown
Member

@Jessidhia Jessidhia Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to call createContext() to create one with defaultValue of undefined.

function createContext<T>(): Context<T|undefined> override? This is one of those that look like the antipattern warned against in the docs, but I think it's correct to use here, similar to a collection constructor.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I'm confused here. Calling createContext() (i.e., no arguments) would require the defaultValue to be optional. Otherwise you'd need to do an explicit createContext(undefined). Also, since T can be anything, I don't understand T|undefined.

FYI, I address this case of optional default values in my article on the new Context API (using the typings as they stand here, but wrapping them in one additional layer). Also note that, in my opinion (see article), it isn't just about making the default value optional. But again, I used these typings without issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16.3 released today and the docs seem to indicate @Kovensky is correct here — they use ‘createContext’ without args so it must be a correct usage. I’ll take a look at updating later today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, rebased this post merge conflict.

Spent some time with this tonight and it looks like the behavior of the final API is that
createContext() gives you a context object where _defaultValue is undefined.

createContext('foo') gives you a context object where _defaultValue is foo

Given that, I'm a bit confused as to the correct typing for createContext() with no params. I tried the overload @Kovensky mentioned but doing so types the resulting RenderProps function args with {} or undefined, which I don't think is correct.

I suspect the overload closest to the API's usage would be

function createContext(): Context<any>

Which completely gives up type safety for RenderProps function but at least allows it to be used. {} or undefined doesn't allow you to still give, say, an anonymous object in the Provider's value field and still be able to retrieve it later in the RenderProps.

If anyone has any opinions on this they'd be welcome.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I'm just saying that we shouldn't want to correct that behavior, because you can always use Consumer not paired with a Provider so the value of T should not change even if you later pass in the value as a prop to Provider.

I.e. if you create the context with the default value, you are always guaranteed to have a defined value wherever you use Consumer (even not as a descendant of Provider), but once you create the context without a default value, you are not guaranteed that defined value unless you wrap the Consumer inside a Provider, which wouldn't be able to be statically analyzed before runtime.

I'm agreeing with you that it's unfortunate that we'll always have to check for undefined, but I think that should be the way the interface is structured due to the lack of compile-time guarantees of the consumer value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in your first example you had the comment there /* value here will be defined, even though it's typed as optional */ and that was what's confused me the most. As you have said now, you will have to check for undefined either way. I understand now.

I see a bit of problem arising here. Developers are generally lazy and we might end up with pattern like this.

interface MyContextType { myValue: string }
const Context = createContext<MyContextType>()

<Context.Consumer>{value => { 
  <div>{value!.myValue.toUpperCase()}</div> // <-- notice the "!" mark there
</Context.Consumer>

That might be a good source of runtime errors (and blaming TypeScript/React for it) in case someone really forgets to add the Provider. It's true that if anyone is using ! it's for own risk, but there are people that might just copy&paste and then be surprised.


What about this approach? Would this work the same way? I am not sure if it's a correct syntax, but you get the gist. This would at least allow to override this undefined type in case someone knows what they are doing.

function createContext<T, S = T | undefined>(): Context<S>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. it might not be immediately clear to the user why the second generic type param may be used, and they'll have to dig into the type defs to actually find out how they resolve.

With my suggestion (the two overloads), creating context with a default param will assert the value in the consumer to be always be defined:

interface ContextProps {
  foo: string
  bar?: number
}

// note the default values passed in as the first parameter
const Context = createContext<Props>({ foo: 'foo' }) // typed as Props
const OtherContext = createContext({ foo: 'foo' }) // inferred to be type { foo: string }

// even without a Provider it is still typed to be required
const app = (
  <App>
    <Context.Consumer>
      {( value ) => /* value is typed to be Props here, no need to check for undefined */ }
    </Context.Consumer>
    <Context.Consumer>
      {( value ) => /* value is typed to be { foo: string } here, no need to check for undefined */ }
    </Context.Consumer>
  </App>
)

in this case, all you need as a type consumer to override the undefined behavior is to just provide a default value, the type inference will work for you or you can explicitly set the generic type parameter, and as long as the default value is passed in as a parameter, the consumer render props will always be defined

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ferdaber I'm trying to use the TypeScript 2.8 features you mentioned in this definition, but it looks like that's not possible yet?

I've updated the headers to // TypeScript Version: 2.8 but that results in a linting error.

According to this comment, we can only use TypeScript up to 2.7 right now, so I may not be able to use the definition you specify.

#24586 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, that's too bad, we'll keep it in the backburner then

type Provider<T> = ComponentType<{
value: T,
children?: ReactNode
}>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure these should be ComponentType; similarly to a Fragment, they can't actually be constructed, don't have a propTypes property, etc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you say they can't be constructed? That's exactly how they are used (or perhaps I'm misunderstanding what you mean by "constructed").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kovensky do you have a suggestion for what types they should be?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "constructed" I mean given as argument to new. They're not even functions, as you can see in their definition, but unfortunately TSC doesn't support that 😞

I guess we're stuck with ComponentType here, at least until TSC is updated to use the overloads of React.createElement for JSX type checking.

microsoft/TypeScript#21699

@scally
Copy link
Copy Markdown
Contributor Author

scally commented Mar 30, 2018

@timwangdev check in regards to what? Did you have a concern about some part in particular?

value: T,
children?: ReactNode
}>;
type Consumer<T> = ComponentType<{
Copy link
Copy Markdown
Contributor

@ferdaber ferdaber Mar 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worthwhile to refactor the props interface of Consumer and Provider in case those props are spread from another place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ferdaber I'm not sure I understand the specific refactoring you want here. Could you elaborate?

Copy link
Copy Markdown
Contributor

@ferdaber ferdaber Mar 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm talking about refactoring the props as their own exported interface like this:

interface ConsumerProps<T> {
  children(value: T): ReactNode,
  unstable_observedBits?: number
}
interface ProviderProps<T> {
  children?: ReactNode
  // if you choose to use my suggestion, otherwise just T
  value: T extends undefined ? never : T 
}

// this way a consumer of the type can import `ConsumerProps` and `ProviderProps` for other purposes
type Consumer<T> = ComponentType<ConsumerProps<T>>
type Provider<T> = ComponentType<ProviderProps<T>>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm.. When I try that, in the test code proposed below it no longer is able to determine the type of value:

const ContextRenderPropsConsumerComponent = (
    <ContextWithDefaultValue.Consumer>
        {value => value.assert} // <--- Value here is now "any" with the props refactor
    </ContextWithDefaultValue.Consumer>
);

See the test example below for the full proposed test code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ferdaber It looks like the refactoring you proposed might cause some issues as well, listed in the comment above. Do you see a way around them?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

odd... I tested it out and it works fine for me, these are my relevant snippets (I copied your test code as-is):

// react/index.d.ts

    interface ProviderProps<T> {
        value: T;
        children?: ReactNode;
    }

    interface ConsumerProps<T> {
        children: (value: T) => ReactNode;
        unstable_observedBits?: number;
    }

    type Provider<T> = ComponentType<ProviderProps<T>>;
    type Consumer<T> = ComponentType<ConsumerProps<T>>;
    interface Context<T> {
        Provider: Provider<T>;
        Consumer: Consumer<T>;
    }
    function createContext<T>(
        defaultValue: T,
        calculateChangedBits?: (prev: T, next: T) => number
    ): Context<T>;
    function createContext<T>(): Context<T | undefined>;
// react/test/tsx.tsx

import React from '.'

const RenderPropsContext = React.createContext('defaultValue');

interface RenderPropsContextTest {
    assert: boolean;
}

const ContextWithDefaultValue = React.createContext<RenderPropsContextTest>({ assert: true });
const ContextWithoutDefaultValue = React.createContext<RenderPropsContextTest>();

const ContextRenderPropsConsumerComponent = (
    <ContextWithDefaultValue.Consumer>
        {value => value.assert}
    </ContextWithDefaultValue.Consumer>
);

const ContextRenderPropsProviderConsumerComponent = (
    <ContextWithoutDefaultValue.Provider value={{ assert: true }}>
        <ContextWithoutDefaultValue.Consumer>
            {value => value.assert}
        </ContextWithoutDefaultValue.Consumer>
    </ContextWithoutDefaultValue.Provider>
); // $ExpectError

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scally wrote:

When I try that, in the test code proposed below it no longer is able to determine the type of value:

perhaps that was an IDE bug? There are cases where Webstorm & IntelliJ IDEA don't show inferred argument type correctly (claims it is any), but when used incorrectly it still emits error with message mentioning correct type of the argument

<StatelessComponentWithoutProps />;

// React.createContext
const ContextWithRenderProps = React.createContext('defaultValue');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the way too simple test to cover the changes. I would propose following, but I am not entirely sure how to run these tests, so there might some mistake (or several)

interface ContextTest = {
  assert: boolean
}
const ContextWithValue = React.createContext<ContextTest>({ assert: true })
const ContextWithoutValue = React.createContext<ContextTest>()

<ContextWithValue.Context>{value => value.assert}</ContextWithValue.Context>

<ContextWithoutValue.Provider value={{ assert: true }}>
  <ContextWithoutValue.Context>{value => value.assert}</ContextWithoutValue.Context>
</ContextWithoutValue.Provider>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FredyC I'm not sure myself. The test files here don't seem to be assertion-style tests so much as a sandbox to prove that the typings work without syntax errors.

I agree that there could be more code here exercising both context scenarios (with and without defaults), but I don't know that they should be executable as none of the rest of the code in this file seems to be.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yes, tests code is not executed, because you're testing types, not React's behavior.
But tests shouldn't always be positive, if you think that there might be some usage that is not correct, but could be assumed as correct one.

What you have to do for failing tests is add // $ExpectError line immediately before the test line. More about that in DT readme:
https://github.com/DefinitelyTyped/DefinitelyTyped#lint
And dtslint readme:
https://github.com/Microsoft/dtslint#write-tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scally Are you willing to try that? Perhaps it's overkill for such relatively simple thing. It's your call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FredyC Yes, I'll try it out and see where I get with it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying something like this:

const RenderPropsContext = React.createContext('defaultValue');

interface RenderPropsContextTest {
    assert: boolean;
}

const ContextWithDefaultValue = React.createContext<RenderPropsContextTest>({ assert: true });
const ContextWithoutDefaultValue = React.createContext<RenderPropsContextTest>();

const ContextRenderPropsConsumerComponent = (
    <ContextWithDefaultValue.Consumer>
        {value => value.assert}
    </ContextWithDefaultValue.Consumer>
);

const ContextRenderPropsProviderConsumerComponent = (
    <ContextWithoutDefaultValue.Provider value={{ assert: true }}>
        <ContextWithoutDefaultValue.Consumer>
            {value => value.assert}
        </ContextWithoutDefaultValue.Consumer>
    </ContextWithoutDefaultValue.Provider>
); // $ExpectError

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FredyC Are these tests sufficient for what you feel should be achieved here?

Copy link
Copy Markdown
Member

@Jessidhia Jessidhia Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange that the error is in the full statement instead of in the argument to Consumer, but seems to be the case.

An additional non-erroring test where the argument is value => value === undefined ? null : value.assert would also help demonstrate it.

There's one last confirmation I'd like to take; if the Context is created with a default value, will the Consumer receive that value even if it's not put inside its Provider in the tree? 🤔 looks at source

EDIT: The context's context has a copy of the default argument as its value, and it's only overwritten by the reconciler if the Provider is present. It's also restored to its original value once the Provider tree has been processed. So indeed it can only be undefined if you passed something that can be undefined (or nothing) to the createContext call.

@typescript-bot typescript-bot added Revision needed This PR needs code changes before it can be merged. and removed Other Approved This PR was reviewed and signed-off by a community member. labels Mar 30, 2018
@typescript-bot typescript-bot added Has Merge Conflict This PR can't be merged because it has a merge conflict. The author needs to update it. and removed Revision needed This PR needs code changes before it can be merged. labels Apr 2, 2018
// Context via RenderProps
type Provider<T> = ComponentType<{
value: T,
children?: ReactNode
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the children be made mandatory? Not much point in a Provider without any children.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it really matter since ReactNode already accounts for undefined? I can see a scenario where someone may not render an app while waiting for an API result but would still like to layer a context provider on top of it (undefined or null children)

Copy link
Copy Markdown
Contributor

@danielkcz danielkcz Apr 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that ReactNode is wrong altogether, it should not include undefined. See this #23422 (review) from Dan. It should be better eventually when this is resolved ... microsoft/TypeScript#21699.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, ReactNode is definitely wrong. If you actually use ReactNode anywhere, you'll get a warning from React.

I think the only time it doesn't warn is if you put it as an array element, but otherwise it's a warning.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to be ReactNode due to the above TypeScript issue though, otherwise it's impossible to use React with noImplicitAny. After the above issue is resolved this and many more types can be cleaned up but right now there is no option.

Copy link
Copy Markdown
Contributor

@danielkcz danielkcz Apr 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note, I do have a custom alias for this so I can actually have render prop SFC components and I am ignoring the type from ofic. typings. declare type ReactNode = React.ReactElement<any> | null. When this is resolved correctly, I can easily update this alias. It's not really useful here, but works well otherwise :)

jostnes pushed a commit to jostnes/gutenberg that referenced this pull request Mar 23, 2022
…WordPress#39526)

Part of WordPress#39211

Previously we have been calling `createContext()` without any
arguments, but there's a funny note in the official React type
for that function.

> // If you thought this should be optional, see
> // DefinitelyTyped/DefinitelyTyped#24509 (comment)

Although not passing an argument is practically the same as passing
`undefined` as the argument we have a type error that TypeScript
doesn't like while we're relying on it to parse JS files with the
JSDoc typings.

In this patch we're just adding the explicit `undefined` which should
have no behavioral change on the output but removes the type issue.
@alamothe
Copy link
Copy Markdown

alamothe commented Sep 4, 2022

I don't understand the reasoning at all. In 95%+ of the cases, a context provider will be rendered in the tree. A default value is non-sensical, it implies that a context can be used without an explicit provider, but it cannot.

E.g. think a default value for Apollo client, react router, react-navigation etc.

We ended up patching @types/react to avoid compile errors.

@luxalpa
Copy link
Copy Markdown

luxalpa commented Sep 4, 2022

You can do createContext<MyContextType>(null!) as a workaround.

@PatrissolJuns
Copy link
Copy Markdown

@luxalpa

You can do createContext(null!) as a workaround.

null! ? Are you sure there is "!" at the end ?

@luxalpa
Copy link
Copy Markdown

luxalpa commented Oct 6, 2022

@PatrissolJuns Yes, the ! is a hack to tell typescript a lie, which is that this null is actually not null, but the data type that it expects. :)

@Pascal-Ahmadu
Copy link
Copy Markdown

Capture
please i need help with this any ideas?

@Inoir
Copy link
Copy Markdown

Inoir commented Nov 2, 2022

rly aweful, you need to declare it as default value and as a value on the Provider.

const UserStoreContext = createContext(new UserStore())

export const UserStoreProvider = UserStoreContext.Provider
export const UserStoreConsumer = UserStoreContext.Consumer
import UserDisplay from "./UserDisplay"
import { UserStore, UserStoreProvider } from "./stores/user"

function App() {
  return (
    <UserStoreProvider value={new UserStore()}>
      <UserDisplay />
    </UserStoreProvider>
  )
}

both is required in typescript without any reason in my opinion. if youre forced to set a default value, the value on the provider instance can be optional

so only way to make it useable is like this

const StoreInstance = new UserStore()

const UserStoreContext = createContext(StoreInstance)

export function UserStoreProvider({ children }: PropsWithChildren) {
  return (
    <UserStoreContext.Provider value={StoreInstance}>
      {children}
    </UserStoreContext.Provider>
  )
}

export const UserStoreConsumer = UserStoreContext.Consumer

@cbodin
Copy link
Copy Markdown
Contributor

cbodin commented Mar 7, 2023

Edit: Updated example with the suggestion from @nlukk that will allow null values in the context by using a symbol as a default value.

As this thread seems to pop up here and there, a better solution than workarounds using non-null assertions and risking undefined behaviour seems merited.

We can use the default value as an indication that the context is missing a value in the tree and throw an error.

This won't ensure build-time safety but provides a great developer experience as mistakes done by the developer will show up with an error describing exactly what and where the problem is.

Here's a complete example including both a hook and consumer:

export enum Theme {
  Dark = "dark",
  Light = "light"
}
/** Symbol used as default value of context. */
const defaultValue = Symbol();

/** Non-exported context so we can force how it's consumed and provided. */
const ThemeContext = createContext<Theme | typeof defaultValue>(defaultValue);

/** Only exporting the provider as we want to manually handle the consumer. */
export const ThemeProvider = ThemeContext.Provider;

/** Context consumer where the value is non-nullable. */
interface ThemeConsumerProps {
  children: (value: Theme) => ReactNode;
}

export const ThemeConsumer: FunctionComponent<ThemeConsumerProps> = ({
  children
}) => {
  return (
    <ThemeContext.Consumer>
      {(theme) => {
        if (theme === defaultValue) {
          throw new Error(
            "<ThemeConsumer /> used without a <ThemeProvider /> in the tree."
          );
        }

        return children(theme);
      }}
    </ThemeContext.Consumer>
  );
};

/** Hook so we can use the context value without needing to do null checks. */
export const useTheme = (): Theme => {
  const theme = useContext(ThemeContext);
  if (theme === defaultValue) {
    throw new Error(
      "useTheme() called without a <ThemeProvider /> in the tree."
    );
  }

  return theme;
};

If we try to use the hook or consumer without a <ThemeProvider /> in the tree:

const theme = useTheme();
<ThemeConsumer>{(theme) => <>The theme is {theme}</>}</ThemeConsumer>

We'll get friendly error messages:

image

image

Use the provider as any other provider or component:

  <ThemeProvider theme={Theme.Dark}>
    ...
  </ThemeProvider>

@nlukk
Copy link
Copy Markdown

nlukk commented May 12, 2023

@cbodin IMO, it is better to use Symbol() here. Sometimes, null or undefined may be valid values, and it can cause in-band error signaling. Also, with Symbol() you wouldn't need to rewrap provider to only enforce types.

@cbodin
Copy link
Copy Markdown
Contributor

cbodin commented May 12, 2023

@cbodin IMO, it is better to use Symbol() here. Sometimes, null or undefined may be valid values, and it can cause in-band error signaling. Also, with Symbol() you wouldn't need to rewrap provider to only enforce types.

@nlukk I'm not sure how the use of a Symbol would help with the rewrap of the provider? If null or undefined are valid values you can export and use the context as-is:

export const ThemeContext = createContext<Theme | undefined | null>();

@nlukk
Copy link
Copy Markdown

nlukk commented May 12, 2023

@cbodin Yes, but in your previous example, null is not a valid value. If you would export the context, you would be able to pass null to the provider, and it would throw, which would confuse me. With Symbol() you don't have that problem (as long as you don't export it and use it explicitly).

@cbodin
Copy link
Copy Markdown
Contributor

cbodin commented May 12, 2023

@nlukk Yes, if exported and passed null it would throw. Could you provide me with an example when using Symbol() as per your suggestion?

@nlukk
Copy link
Copy Markdown

nlukk commented May 12, 2023

@cbodin sure, here it is

const sym = Symbol();
export const Context = createContext<number | typeof sym>(sym);

@cbodin
Copy link
Copy Markdown
Contributor

cbodin commented May 12, 2023

@nlukk But aren't we back to the original problem now, where we need to check everywhere that the context returns a valid value?

const value = useContext(Context);
if (typeof value === 'number') {
  // Use value
}

@nlukk
Copy link
Copy Markdown

nlukk commented May 12, 2023

@cbodin yes, but you can write a custom hook for this, so that you don't need to also import context everywhere.

const sym = Symbol();
export const SomethingContext = createContext<number | typeof sym>(sym);

export function useSomethingContext() {
  const something = useContext(SomethingContext);
  if (something === sym) throw new Error();
  return something;
}

@basememara
Copy link
Copy Markdown

basememara commented Jul 10, 2023

@fabiospampinato Maybe this use case has been mentioned already, if so I missed it: basically I need to create a context for each editor tab lets say, it doesn't make sense to even have a default value as I don't have a default tab. So I'd expect to be able to create a context without a default value, and if that context is being accessed without a value being provided for it first it should throw.

I'm currently resorting to doing something like this but it's ugly:

createContext<ContextType> ( undefined as any );

Thx this is great, one step further to satisfy lints as well is you can do:

createContext<ContextType>(undefined as never);

@cbodin
Copy link
Copy Markdown
Contributor

cbodin commented Jul 10, 2023

@nlukk I've updated my example above with a symbol instead of relying on null as per your suggestion. Also removed the custom provider as the default provider can be used as-is now. Still kept the custom consumer so that a developer-friendly error message can be shown if the provider is missing.

@EskiMojo14
Copy link
Copy Markdown
Contributor

apologies for necroposting (and technically self promoting), but I found myself doing the whole "throw an error if no Provider" pattern often enough that i created a little library for it

import { createRequiredContext } from "required-react-context";

export const { CountContext, useCount, CountProvider, CountConsumer } =
  createRequiredContext<number>().with({ name: "count" });

function Child() {
  // This will throw an error if used outside a CountProvider
  const count = useCount();
  return <div>{count}</div>;
}

function App() {
  return (
    <CountProvider count={5}>
      <Child />
    </CountProvider>
  );
}

hopefully somebody finds this useful :)

@camsteffen
Copy link
Copy Markdown

camsteffen commented May 2, 2024

You have to realize that useContext can be called anywhere, even outside of a Provider context. If you expect to always use the context inside of a Provider where the provided value is non-null, then you should assert that at the time you call useContext.

const MyContext = createContext<MyContextValue | null>(null);

export function useMyContext() {
  const value = useContext(MyContext);
  if (value == null) throw new Error('expected MyContext not to be null');
  return value;
}

@marcog83
Copy link
Copy Markdown

marcog83 commented Nov 20, 2024

You have to realize that useContext can be called anywhere, even outside of a Provider context.

Hi, i'm late to the party :), but for curiosity. What would be a valid use case in which you call useContext outside Provider?
Don't take me wrong, i'm really interested in this, because maybe i'm missing a piece on how i can structure my React Application.
Thanks

@camsteffen
Copy link
Copy Markdown

@marcog83 In my experience such use cases are quite rare and I don't know that I've ever had one. You would have to have some kind of global default behavior that you want to override only in some contexts. IMO it is very much the norm to only call useContext within a corresponding Provider, and sometimes I wonder if useContext should have been better designed with that in mind. In my project I have a helper function useDefinedContext(MyContext) that I use basically everywhere.

@marcog83
Copy link
Copy Markdown

marcog83 commented Jan 13, 2025

Life is strange 😄 ...
@camsteffen i found a case, where i can use the hook without the Provider.
I'm developing a library to get UserPreferences (it's a fetch to an internal API of my company).
Under the hood i need to setup the fetch options, (i use ky library) because i don't know if my consumers use the API as is, or they added some other layers on top of it. Below you can see my implementation, where options is optional.

/**
 *
 * getApiInstance
 *
 * This function is used to get an instance of Ky with the given configuration.
 * @param {ApiConfig} param - The configuration for the API.
 * @param {Options} options - The options for the API.
 * @category Functions
 */
export const getApiInstance = ({ authToken, prefixUrl }: ApiConfig, options?: Options) => {
  let hooks = options?.hooks;

  if (authToken) {
    hooks = {
      ...hooks,
      beforeRequest: [ getBeforeRequest(authToken), ...(hooks?.beforeRequest ?? []) ],
    };
  }
  const api: KyInstance = ky.create({
    ...options,
    prefixUrl,
    hooks,
    retry: 0,
  });

  return api;
};

then my provider looks like below.

/**
 *

 Use the `UserPreferencesProvider` component to connect and provide a `preferences` client to your application:

import { UserPreferencesProvider } from "@my-sdk/user-preferences-react"

const authToken="OPTIONAL_AUTH_TOKEN"

const App = () => (
  <UserPreferencesProvider
    prefixUrl="https://example.domain.com/user-preferences"
    authToken={authToken}
  >
    <MyComponent/>
  </UserPreferencesProvider>
);


@param {UserPreferencesProviderProps} props The props for the UserPreferencesProvider component.

@category Providers
 */
export const UserPreferencesProvider = ({ children, prefixUrl, authToken }: UserPreferencesProviderProps) => {
  const options = useApiOptions();
  const manager = useMemo<Preferences>(
    () => getPreferences({ authToken, prefixUrl }, options),
    [ prefixUrl, authToken, options ],
  );

  return (
    <UserPreferencesContext.Provider value={manager}>
      {children}
    </UserPreferencesContext.Provider>
  );
};

You can see this custom hook useApiOptions.
This hook returns the custom options that a consumer can specify. Its implementation looks like this

import { createContext, useContext } from 'react';
import type { Options } from 'ky';

const ApiContext = createContext<Options | undefined>(undefined);

export const useApiOptions = () => useContext(ApiContext);
/**
 * Provider for the API options
 * @type {React.Provider<Options>}
 * @example
 * ```tsx
 * const options=defineOptions({
 *  headers: {
 *      "X-Custom-Header":"Custom-Value"
 *  }
 * });
 * <ApiOptionsProvider value={options}>
 * <App />
 * </ApiOptionsProvider>
 * ```
 */
export const { Provider: ApiOptionsProvider } = ApiContext;

How does the cosumer specify custom options? by wrapping the application into a ApiOptionsProvider.
This way if the consumer doesn't need to customise the fetch requests, they can use the UserPreferences like below

const App = () => (
  <UserPreferencesProvider
    prefixUrl="https://exmple.domain.com/user-preferences"
    authToken={authToken}
  >
    <MyComponent/>
  </UserPreferencesProvider>
);

but if they need to add extra infos, like custom headers they can use the library like this

const options={
   headers: {
       "X-Custom-header:"Custom-value"
   }
 }
const App = () => (
<ApiOptionsProvider value={options}>
  <UserPreferencesProvider
    prefixUrl="https://exmple.domain.com/user-preferences"
    authToken={authToken}
  >
    <MyComponent/>
  </UserPreferencesProvider>
</ApiOptionsProvider>
);

@TuiKiken
Copy link
Copy Markdown

TuiKiken commented Mar 26, 2025

I suggest you create wrapper over context. Something like this:

import { createContext, FC, PropsWithChildren, useContext, useMemo } from "react";
import { UserId, UserType } from "types";

type UserContextValue = {
    userId: UserId;
    userType: UserType;
};

const UserContext = createContext<UserContextValue | null>(null);

type UserContextProviderProps = PropsWithChildren<{
    userId: UserId;
    userType: UserType;
}>;

export const UserContextProvider: FC<UserContextProviderProps> = ({ userId, userType, children }) => {
    const value = useMemo(() => ({
        userId,
        userType,
    }), [userId, userType]);

    return (
        <UserContext.Provider value={value}>
            {children}
        </UserContext.Provider>
    )
};

export const useUserContext = () => {
    const value = useContext(UserContext);

    if (value === null) {
        throw new Error('`useUserContext` hook used outside of `UserContextProvider`');
    }

    return value;
}

Wrap components with UserContextProvider like this:

<UserContextProvider userId={userId} userType={userType}>
  <SomeComponent />
</UserContextProvider>

And use values inside components like this:

const SomeComponent: FC = () => {
    const { userId } = useUserContext();
    
    ....
}

@andrewshawcare
Copy link
Copy Markdown

Hi, I work on the React team. 👋

I’m not sure I understand the whole discussion above (I’m not a TS user), but I don’t see the use case for calling createContext() without defaultValue. The only reason it’s “optional” is because JS doesn’t let us enforce arity of the call. This is why our Flow signature is createContext<T>(defaultValue: T).

If you really want to make it undefined you can pass it explicitly as an argument but it doesn’t make sense as a “default” to me. In my opinion calling it without an argument shouldn’t type check at all.

I hope this is helpful. In the future please feel free to reach out to us if some API detail or intention is not clear. Cheers!

When you define context, you may not understand what specific values should go into it yet.

This would be similar to providing a "default value" for a SQL table, an array, or some other collection structure.

What would go in it? Nothing? A default row with some made up values?

Context definition (createContext) is the idea that there will be a context used later that will have a certain shape. Defining that shape (including the use of generics) should be fair game, but the instantiation (of generic types and values) should be able to happen later (i.e. useContext).

What is the rationale for your opinion? Can you provide a model you're using to inform your opinion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Popular package This PR affects a popular package (as counted by NPM download counts).

Projects

None yet

Development

Successfully merging this pull request may close these issues.