Open drldavis opened 2 years ago
Could you post some code to show how you expected it work? And how you finally got it to work?
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"
}
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;
}
`
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.
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".
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.
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/
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;
`;
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.
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