-
Notifications
You must be signed in to change notification settings - Fork 82
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
ReactCDK: Add JSX/TSX Support #256
Comments
Some helpful notes: Webpack for example uses JSX as configuration https://webpack.js.org/configuration/configuration-languages/#babel-and-jsx They use this JSX factory https://github.com/developit/jsxobj |
+100 on this idea. JSX is a wonderful language that perfectly represents data-rich components. Component hierarchy is an important component of CDK. Why not represent it in a tree-like structure instead of a bunch of class member reference chains? I've always wondered if CDK could be considered a good example of how not to use OOP. This should be totally do-able with React and a custom renderer that proxies props through to CDK constructors. |
I thought it'd be fun to work backwards from what a ReactCDK app could look like. import * as ECS from '@aws-cdk/react';
export default function MyApp() {
return (
<VPC>
<ECS.Cluster mode="fargate">
<ECS.ApplicationLoadBalancedFargateService>
<ECS.TaskDefinition>
<ECS.Container fromRegistry="amazon/cloudwatch-agent" />
<ECS.Container fromRegistry="my/app" />
</ECS.TaskDefinition>
</ECS.ApplicationLoadBalancedFargateService>
</ECS.Cluster>
</VPC>
);
}; Obviously this example over-simplifies the number of props you'd need to pass to each JSX element, but you'd be able to abstract that away behind other components. You'll naturally start moving complexity out of the root component and down into descendants, abstracting common branches of your architecture with higher-order concepts. Next comes the magic: your root component file no longer feels as daunting as the first sentence of a Steven King novel. Instead, it looks as simple and easily-understood as a diagram. You've achieved self-documenting code. export default function MyApp() {
return (
<Infrastructure>
<Services>
<FooBarService />
<DataBoiService />
<SecretProject />
</Services>
<Ops>
<Alarms />
<CICD />
<Dashboards />
</Ops>
</Infrastructure>
);
}; |
For referencing constructs between them a ref can be used I think export default function MyApp() {
const fooBar = useRef()
return (
<Infrastructure>
<Services>
<FooBarService ref={fooBar} />
<DataBoiService />
<SecretProject fooBar={fooBar} />
</Services>
</Infrastructure>
);
}; |
Having a hook specifically for creating exports in cross stack references would resolve a pretty serious design flaw in current CDK known as ‘deadly embrace’. The automatic export mechanism when referencing resources across stacks is a dangerous kind of magic and has jammed up my pipelines many times. Defining your infrastructure in JSX would enforce a unidirectional flow of resource dependencies. Manually opting in to exports using a hook like ‘useStackExport’ is much more predictable and maintainable than the current automatic approach. More reading on ‘deadly embrace’ here: aws/aws-cdk#12778 |
This was fun to play around with. Someone who knows React could probably make this slicker, but it's not too hard to get started using jsxobj to make some simple components. I added the following to my tsconfig.json:
The code was pretty simple, but it's not quite what it should be:
Resulting template snippet:
|
Started to do some experiments related to this RFC It basically works, but we need a way to write also imperative code. ps: I don't have too much knowledge about AWS/CLI/CDK and how it works, just touching the waters |
One more attemp creating a custom renderer because we still need to write somehow imperative code. https://github.com/iamandrewluca/react-constructs There are some problems, waiting for an advice from react team. facebook/react#13006 (comment) ps: I don'y know why I'm doing this 😄 I never played with AWS CDK, and I think will never have the oportunity soon, but it's interesting 🙂 |
it seems that a custom renderer it's too compicated, and I't may not work. Will make a POC using default AWS CDK sample app |
remained to improve TypeScript support, and publish it. Would be Action: https://github.com/iamandrewluca/constructs-jsx/runs/4127293576?check_suite_focus=true#step:4:8 |
@iamandrewluca super cool and very exciting. |
An update: Implementing TypeScript support is not fully possible at the moment if we want to use directly To tell TS how to infer a declare namespace JSX {
interface ElementAttributesProperty {
props; // specify the property name to use
}
} This means that TS will look at So first, we don't have a field on the If I go for example in class Stack {
/* ... */
props: StackOptions
} If we had the export class Construct<Options extends ConstructOptions = {}> implements IConstruct {
options: Options;
constructor(scope: Construct, id: string, options: Options) {
// this is optional, we just need the type above
this.options = options;
// ...
}
} One more solution (that I don't like) instead of using directly a @aws-cdk/core/react or react-cdk/@aws-cdk/core that is generated automatically import { StackProps, StageProps } from '@aws-cdk/core'
export const Stack = '@aws-cdk/core/Stack'
export const Stage = '@aws-cdk/core/Stage'
// ...
declare global {
declare namespace JSX {
// For string types TS looks at this interface to infer props
interface IntrinsicElements {
[Stack]: StackProps
[Stage]: StageProps
// ...
}
}
} user code import { Stack } from '@aws-cdk/core/react'
const element = <Stack /> in renederer something like this function render(element, parent) {
const [user, package, construct] = element.type.split('/')
const Construct = require(`${user}/${package}`)[construct]
return new Construct(parent, element.key, element.props)
} |
@iamandrewluca For AWS CDK L2s we might be able to add a "props" property to all types. We have awslint which can enforce that. Is this something you might be interested to contribute? We'll need to work with @rix0rrr to flush the details. |
@eladb yes, I'm in. I will need some giudance. One more thing. Using JSX there is one limitation. Because
<Stack key="unique-id" ref={stack} props={{ /* ... */ }} /> Infering props complicates with this method |
The simplest solution would seem to be generating |
The problem with
Yes, doing this way, nothing should be changed in But with this solution you would need to generate this react "fake libraries" for each cdk library
|
Picking up this topic, I've started experimenting a bit with this idea. The following is I will work on it a bit more to figure out the restrictions and what they would mean for an RFC. |
I've experimented a bit more, and it would not be a problem to add a JavaScript layer which allows the following code (without need for custom wrappers for each construct, note that /* @jsx h */
import h, { attachToApp } from "../src";
import { App, Duration, Stack, StackProps } from "aws-cdk-lib";
import { Queue } from "aws-cdk-lib/aws-sqs";
const CdkExampleStack = (props: StackProps & { id: string }) => {
return <Stack id={props.id} env={props.env}>
<Queue id="CdkExampleQueue" visibilityTimeout={Duration.seconds(300)} />
</Stack>;
};
void attachToApp(
<CdkExampleStack
env={{ account: "111111111", region: "eu-central-1" }}
id="CdkExampleStack"
/>,
new App()
); Unfortunately, TypeScript currently wouldn't allow it, as it limits which kind of classes can be used as JSX Elements. I've opened an issue with TypeScript, but I do not expect this to be of any priority to them, as JSX so far is used almost exclusively for React or compatible libraries. Without that, we could create workarounds like the following: /* @jsx h */
import h, { attachToApp, C } from "../src";
import { App, Duration, Stack, StackProps } from "aws-cdk-lib";
import { Queue } from "aws-cdk-lib/aws-sqs";
const CdkExampleStack = (props: StackProps & { id: string }) => {
return <C construct={Stack} id={props.id} env={props.env}>
<C construct={Queue} id="CdkExampleQueue" visibilityTimeout={Duration.seconds(300)} />
</C>;
};
void attachToApp(
<CdkExampleStack
env={{ account: "111111111", region: "eu-central-1" }}
id="CdkExampleStack"
/>,
new App()
); I don't think, this would cut it, though, and therefore I have decided to drop this until there has been some movement on the TypeScript front, or a non-class interface that is supported by CDK. |
The ticket for TypeScript is now in "Awaiting more feedback", so if this would also help you, please write about your use case there. |
Nice. Also we may approach this problem from both sides.
|
Is there a common parent class where this might best be implemented? E. g. we could add a export abstract class PropsResource<Props = unknown> extends Resource {
public readonly props: Props;
constructor(scope: Construct, id: string, props: Props & ResourceProps) {
super(scope, id, props);
this.props = props
}
} and then change all L2 constructs to rely on this instead of If this is a way forward, I'm happy to contribute it. What would be the next step? |
I think |
To add it there would be a breaking change, though, as the constructor would need to receive the props. I don't think this is an option. |
I supposed to make this props optional from the beginning. 🤔 |
I think the use of Refs in https://github.com/iamandrewluca/react-cdk/blob/main/lib/constructs-jsx-stack.tsx makes sense, but it also makes it clear that using JSX/React paradigms for CDK is not going to be that ergonomic without changes to the way CDK models its component tree and APIs. CDK is fundamentally highly imperative by design, and I don't really see that changing any time soon. I do sometimes wish that CDK had less boiler-plate for constructing the component tree, but my opinion is that the JSX/React model is too far away from the current CDK model. JSX trees use postorder traversal, CDK trees use preorder traversal. React tries to make you use a unidirectional data flow where CDK APIs tend to be all about imperative child -> parent interaction e.g. calling methods on children. That said, is the push here to make CDK more declarative, or to reduce CDK boilerplate using a DSL? One of the reasons that React embraced declarative, unidirectional data flow was to make reasoning about application state easier. This choice obviously comes with tradeoffs, so it feels like you might be signing up for all the cons of unidirectional data flow and none of the pros, since CDK apps don't have state and render just once. So then, maybe the real objective should be a better DSL for building CDK trees? It should be possible to create some DSL / syntactic sugar for CDK stacks that compliments CDK's constraints rather than working against them. I think it's a good idea to work backwards from the constrains / programming model to arrive at the DSL you want rather than trying to shoehorn CDK into a different paradigm. A really nice DSL for CDK:
As for ergonomics, JSX may or may not still be preferable but a non JSX dsl might look like this:
Where
Not that confident Edit: The other thing this DSL should do is allow you to write constructs like this:
Function constructs could be invoked within the dsl and have their underlying constructs' constructors automatically called with the correct scope and id |
Marking this RFCs as |
Description
Express Infrastructure Resources using React JSX/TSX. This is an additional mechanism to compose services/CDK applications at a framework / technology level over a programming language.
Progress
The text was updated successfully, but these errors were encountered: