How to create a generic React component with a typed context provider?

I don't know TypeScript so I can't answer in the same language, but if you want your Provider to be "specific" to your MyList class, you can create both in the same function.

function makeList() {
  const Ctx = React.createContext();

  class MyList extends Component {
    // ...
    render() {
      return (
        <Ctx.Provider value={this.state.something}>
          {this.props.children}
        </Ctx.Provider>
      );
    }
  }

  return {
    List,
    Consumer: Ctx.Consumer 
  };
}

// Usage
const { List, Consumer } = makeList();

Overall I think you might be over-abstracting things. Heavily using generics in React components is not a very common style and can lead to rather confusing code.


I think the answer, unfortunately, is that the question doesn't actually make sense.

Let's take a step back; what does it mean for a Context to be generic? Some component Producer<T> that represents the Producer half of a Context would presumably only provide values of type T, right?

Now consider the following:

<Producer<string> value="123">
  <Producer<number> value={123}>
    <Consumer />
  </Producer>
</Producer>

How SHOULD this behave? Which value should the consumer get?

  1. If Producer<number> overrides Producer<string> (i.e., consumer gets 123), the generic type doesn't do anything. Calling it number at the Producer level doesn't enforce that you'll get a number when consuming, so specifying it there is false hope.
  2. If both Producers are meant to be completely separate (i.e., consumer gets "123"), they must come from two separate Contexts instances that are specific to the type they hold. But then they're not generic!

In either case, there's no value in passing a type directly to Producer. That's not to say generics are useless when Context is at play...

How CAN I make a generic list component?

As someone who has been using generic components for a little while, I don't think your list example is overly abstract. It's just that you can't enforce type agreement between Producer and Consumer - just like you can't "enforce" the type of a value you get from a web request, or from local storage, or third-party code!

Ultimately, this means using something like any when defining the Context and specifying an expected type when consuming that Context.

Example

const listContext = React.createContext<ListProps<any>>({ onSelectionChange: () => {} });

interface ListProps<TItem> {
  onSelectionChange: (selected: TItem | undefined) => void;
}

// Note that List is still generic! 
class List<TItem> extends React.Component<ListProps<TItem>> {
    public render() {
        return (
            <listContext.Provider value={this.props}>
                {this.props.children}
            </listContext.Provider>
        );
    }
}

interface CustomListItemProps<TItem> {
  item: TItem;
}

class CustomListItem<TItem> extends React.Component<CustomListItemProps<TItem>> {
    public render() {
        // Get the context value and store it as ListProps<TItem>.
        // Then build a list item that can call onSelectionChange based on this.props.item!
    }
}

interface ContactListProps {
  contacts: Contact[];
}

class ContactList extends React.Component<ContactListProps> {
    public render() {
        return (
            <List<Contact> onSelectionChange={console.log}>
                {contacts.map(contact => <ContactListItem contact={contact} />)}
            </List>
        );
    }
}

I had the same problem and I think I solved it in a more elegant way: you can use lodash once (or create one urself its very easy) to initialize the context once with the generic type and then call him from inside the funciton and in the rest of the components you can use custom useContext hook to get the data:

Parent Component:

import React, { useContext } from 'react';
import { once } from 'lodash';

const createStateContext = once(<T,>() => React.createContext({} as State<T>));
export const useStateContext = <T,>() => useContext(createStateContext<T>());

const ParentComponent = <T>(props: Props<T>) => {
    const StateContext = createStateContext<T>();
    return (
        <StateContext.Provider value={[YOUR VALUE]}>
            <ChildComponent />
        </StateContext.Provider>
    );
}

Child Component:

import React from 'react';
import { useStateContext } from './parent-component';

const ChildComponent = <T>(props: Props<T>) => {
     const state = useStateContext<T>();
     ...
}

Hope it helps someone