emotion-js / emotion

👩‍🎤 CSS-in-JS library designed for high performance style composition
https://emotion.sh/
MIT License
17.34k stars 1.1k forks source link

Include `.defaultProps` in the documentation #2573

Open drldavis opened 2 years ago

drldavis commented 2 years ago

Description: It took me forever to figure out how to set default props with emotion's styled components. This isn't explained anywhere in the emotion docs.

Documentation links: https://emotion.sh/docs/styled

srmagura commented 2 years ago

Could you post some code to show how you expected it work? And how you finally got it to work?

drldavis commented 2 years ago

The difficult part for me was just finding out that this method existed.

const Title = styled(Typography)`
  display: flex;
  flex-wrap: wrap;
  align-self: start;
  @media (min-width:528px) { 
    align-self: center; 
    margin-left: .25em;
  }
`
Title.defaultProps = {
  variant: "h3"
}
srmagura commented 2 years ago

defaultProps is a React feature, i.e. it's not specific to @emotion/styled. So IMO we don't need to include it in our documentation.

The currently recommended way to do default props is with object destructuring default values. I would implement your use case like this — though the way you have done it is perfectly valid too.

function UnstyledTitle({ variant = 'h3', className, children }) {
    return <Typography variant={variant} className={className}>{children}</Typography>
}

const Title = styled(UnstyledTitle)`
  display: flex;
  flex-wrap: wrap;
  align-self: start;
  @media (min-width:528px) { 
    align-self: center; 
    margin-left: .25em;
  }
`
drldavis commented 2 years ago

Could I do the above but with an anonymous function?

The reason I think this could be beneficial to include in the docs is because people coming from styled-components are used to having the .attrs to set the variant of components.

srmagura commented 2 years ago

You can always use an arrow function in place of a normal named function. I would choose the option that makes your code more readable.

@Andarist Is there any reason for us to add an API like styled-components' attrs? Documentation here If the answer is "no", it may be helpful to add a section to the documentation that says something to the effect of "We don't support .attrs but here's an example of how you can pass default props to the underlying component".

lsegal commented 6 months ago

Now that defaultProps is officially deprecated in React 18.3, there's a need to revisit this issue and provide a better path forward. While the suggested approach above in https://github.com/emotion-js/emotion/issues/2573#issuecomment-983841698 technically works, the reality is that it creates pretty heavy boilerplate for what was previously extremely simple, especially when TypeScript is involved.

For example, consider what it would look like to extend some component Flex = styled.div<{ direction?: Direction, align?: Alignment, justify?: Justification, gap?: Gap, ... }>

Before, with defaultProps:

const ExtendedFlex = styled(Flex)`
  // ... css rules ...
`;

ExtendedFlex.defaultProps = { gap: "0", align: "stretch", justify: "stretch" };

After, using "recommended" approach:

function UnstyledExtendedFlex({
  gap = "0",
  align = "stretch",
  justify = "stretch",
  ...props
}: Parameters<typeof Flex>[0]) {
  return <Flex gap={gap} align={align} justify={justify} {...props} />;
}

const ExtendedFlex = styled(UnstyledExtendedFlex)`
  // ... css rules ...
`;

This is quite a lot of boilerplate for just a few default props. An attrs type approach would be very helpful.

karlhorky commented 4 months ago

In my repo karlhorky/jscodeshift-tricks have a codemod for jscodeshift which seems to be working for simple cases for me:

migrate-defaultProps.ts

// Convert defaultProps to default function parameters
// npx jscodeshift --parser=tsx --extensions=tsx,ts -t migrate-defaultProps.ts components/

import { API, FileInfo, Options } from 'jscodeshift';

const transform = (file: FileInfo, api: API, options: Options) => {
  const j = api.jscodeshift;
  const root = j(file.source);

  // Find the imported component name dynamically
  let styledComponentName;
  root
    .find(j.CallExpression, {
      callee: {
        name: 'styled',
      },
    })
    .forEach((path) => {
      if (
        path.value.arguments.length > 0 &&
        path.value.arguments[0].type === 'Identifier'
      ) {
        styledComponentName = path.value.arguments[0].name;
      }
    });

  // Find the styled component's template literal
  let styledTemplateLiteral;
  root.find(j.TaggedTemplateExpression).forEach((path) => {
    if (
      path.value.tag.type === 'CallExpression' &&
      path.value.tag.callee.name === 'styled' &&
      path.value.tag.arguments[0].name === styledComponentName
    ) {
      styledTemplateLiteral = path.value.quasi;
    }
  });

  // Find the property name dynamically
  let propertyName;
  root.find(j.AssignmentExpression).forEach((path) => {
    if (
      path.value.left.property &&
      path.value.left.property.name === 'defaultProps'
    ) {
      propertyName = Object.keys(
        path.value.right.properties.reduce((acc, prop) => {
          acc[prop.key.name] = true;
          return acc;
        }, {}),
      )[0];
    }
  });

  root
    .find(j.AssignmentExpression, {
      left: {
        type: 'MemberExpression',
        property: {
          name: 'defaultProps',
        },
      },
    })
    .forEach((path) => {
      const componentName = path.value.left.object.name;
      const defaultProps = path.value.right;

      // Create a new functional component with default parameters
      const newComponent = j.functionDeclaration(
        j.identifier(`Unstyled${componentName}`),
        [
          j.objectPattern([
            // Add default parameters to the new component
            ...defaultProps.properties.map((prop) => {
              const id = j.identifier(prop.key.name);
              let defaultValue;

              // Check if the value is an array expression
              if (prop.value.type === 'ArrayExpression') {
                defaultValue = j.arrayExpression(prop.value.elements);
              } else {
                // For literals, use the literal value
                defaultValue = j.literal(prop.value.value);
              }

              // Create an assignment pattern for default values
              const assignmentPattern = j.assignmentPattern(id, defaultValue);
              const property = j.property('init', id, assignmentPattern);
              property.shorthand = true; // Enable shorthand syntax
              return property;
            }),
            // Spread the rest of the properties
            j.restElement(j.identifier('props')),
          ]),
        ],
        j.blockStatement([
          j.returnStatement(
            j.jsxElement(
              j.jsxOpeningElement(
                j.jsxIdentifier(styledComponentName),
                [
                  // Spread props into the Box component
                  j.jsxSpreadAttribute(j.identifier('props')),
                  // spread the rest of the properties
                  ...defaultProps.properties.map((prop) => {
                    return j.jsxAttribute(
                      j.jsxIdentifier(prop.key.name),
                      j.jsxExpressionContainer(j.identifier(prop.key.name)),
                    );
                  }),
                ],
                true,
              ),
              null,
              [],
            ),
          ),
        ]),
      );

      // Replace the old component with the new one
      root
        .find(j.ImportDeclaration)
        .at(-1)
        .forEach((importPath) => {
          j(importPath).insertAfter(newComponent);
        });

      root
        .find(j.VariableDeclaration)
        .filter(
          (variablePath) =>
            variablePath.value.declarations[0].id.name === componentName,
        )
        .forEach((variablePath) => {
          variablePath.value.declarations[0].init = j.taggedTemplateExpression(
            j.callExpression(j.identifier('styled'), [
              j.identifier('Unstyled' + componentName),
            ]),
            styledTemplateLiteral,
          );
        });

      j(path).remove();
    });

  return root.toSource({ quote: 'single' });
};

export default transform;

Input:

import styled from '@emotion/styled';
import { Box } from 'rebass';

const Container = styled(Box)`
  margin-left: auto;
  margin-right: auto;
  max-width: 1310px;
`;

Container.defaultProps = {
  px: 3,
};

export default Container;

Output:

import styled from '@emotion/styled';
import { Box } from 'rebass';

function UnstyledContainer(
  {
    px = 3,
    ...props
  }
) {
  return <Box {...props} px={px} />;
}

const Container = styled(UnstyledContainer)`
  margin-left: auto;
  margin-right: auto;
  max-width: 1310px;
`;

export default Container;

I run the script like this:

npx jscodeshift --parser=tsx --extensions=tsx,ts -t migrate-defaultProps.ts components/Container/index.tsx

Or, to run on a whole directory:

npx jscodeshift --parser=tsx --extensions=tsx,ts -t migrate-defaultProps.ts components/
AntonNiklasson commented 2 months ago

I've been looking into potentially migrating a large codebase from styled-components to emotion. We use .attrs quite heavily, it would be great to have an easy migration path for those use-cases.

I think it makes a lot of sense like this:

const SendIcon = styled(Icon).attrs({ iconId: 'send--filled' })`
  background: orange;
`;
Mario-Eis commented 2 months ago

Isn't this an option?

const StyledLink = styled(
    ({underline = "none", ...props}: LinkTypeMap["props"]) => <MuiLink underline={underline} {...props}/>
)`
    padding: ${({theme}) => `${theme.spacing(1)} 0`};
    display: inline-block;

    &.current {
        color: ${({theme}) => theme.palette.common.black};
    }
` as typeof MuiLink;

Not too far off of the attrs approach. And compatible to all IDE's SCSS code block highlighting. At least thats what I do. And it works pretty well so far.