0

Is it possible to pass props down to children components whose required types we don't yet know? Something like this:

interface CardProps<T> {
  set: SetProps<T>;
  setSet: Dispatch<SetStateAction<SetProps<T>>>;
  children: ReactNode;
}

const RenderCards = <T extends object>({ set, setSet, children }: CardProps<T>) => (
  <>
    {set &&
      set.data &&
      set.data.map((item: T, idx: number) => (
        <SwipeBox
          key={idx}
          state={{ set, setSet }}
        >
          <MovieCard movie={item as MovieType} /> // This is too specific, want to delete.
          {children ...props} // This is flexible, but wrong.
        </SwipeBox>
      ))}
  </>
);

So far, I've come up with Context API as a viable solution. It's certainly less boilerplate than Redux but, is there an even easier way?

nusantara
  • 1,109
  • 1
  • 15
  • 38
  • If you want to use ``typescript``, you should use it the correct way. Is your problem that ``set.data`` contains objects of different types? If so, two possible answers. The short one, use ````. The better alternative, use type union to define beforehand all the different types of ``set.data``'s objects. Without more context that's the best I can advise – Kibonge Murphy Feb 01 '21 at 18:51
  • @KibongeMurphy yes, the data will be of different types, but in this case it's not possible to know what's coming in and how it will be used, so i can't really define them yet. `item as any` is a really good compromise though. thanks! – nusantara Feb 01 '21 at 19:37
  • There's a few ways to do this. You can use `React.cloneElement` to inject values https://stackoverflow.com/questions/55464194/how-to-define-typescript-for-react-children-map **but** if the injected values are required props then you'll get errors on the JSX when you write them without those props. You can make `children` be a function which takes your props and renders them. You can also use a prop called `Render` or `CardBody` to pass a component and then call the component with your props. In the current setup, is `children` always a single node or do you need to support arrays? – Linda Paiste Feb 01 '21 at 21:33
  • Also what are the props that you are passing? `props` is not defined here. – Linda Paiste Feb 01 '21 at 21:35

1 Answers1

0

I'm guessing a bit about your intentions, but I am interpreting this as:

  • You want to support rendering different kinds of cards for different T types.
  • You want to pass through certain props to every individual card.

I came up with a solution based on a render props pattern. We now have two generics -- T which is still the item type and Extra which is the props that we pass through.

Each individual card will receive the properties of the item as props, all of the Extra props, and set and setSet (not sure if those last two are actually needed).

The card group requires set, setSet, the Extra props, and a Render component for the individual card.

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

interface SetProps<T> {
  data: T[];
}

interface CardProps<T> {
  set: SetProps<T>;
  setSet: Dispatch<SetStateAction<SetProps<T>>>;
}

type CardGroupProps<T, Extra = {}> = Extra &
  CardProps<T> & {
    Render: ComponentType<T & Extra & CardProps<T>>;
  };

const CardGroup = <T extends object, Extra extends object>(
   {set, setSet, Render, ...extra}: CardGroupProps<T, Extra>
) => (
  <>
    {set.data.map((item: T, idx: number) => (
      <SwipeBox 
        key={idx} 
        state={{ set, setSet }}
      >
        <Render 
           set={set} 
           setSet={setSet} 
           {...(extra as Extra)} 
           {...item} 
        />
      </SwipeBox>
    ))}
  </>
);

Here's a silly example to show usage. Here our RenderMovieCard component requires the Movie item and also a color prop. You will get an error if you try to call a CardGroup with Render={RenderMovieCard} without passing this required color prop.


interface Movie {
  title: string;
}

const RenderMovieCard = ({ title, color }: Movie & { color: string }) => (
  <div style={{ color }}>{title}</div>
);

export default () => {
  const [movies, setMovies] = useState<SetProps<Movie>>({
    data: [{ title: "Jaws" }, { title: "Star Wars" }]
  });

  return (
    <CardGroup
      set={movies}
      setSet={setMovies}
      Render={RenderMovieCard}
      color="red"
    />
  );
};

Code Sandbox

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102