24

With React's new context API, you can create a typed context producer/consumer like so:

type MyContextType = string;

const { Consumer, Producer } = React.createContext<MyContextType>('foo');

However, say I have a generic component that lists items.

// To be referenced later
interface IContext<ItemType> {
    items: ItemType[];
}

interface IProps<ItemType> {
    items: ItemType[];
}

class MyList<ItemType> extends React.Component<IProps<ItemType>> {
    public render() {
        return items.map(i => <p key={i.id}>{i.text}</p>);
    }
}

If I instead wanted to render some custom component as the list item and pass in attributes from MyList as context, how would I accomplish that? Is it even possible?


What I've tried:

Approach #1

class MyList<ItemType> extends React.Component<IProps<ItemType>> {
    // The next line is an error.
    public static context = React.createContext<IContext<ItemType>>({
        items: []
    }
}

This approach doesn't work because you can't access the class' type from a static context, which makes sense.

Approach #2

Using the standard context pattern, we create the consumer and producer at the module level (ie not inside the class). The problem here is we have to create the consumer and producer before we know their type arguments.

Approach #3

I found a post on Medium that mirrors what I'm trying to do. The key take away from the exchange is that we can't create the producer/consumer until we know the type information (seems obvious right?). This leads to the following approach.

class MyList<ItemType> extends React.Component<IProps<ItemType>> {
    private localContext: React.Context<IContext<ItemType>>;

    constructor(props?: IProps<ItemType>) {
        super(props);

        this.localContext = React.createContext<IContext<ItemType>>({
            items: [],
        });
    }

    public render() {
        return (
            <this.localContext.Provider>
                {this.props.children}
            </this.localContext.Provider>
        );
    }
}

This is (maybe) progress because we can instantiate a provider of the correct type, but how would the child component access the correct consumer?


Update

As the answer below mentions, this pattern is a sign of trying to over-abstract which doesn't work very well with React. If a were to try to solve this problem, I would create a generic ListItem class to encapsulate the items themselves. This way the context object could be typed to any form of ListItem and we don't have to dynamically create the consumers and providers.

7 Answers 7

28

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

Sign up to request clarification or add additional context in comments.

3 Comments

Looks like a cool answer, but where is State coming from in third line, as State<T>?
This is a very elegant solution! I like it a lot!
@brandonscript - I thought the same thing. It's the model for the Context so you should already have a type that you can slot in there.
17

Check how Formik does it

For me it was important to check types when using context (useContext) just like Formik useFormikContext.

Example of generic type

type YourContextType<T> = { state: T; setState: (s: T) => void }

Create context (do not care about type)

const YourContext = createContext<YourContextType<any>>(
  undefined as any,
);

Context hook with generic type

function useYourContext<T>() {
  const context = useContext<YourContextType<T>>(YourContext);


  if (context === undefined) {
    // assert if context is available
    throw new Error('No context provided');
  }

  return context;
}

Use hook

 const { state, changeState } = useYourContext<{ name: string }>();

You will see state and changeState have types. Maybe it is not in 100% what you are looking for but for me it was enough. Check @TheRubberDuck explanation

src: https://github.com/jaredpalmer/formik/blob/master/packages/formik/src/FormikContext.tsx

Comments

14

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.

2 Comments

I agree that his approach was an over-abstraction. It's almost a benefit of React that it makes this difficult which ends up encouraging a more appropriate solution. Thanks for the response.
How would a descendant component of List access the context Consumer? Would the Consumer itself need to be passed down the component tree as a prop? That feels slightly counter intuitive since contexts help avoid plumbing a bunch of props down a deep tree.
8

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>
        );
    }
}

Comments

4

Typescript version inspired by the accepted answer.

export function makeContext<T>(
  displayName: string
): [FC<ContextProviderProps<T>>, () => T] {
  const Ctx = createContext<T | null>(null);
  Ctx.displayName = displayName;

  const ContextProvider: FC<ContextProviderProps<T>> = ({
    value,
    children,
  }) => <Ctx.Provider value={value}>{children}</Ctx.Provider>;

  const _useContext = () => {
    const value = useContext(Ctx);
    if (!value) {
      throw new Error(`Provider for ${Ctx.displayName} not found`);
    }
    return value;
  };

  return [ContextProvider, _useContext];
}

Example of usage

type Theme = 'dark' | 'light';

const [ThemeProvider, useTheme] =
  makeContext<[Theme, Dispatch<SetStateAction<Theme>>]>('Theme');

const App = () => {
  const theme = useState('dark');

  return (
    <ThemeProvider value={theme}>
      <ThemedView />
    </ThemeProvider>
  );
};

const ThemedView = () => {
  const [theme, setTheme] = useTheme();
  return (
    <View>
      <Text>{theme}</Text>
      <Button
        title="Change"
        onPress={() => setTheme((prev) => (prev == 'dark' ? 'light' : 'dark'))}
      />
    </View>
  );
};

Comments

0

You can make it like this. I think it will be more reliable than just casting.

type StateContext<T> = {
    value: T | T[] | undefined;
    multiple: boolean;
    toggleValue: (value: T) => void;
    isSelected: (value: T) => boolean;
};

type UIContext = {
    item: { height?: number };
    menu: { optionsGap?: number; labelNodesGap?: number };
    root: { width?: number; fullWidth?: boolean; isDisabled?: boolean; className?: string };
};

export type SelectMenuContextType<T> = {
    state: StateContext<T>;
    ui: UIContext;
};

export const SelectMenuContext = createContext<SelectMenuContextType<any> | null>(null);

export const useSelectMenuContext = <T extends string | number>() => {
    const ctx = useContext(SelectMenuContext);

    if (!ctx) {
        throw new Error("useSelectMenuContext must be used inside SelectMenuContextProvider");
    }

    return ctx as SelectMenuContextType<T>;
};

export const SelectMenuContextProvider = <T extends string | number>(props: {
    value: SelectMenuContextType<T>;
    children: ReactNode;
}) => <SelectMenuContext.Provider value={props.value}>{props.children}</SelectMenuContext.Provider>;

Comments

-1

Generic type arguments work just fine in Context API props, with a little bit of coaxing. The main issue is that when initializing the Context, you don't yet know what the "narrowest" type will be, so you need to use a wider one, such as any.

Then, when setting the value via Provider, you can use the more narrow type. However, as of writing this, this won't transfer the narrow type to the consumers. To make that work, you'll simply need to provide the type again to useContext.

Example:

type MyNarrowType = {a: number, b: string};
type MyContextProps<T> = {data: T};

const MyContext = createContext<MyContextProps<any>>({
  data: {},
});

function MyProvider(props: {data: MyNarrowType}) {
  const value: MyContextProps<MyNarrowType> = {data};
  return <MyContext.Provider><MyComponent /></MyContext.Provider>
}

function MyComponent() {
  const {data} = useContext<MyContextProps<MyNarrowType>>(MyContext);
  // data is now a MyNarrowType
}

It's not ideal, but I think it works well enough for most situations.

1 Comment

This simply reiterates the problem the OP tried to solve. The purpose here was to have MyComponent generic, so you don't have to cast the NarrowType manually, but let it be infered by TypeScript.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.