6

How can we use generics in React.CreateContext?

I had this:

interface Props {
}

export interface SearchContextProps {
  dtos: any[];
}

export const SearchContext = React.createContext<SearchContextProps>({
  dtos: []
});

const SearchPage: React.FunctionComponent<Props> = (props) => {
  const [dtos, setDtos] = React.useState<any[]>([]);

  const searchContext = {
    dtos: dtos
  };

  return (
    <SearchContext.Provider value={searchContext}>
      ...
    </SearchContext.Provider>
  );
}

export default SearchPage;

Now I want to use generics, so I would write something like:

interface Props<T> {
}

export interface SearchContextProps<T> {
  dtos: T[];
}

export const SearchContext = React.createContext<SearchContextProps<T>>({
  dtos: []
});

const SearchPage = <T extends object>(props: Props<T>) => {
  const [dtos, setDtos] = React.useState<T[]>([]);

  const searchContext = {
    dtos: dtos
  };

  return (
    <SearchContext.Provider value={searchContext}>
      ...
    </SearchContext.Provider>
  );
}

export default SearchPage;

but I am missing how can I get to work the line:

export const SearchContext = React.createContext<SearchContextProps<T>>({

how can I use generics here, how can I have access to T?

I tried to move context inside the component:

interface Props<T> {
}

export interface SearchContextProps<T> {
  dtos: T[];
}

const SearchPage = <T extends object>(props: Props<T>) => {
  const [dtos, setDtos] = React.useState<T[]>([]);

  const SearchContext = React.createContext<SearchContextProps<T>>({
    dtos: [],
  });

  const searchContext = {
    dtos: dtos,
  };

  return (
    <SearchContext.Provider value={searchContext}>
      ...
    </SearchContext.Provider>
  );

}

export default SearchPage;

but now I don't know how to export it.

Any help?

2
  • 1
    Hi Simone, I am currently dealing with the same topic. Have you figured out a solution yet? Commented Mar 29, 2020 at 19:35
  • 1
    Yes, see my own answer. Commented Mar 31, 2020 at 11:03

2 Answers 2

2

I've been trying to figure this out for a while too, finally did it with a wizard I'm building atm.

The provider part is similar to yours:

import React, { Dispatch, SetStateAction } from "react";

export interface IWizardContext<T> {
  formData: T;
  setFormData: Dispatch<SetStateAction<T>>;
}

export const WizardContext = React.createContext<IWizardContext<any>>({
  formData: {},
  setFormData: () => {},
});

const WizardProvider: React.FC = (props) => {
  const [formData, setFormData] = React.useState<any>({});

  return (
    <WizardContext.Provider
      value={{
        formData: formData,
        setFormData: setFormData,
      }}
    >
      {props.children}
    </WizardContext.Provider>
  );
};

export default WizardProvider;

export function withWizardProvider<TProps = {}>(component: React.FC<TProps>) {
  const Component = component;

  return ((props: TProps) => {
    return (
      <WizardProvider>
        <Component {...props} />
      </WizardProvider>
    );
  }) as React.FC<TProps>;
}

And the consumer:

import React from "react";
import {
  IWizardContext,
  withWizardProvider,
  WizardContext,
} from "./WizardProvider";

interface ISampleWizardState {
  name?: string;
  lastName?: string;
}

const SampleWizard: React.FC = (props) => {
  const wizard = React.useContext<IWizardContext<ISampleWizardState>>(
    WizardContext
  );

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "flex-start",
        flexDirection: "column",
      }}
    >
      <span>{`${wizard.formData.name || "John"} ${
        wizard.formData.lastName || "Doe"
      }`}</span>
      <input
        type="text"
        name="name"
        value={wizard.formData.name}
        onChange={(e) => {
          const text = e?.target?.value;
          wizard.setFormData((prev) => ({
            ...prev,
            name: text,
          }));
        }}
      />
      <input
        type="text"
        name="lastName"
        value={wizard.formData.lastName}
        onChange={(e) => {
          const text = e?.target?.value;
          wizard.setFormData((prev) => ({
            ...prev,
            lastName: text,
          }));
        }}
      />
    </div>
  );
};

export default withWizardProvider(SampleWizard);

So the main thing for the consumer is to define your T in the useContext:

React.useContext<IWizardContext<ISampleWizardState>>(WizardContext);
Sign up to request clarification or add additional context in comments.

Comments

2

I ended up with:

import * as React from 'react';

interface Props<T> {}

export interface SearchContextProps<T> {
  dtos: T[];
}

export const SearchContext = React.createContext<SearchContextProps<any>>({
  dtos: [],
});

const SearchPage = <T extends object>(props: React.PropsWithChildren<Props<T>>) => {
  const [dtos, setDtos] = React.useState<T[]>([]);

  const searchContext = {
    dtos: dtos,
  };

  return <SearchContext.Provider value={searchContext}>...</SearchContext.Provider>;
};

export default SearchPage;

And the consumer

const searchContext = React.useContext<SearchContextProps<Interfaces.FileDto>>(SearchContext)

3 Comments

Can you please add an example of SearchContext.Consumer?
React.useContext<SearchContextProps<SomeSpecificInterface>>(SearchContext)
Thank you, op! Very simple and for my case I had a lot of properties in my Context type that weren't affected by the generic type, so in the components that only used those non generic properties from the context I didn't even specify the type when calling useContext.

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.