Home > Software engineering >  Why is styled-components with Next.js adding my attributes to the DOM?
Why is styled-components with Next.js adding my attributes to the DOM?

Time:09-19

I have this in some parts of the code:

import React from 'react'
import styled from 'styled-components'

export type BoxType = React.ReactElement<typeof Box>

type JustifyType = 'between' | 'center'

type ScrollType = 'auto' | 'scroll'

type PropsType = {
  direction?: 'horizontal' | 'vertical'
  flex?: number | string
  width?: number | string
  minWidth?: number | string
  height?: number | string
  minHeight?: number | string
  fill?: string
  justify?: JustifyType
  padding?: number | string
  paddingTop?: string | number
  paddingRight?: string | number
  paddingBottom?: string | number
  paddingLeft?: string | number
  scrollX?: ScrollType
  scrollY?: ScrollType
}

const JUSTIFY: Record<JustifyType, string> = {
  between: 'space-between',
  center: 'center',
}

const Box = styled.div<PropsType>`
  display: flex;
  ${({ scrollX }) => scrollX && `overflow-x: ${scrollX};`}
  ${({ scrollY }) => scrollY && `overflow-y: ${scrollY};`}
  ${({ padding }) =>
    padding &&
    `padding: ${typeof padding === 'number' ? `${padding}px` : padding};`}
  ${({ width }) =>
    width && `width: ${typeof width === 'number' ? `${width}px` : width};`}
  ${({ minWidth }) =>
    minWidth &&
    `min-width: ${typeof minWidth === 'number' ? `${minWidth}px` : minWidth};`}
  ${({ height }) =>
    height && `height: ${typeof height === 'number' ? `${height}px` : height};`}
  ${({ minHeight }) =>
    minHeight &&
    `min-height: ${
      typeof minHeight === 'number' ? `${minHeight}px` : minHeight
    };`}
  ${({ flex }) => flex && `flex: ${flex};`}
  ${({ fill }) => fill && `background-color: ${fill};`}
  ${({ direction }) =>
    direction &&
    `flex-direction: ${direction === 'horizontal' ? 'row' : 'column'};`}
  ${({ justify }) => justify && `justify-content: ${JUSTIFY[justify]};`}
  ${({ paddingTop }) =>
    paddingTop &&
    `padding-top: ${
      typeof paddingTop === 'number' ? `${paddingTop}px` : paddingTop
    };`}
  ${({ paddingRight }) =>
    paddingRight &&
    `padding-right: ${
      typeof paddingRight === 'number' ? `${paddingRight}px` : paddingRight
    };`}
  ${({ paddingBottom }) =>
    paddingBottom &&
    `padding-bottom: ${
      typeof paddingBottom === 'number' ? `${paddingBottom}px` : paddingBottom
    };`}
  ${({ paddingLeft }) =>
    paddingLeft &&
    `padding-left: ${
      typeof paddingLeft === 'number' ? `${paddingLeft}px` : paddingLeft
    };`}
`

export default Box

And then I have this:

export default function Page({ url, tab, note, tags }: PagePropsType) {
  const [output, setOutput] = useState('')

  const handleChange = (e: React.FormEvent<HTMLTextAreaElement>): void => {
    if (e.target instanceof HTMLTextAreaElement) {
      setOutput(clearNiqqud(e.target.value ?? ''))
    }
  }

  return (
    <Content url={url} tab={tab} note={note} tags={tags}>
      <Box padding={24} width="100%">
        <Type.H1 align="center">{tab}</Type.H1>
      </Box>
      {/* נִקּוּד */}
      <Box fill={COLOR.white} paddingTop={24} paddingBottom={24}>
        <Type.Text
          paddingLeft={24}
          paddingRight={24}
          align="right"
          font="hebrew"
          size={40}
          rows={4}
          onChange={handleChange}
        />
      </Box>
      <Box padding={24}>
        <Type.P preserveNewlines font="hebrew" size={40} align="right">
          {output}
        </Type.P>
      </Box>
    </Content>
  )
}

The relevant part is the fill attribute, and other attributes, which show up in the DOM:

enter image description here

Is this normal behavior? Or how do I remove it from the DOM? I am using Next.js and have the default next.config.js, with this tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "types": ["node"],
    "baseUrl": "."
  },
  "include": ["next-env.d.ts", "index.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"],
  "ts-node": {
    "compilerOptions": {
      "module": "commonjs"
    }
  }
}

CodePudding user response:

Indeed we normally expect that styled-components is smart enough to not pass extra props to styled base HTML tags (like div in this case):

If the styled target is a simple element (e.g. styled.div), [...] styled-components [is] smart enough to filter non-standard attributes automatically for you.

Unfortunately, the "smart enough" is not enough in this case: fill attribute is standard for SVG elements, hence styled-components lets it through...


A very simple workaround is to use a transient prop instead. It consists in prefixing the prop with a dollar sign:

If you want to prevent props meant to be consumed by styled components from being passed to the underlying React node or rendered to the DOM element, you can prefix the prop name with a dollar sign ($), turning it into a transient prop.

const Box2 = styled.div<{ $fill?: string; }>`
  ${({ $fill }) => $fill && `background-color: ${$fill};`}
`;

<Box2 $fill="yellow">Content...</Box2>

Unfortunately, this means slightly changing your internal API.


Another workaround consists in using the shouldForwardProp configuration. With this, we can precisely specify what should be passed through or not:

A prop that fails the test isn't passed down to underlying components, just like a transient prop.

const Box3 = styled.div.withConfig({
  shouldForwardProp(prop, isValidProp) {
    return (prop as string) !== "fill" && isValidProp(prop);
  }
})<{ fill?: string; }>`
  ${({ fill }) => fill && `background-color: ${fill};`}
`;

<Box3 fill="yellow">Content...</Box3>

Demo: https://codesandbox.io/s/wispy-silence-mp54zv?file=/src/App.tsx


Some more explanation on the styled-components built-in "smart" filter: as implied above, as soon as the prop is a standard attribute for any HTML standard element (like fill for SVG in this case), it is white listed, and passed through.

This is because styled-components actually relies on @emotion/is-prop-valid, which implements the filter as a single common white list for simplicity, instead of having a list per HTML standard tag.

We can find the fill attribute white listed here.

  • Related