0

I am in the process of creating a generic component in typescript that we call GenericComponent for a library.

I want to achieve the following behavior from my library :

function GenericComponent<T>(props: GenericComponentProps<T>) {
  // ...
  return (
    <GenericContext.Provider value={...}>
      {/* ... */}
    </GenericContext.Provider>
  );
} 

However, the issue is that the data provided in my context depends on the generic type of the component where it's being provided, T. So, how can I create my context? Because I can't do:

const GenericContext = createContext<GenericContextType<T>>(...)

I tried to create my context within the component, but I don't know if it's a best approach.

After development, using GenericComponent should looks like this :


type TheTypeT = ...

function App() {
   // a state or something that we pass to the component
   const [theTypeT, setTheTypeT] = useState<TheTypeT>(...);

   return (
      <GenericComponent 
          theTypeT={theTypeT}
      />
   );
}

But I don't know where in my library I could create my context that depend on the type of theTypeT

1 Answers1

0

Here, I present the approach I take, it's pretty clean, imho. Here's a Codesandbox link for all the code, down below, each file is linked individually as well.

First, I create a wrapper around the react's createContext like this -

src/hooks/create-context.ts

import { createContext as reactCreateContext, useContext as reactUseContext } from "react";

export default function createContext<T extends Record<string,unknown>>() {
    const context = reactCreateContext<T | undefined>(undefined);

    const useContext = (): T => {
        const c = reactUseContext(context);

        if (c === undefined) {
            throw new Error("No Context Found: make sure to use the hook inside the Context Provider children");
        }
        return c;
    }

    return [useContext, context.Provider] as const;

}

Then, to make the typings for different context types available, without having to import them manually, create a file index.d.ts at project root level to house typings, and add your types here to use from, I'm showing a simple counter example using this -

declare namespace Counter {
    export type State = { counter: number };
    export type Action = { type: "INC_COUNTER" | "DEC_COUNTER" | "RESET_COUNTER", payload: number };
    export type Context = {
        counterState: State;
        counterDispatch: React.Dispatch<Action>
    }
}

To make the types from index.d.ts available in all the places, edit tsconfig.json to add the ./index.d.ts path in the include field -

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": [
      "DOM",
      "DOM.Iterable",
      "ESNext"
    ],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src", "./index.d.ts"
  ],
  "references": [
    {
      "path": "./tsconfig.node.json"
    }
  ]
}

Now, on to creating our counter context, create a file src/hooks/counter-context.tsx and use the createContext hook created to create a counter context -

import { useReducer } from "react";
import createContext from "./create-context";

const reducer = (state: Counter.State, action: Counter.Action): Counter.State => {
    const copy = { ...state }
    switch (action.type) {
        case "DEC_COUNTER":
            copy.counter -= action.payload;
            return copy;
        case "INC_COUNTER":
            copy.counter += action.payload;
            return copy;
        default:
            return state;
    }
}

const initialState: Counter.State = { counter: 0 };

const [_useCounterContext, CounterContextProvider] = createContext<Counter.Context>();

export const useCounterContext = _useCounterContext;

export function CounterContext ({ children }: { children: React.ReactNode }) {

    const [state, dispatch] = useReducer(reducer, initialState);

    return <CounterContextProvider value={{ counterState: state, counterDispatch: dispatch }}>
                {children}
            </CounterContextProvider>
}

From this file, we export the useCounterContext hook provided by the createContext and the CounterContext component that provides access to the context.

After that, use CounterContext above the component, whose children will be using the context. In my case, I use the context in src/App.tsx, so we add the CounterContext in src/main.tsx, which uses <App /> component as child -

// src/main.tsx file

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { CounterContext } from "./hooks/counter-context";


ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <CounterContext>
      <App />
    </CounterContext>
  </React.StrictMode>
);

And then at last, we use the context in any child of the <App /> component, or the <App /> component itself, like so -

// src/App.tsx file

import reactLogo from "./assets/react.svg";
import "./App.css";
import { useCounterContext } from "./hooks/counter-context";

function App() {
  const {counterState: { counter }, counterDispatch } = useCounterContext();

  return (
    <div className="App">
      <div>
        <a href="https://reactjs.org" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
        <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
      </div>
      <h1>React + Vite</h1>
      <div className="card">
      <h2>Count: {counter}</h2>
      <div style={{display: "flex", gap: "1rem"}}>
        <button onClick={() => {counterDispatch({type: "INC_COUNTER", payload: 1})}}>Increment</button>
        <button onClick={() => {counterDispatch({type: "DEC_COUNTER", payload: 1})}}>Decrement</button>
      </div>
      </div>
    </div>
  );
}

export default App;
0xts
  • 2,411
  • 8
  • 16
  • Thank you for taking an interest in this issue @0xts. I know this approach where we create a factory function that generate our hooks and others, but as I said I'm creating a generic component in typescript **for a library**. Thus, I want to do it under the wood, but I don't know the place where I need to create my context, because here you define the type and context by your own, me I want to have this approach : user of my library use my component, so he never need to create it's own context or hooks from the type that he create. – Nekena Rayane RATIARIVELO Aug 12 '23 at 05:55
  • Thanks for updating your question with more information. How about an HOC? With that you'll still be doing the same thing I explained in my answer, but with an HOC. – 0xts Aug 12 '23 at 06:44
  • Okay I'll explore that way – Nekena Rayane RATIARIVELO Aug 12 '23 at 10:59