Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include .defaultProps in the documentation #2573

Open
drldavis opened this issue Nov 29, 2021 · 10 comments
Open

Include .defaultProps in the documentation #2573

drldavis opened this issue Nov 29, 2021 · 10 comments

Comments

@drldavis
Copy link

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
Copy link
Contributor

srmagura commented Dec 1, 2021

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

@drldavis
Copy link
Author

drldavis commented Dec 1, 2021

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
Copy link
Contributor

srmagura commented Dec 1, 2021

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
Copy link
Author

drldavis commented Dec 6, 2021

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
Copy link
Contributor

srmagura commented Dec 6, 2021

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
Copy link

lsegal commented Feb 3, 2024

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 #2573 (comment) 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
Copy link

karlhorky commented Mar 13, 2024

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
Copy link

AntonNiklasson commented May 10, 2024

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
Copy link

Mario-Eis commented May 17, 2024

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.

@nm2501
Copy link

nm2501 commented Sep 25, 2024

Agree with @lsegal . There is a clear need for a concise alternative to defaultProps now that it has been deprecated. When it's finally removed from React, for some users this will become an obstacle to the continued use of emotion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants