dasfacc
dasfacc

Reputation: 175

Children prop in TypeScript and React 18

In App.tsx I'm passing <Left /> and <Right /> to a an imported component called <SplitScreen />

Apparently the "children" prop needs to be explicitly typed since React 18. If I type it as React.Element[] then SplitScreen.tsx works but App.tsx throws:

Type 'Element' is not assignable to type 'ElementType<any>'

If I type it as React.ReactNode (which seems to be the consensus currently) App.tsx works but SplitScreen.tsx throws:

Type 'ReactNode' must have a '[Symbol.iterator]()' method that returns an iterator

And no, React.ReactNode[] doesn't help, it just changes the error to:

Type 'ReactNode[] | undefined' must have a '[Symbol.iterator]()'
// package.json
{
//...
"dependencies": {
    "@babel/preset-typescript": "^7.18.6",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/plugin-transform-typescript": "^7.18.12",
    "@types/react": "^18.0.17",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.0.1",
    "typescript": "^4.6.4"
  }
}
// App.tsx
const LeftHandComponent = () => {
    return <h1 style={{ backgroundColor: 'blueviolet' }}>Left</h1>;
};

function App() {
    return (
        <SplitScreen {...appProps}>
            <LeftHandComponent></LeftHandComponent>
            <!-- Type 'Element' is not assignable to type 'ElementType<any>' -->
            <RightHandComponent></RightHandComponent>
        </SplitScreen>
    );
}
// SplitScreen.tsx
interface SplitScreenProps {
    // extends PropsWithChildren {
    leftWeight: number;
    rightWeight: number;
    children?: React.ReactNode;
    // children: React.ElementType[];
}

export const SplitScreen = ({ leftWeight, rightWeight, children }: SplitScreenProps) => {
    // Type 'ReactNode' must have a '[Symbol.iterator]()' method that returns an iterator
    const [Left, Right] = children; // HERE
    return (
        <Container>
            <Pane weight={leftWeight}>
                <Left></Left>
            </Pane>
            <Pane weight={rightWeight}>
                <Right></Right>
            </Pane>
        </Container>
    );
};

Upvotes: 0

Views: 4007

Answers (2)

dasfacc
dasfacc

Reputation: 175

This is how I solved it; this entry had the last piece of the puzzle The Components need to be wrapped!

// App.jsx

const LeftHandComponent = ({name}: {name: string}) => {
    return <h1 style={{ backgroundColor: 'blueviolet' }}>{name}</h1>
};

const appProps = {
    leftWeight: 1,
    rightWeight: 3,
};

function App() {
    return <SplitScreen {...appProps}>
        <LeftHandComponent name="Left" />
        <RightHandComponent />
    </SplitScreen>
}

// SplitScreen.tsx

interface SplitScreenProps {
    leftWeight: number;
    rightWeight: number;
    children: React.ReactNode;
}

export const SplitScreen = ({ leftWeight, rightWeight, children }: SplitScreenProps) => {
    const [Left, Right] = React.Children.toArray(children);
    return (
        <Container>
            <Pane weight={leftWeight}>
                <React.Fragment>{Left}</React.Fragment>
            </Pane>
            <Pane weight={rightWeight}>
                <React.Fragment>{Right}</React.Fragment>
            </Pane>
        </Container>
    );
};

Upvotes: 0

Terry
Terry

Reputation: 66123

That is because ReactNode is not necessarily iterable (it can potentially be undefined, among its many other union types), so attempting to spread it into an array as: const [Left, Right] = children.

This can be remedied by using React.Children.toArray(children) before attempting to spread them:

const [Left, Right] = React.Children.toArray(children);

Another suggestion is that instead of trying to treat children as array, perhaps a more explicit way to pass these nodes in is not to rely on children prop, but pass them explicitly as props, e.g.:

interface SplitScreenProps {
    leftWeight: number;
    rightWeight: number;
    leftNode: React.ReactNode;
    rightNode: React.ReactNode;
}

Then the consuming parent of the component can just do this:

<SplitScreen
  {...appProps}
  leftNode={<LeftHandComponent />}
  rightNode={<RightHandComponent />}>

Upvotes: 1

Related Questions