Home > front end >  TypeScript React dynamically chose component to render
TypeScript React dynamically chose component to render

Time:12-28

I have, as I thought, a pretty simple task: render a React component according to its name/type.

Here is example of usage:

// WidgetsContainer.ts
// components have a difference in props shape!
const componentsData = [
  {
     type: 'WIDGET_1',
     id: 'WIDGET_1',
     title: 'Hello from widget 1',
     someUniquePropForW1: true,  
  },
  {
     type: 'WIDGET_2',
     id: 'WIDGET_2',
     name: 'Jhonny',
     someUniquePropForW2: 42,  
  }
]

return componentsData.map((componentData) => (
    <ComponentRenderer key={componentData.id} {...componentData} />
  ));

So I've tried to implement my ComponentRenderer in this way:

// Renderer.ts
import React from 'react';

interface IComponent<Properties = Record<string, unknown>> {
    type: string;
    id: string;
    properties?: Properties;
}

const COMPONENT_TYPE = {
  WIDGET_1: 'WIDGET_1',
  WIDGET_2: 'WIDGET_2',
};

interface IWidget1Props {
  title: string,
  someUniquePropForW1: boolean,  
}

interface IWidget2Props {
  name: string,
  someUniquePropForW2: number, 
}


const W1Component = (props: IWidget1Props) => <h1>{props.title}</h1>
const W2Component = (props: IWidget2Props) => <h1>{props.name}</h1>

const componentMap = {
  [COMPONENT_TYPE.WIDGET_1]: W1Component,
  [COMPONENT_TYPE.WIDGET_2]: W2Component,
};

export const Renderer = ({ type, properties }: IComponent) => {
  if (type in componentMap) {
    const ComponentToRender = componentMap[type];
    // !!!!!!!!!!!
    // issue here!
    // !!!!!!!!!!!
    return React.createElement(ComponentToRender, properties);
  }

  return null;
};

Error looks like

 Type 'PropsWithChildren<IWidget1Props>' is missing 
 the following properties from 
 type 'IWidget2Props': name, someUniquePropForW2

As I understand it, TS try to tell me something like that: "Hey! Here is situation possible when type is passed as WIDGET_1, but props are passes as for W2Component! I can't guarantee you are safe here".

So what I need is a way to tell the TS compiler that if type === WIDGET_1 props always are IWidget1Props.

I have been thinking about discriminated union as a type guard but somehow I don't really want to put something like:

// Renderer.ts
// Some code here...
switch(type) {
  case('WIDGET_1') return <W1Component {...properties}/>
  case('WIDGET_2') return <W2Component {...properties}/>
  //... 1000 more widgets
}
// Some code here...

Here is a link to TS sandbox with code described above.

How can I resolve this issue?

CodePudding user response:

This might solve your problem or can give you an idea

import React from 'react';

enum COMPONENT_TYPE {
  WIDGET_1 = "WIDGET_1",
  WIDGET_2 = "WIDGET_2"
}

interface IComponent<Properties = Record<string, unknown>> {
  id: string;
  type: COMPONENT_TYPE;
  properties?: WIDGET_TYPE<Properties>;
}

// chain each widget type!
type WIDGET_TYPE<T = COMPONENT_TYPE> = T extends COMPONENT_TYPE.WIDGET_1 ? IWidget1Props : IWidget2Props

interface IWidget1Props {
  title: string,
  someUniquePropForW1: boolean,
}

interface IWidget2Props {
  name: string,
  someUniquePropForW2: number,
}

const W1Component = (props: IWidget1Props) => <h1>{props.title}</h1>
const W2Component = (props: IWidget2Props) => <h1>{props.name}</h1>

const componentMap = {
  [COMPONENT_TYPE.WIDGET_1]: W1Component,
  [COMPONENT_TYPE.WIDGET_2]: W2Component,
};

export const Renderer = ({ type, properties }: IComponent) => {
  if (type in componentMap) {
    const ComponentToRender = componentMap[type] as (props: WIDGET_TYPE<typeof type>) => JSX.Element;
    return React.createElement(ComponentToRender, properties);
  }

  return null;
};

It's kinda hard to add type-safe in your situation. If the code block does not work for you, I do not have any other idea than casting ComponentToRender as any

 const ComponentToRender = componentMap[type] as any

UPDATE:

Instead of chaining widget types you can use React.FC casting:

const ComponentToRender = componentMap[type] as React.FC

CodePudding user response:

If you used the factory structure while creating the component, you would have solved the definition in the system from a single place. You can use the link below for the factory structure.

https://refactoring.guru/design-patterns/factory-method/typescript/example#example-0

  • Related