Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 657
Chathan Driehuys Avatar asked Jul 20 '18 18:07

Chathan Driehuys


2 Answers

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

like image 106
Adidi Avatar answered Nov 17 '22 15:11

Adidi


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.

like image 12
Dan Abramov Avatar answered Nov 17 '22 15:11

Dan Abramov