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

Stateless Components #28

Closed
wants to merge 15 commits into from
Closed

Conversation

iamdustan
Copy link
Contributor

Starts support for Stateless Component detection and documentation. Currently this adds an isStatelessComponent util method. I’ll start working on working this in to the resolvers now, but wanted early review on this to see if I missed anything.

describe('Stateless Function Components with React.createElement', () => {
it('accepts simple arrow function components', () => {
var def = parse(
'var Foo = () => React.creatElement("div", null);'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far. One remark is actually covered by a typo in this test :D (note the missing e in React.creatElement.

Seems like it accepts any function which calls another function as react component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, well, the issue is that I accidentally deleted the check to ensure that I was calling createElement in the visitor below so basically everything is matched 😇

I’ve gotten a bit stuck on trying to duplicate isReactCreateClassCall to isReactCreateElementCall which handles ensuring the correct createElement method is called and it looks up to the root import React bit to ensure it’s the real react.

in src/utils/resolveToValue.js path.scope.lookup(node.name) is always returning undefined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your test? You'd also have to add var React = require('react'); to the test code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’ve done that to each...well, technically I’ve done import React from 'react'; or import R from 'react';.

I’m bouncing for dinner, but should have the next push sometime tomorrow with the next set of questions :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Really appreciate your effort :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Found my sticking point. Since I’m calling recast.visit(node, {}) in the lcontainsJSXElementOrReactCreateElementCall function I’m losing all of recasts internal state

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Managed to get through this. Passing the root path instead of path.node didn’t lose state.

@iamdustan
Copy link
Contributor Author

I notice you have class components normalize to static propTypes rather than <ES6-style properties. I suspect I’ll need to the propTypeHandler to also read from <ES6-style properties for stateless components unless you have any other ideas.

function Component (props) {
  return <div />;
}

Component.propTypes = {};

@fkling
Copy link
Member

fkling commented Oct 7, 2015

I attribute it to bad design decisions at the beginning ;) Everything kind of expects that the result of the resolver is a single AST node and extracts data from there.

I guess we could introduce some new interface, ResolverResult, and have implementations for class, object literals (React.createClass) and stateless functions, to abstract from the different representations. Could be quite some work though.

Handlers shouldn't necessarily what kind of definition they are dealing with. That custom logic could be implemented in the getMemberValuePath method though, which already takes care of the differences between class declarations and object literals (React.class).

@iamdustan
Copy link
Contributor Author

I’m currently working on extending the getMemberValuePath to also getMemberExpressionValuePath and see how far that takes me. I think it’ll only need to handle variable declarations, function expressions, and function declarations. This should avoid needing an new interface and will continue building on the early design decisions :)

All of my API transformation projects have terrible design decisions. And be “terrible” I mean “no”.

@fkling
Copy link
Member

fkling commented Oct 7, 2015

All of my API transformation projects have terrible design decisions. And be “terrible” I mean “no”.

:)

@iamdustan
Copy link
Contributor Author

Well....I think this is actually working now.. 😮

@iamdustan
Copy link
Contributor Author

hmmm....not quite working, but close.

@iamdustan
Copy link
Contributor Author

Alright, fixed my final commit. This is now working for stateless function components in my current codebase as expected :)

@iamdustan
Copy link
Contributor Author

@fkling I probably have until EOD to finish this up then will have to bow out for a while. I want to prioritize supported use cases versus not supported use cases. This is the known issues ordered by what I think the priority should be. What do you think?

  • isStatelessComponent is too loose in saying true. Should scope this better (high priority)
const NotAComponent = () => {
  const Component = () => React.createElement('div', null);
  const AlsoAComponent = () => React.createElement('div', null);

  return {Component, AlsoAComponent};
}
  • hash of components (low/no priority)
const bag = {
  Component() { return React.createElement('div', null); },
  AlsoAComponent() { return React.createElement('div', null); }
};

bag.Components.propTypes = {
  /**
   * Hey I'm a string
   */
  foo: PropTypes.string.isRequired
};

@iamdustan
Copy link
Contributor Author

Also e4c867c is of note for the integration-style test proving that is indeed working correctly. :)

recast.visit(ast, {
visitFunctionDeclaration: statelessVisitor,
visitFunctionExpression: statelessVisitor,
visitProperty: statelessVisitor,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this, do we? What case does this capture which isn't captured otherwise?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this as part of supporting some of the permutations also being added to gaearon/babel-plugin-react-transform; namely this format: https://github.com/gaearon/babel-plugin-react-transform/pull/34/files#diff-9d6f6ff3fa6b5b63c09190bd7a2efb95R15

Honestly I’m not sure if that is captured or handled correctly yet. 😇

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be handled by the other visitors since methods are just represented as function expressions in the AST.

On Oct 7, 2015, at 12:51 PM, Dustan Kasten <[email protected]mailto:[email protected]> wrote:

In src/resolver/findAllComponentDefinitions.jshttps://github.com//pull/28#discussion_r41437429:

recast.visit(ast, {

  • visitFunctionDeclaration: statelessVisitor,
  • visitFunctionExpression: statelessVisitor,
  • visitProperty: statelessVisitor,

I added this as part of supporting some of the permutations also being added to gaearon/babel-plugin-react-transform; namely this format: https://github.com/gaearon/babel-plugin-react-transform/pull/34/files#diff-9d6f6ff3fa6b5b63c09190bd7a2efb95R15

Honestly I’m not sure if that is captured or handled correctly yet. [:innocent:]


Reply to this email directly or view it on GitHubhttps://github.com//pull/28/files#r41437429.

@fkling
Copy link
Member

fkling commented Oct 7, 2015

This is pretty awesome work, thank you so much! Regarding priorities:

isStatelessComponent is too loose in saying true. Should scope this better (high priority)

Definitely that one. Not sure how tricky this one is though.

hash of components (low/no priority)

I wonder what the use cases are for doing something like this. This would be covered by finding all component definitions anyway though, wouldn't it?


I was wondering about this following case:

function Foo() {
  return <div />;
}

Object.assign(Foo, {
  displayName: 'Foo',
  propTypes: { ... }
});

Do you think that would be a common use case? Or are propTypes and displayName the only properties one would add to stateless components? I guess defaultProps too. Maybe this is a "nice to have".

@iamdustan
Copy link
Contributor Author

isStatelessComponent is too loose in saying true. Should scope this better (high priority)

I have a couple ideas to try. The first is just to filter on all ReturnStatement’s in the body of a possible stateless component and detect if it’s a ReactElement. Though I’m not certain if these will be considered valid components off-hand. +@spicyj if I need to account for those. I don’t think they are at current..

const IsThisAComponent => null;
const IsThisAComponent => 'string';
const IsThisAComponent => 7;
const IsThisAComponent => [<div key="first" />, <div key="last" />];

The weakness is this will definitely fail for the following permutations:

const Component(props) {
  if (Math.random() > 0.5) return <div />;
  else return <span />;
}

const Component(props) {
  return (Math.random() > 0.5) ? <div /> : <span />;
}

Alternatively I could do some scope crawling and ensure that JSXElement and React.createElement are in not in a nested scope. This would handle the following case:

var HOC = (options) => {
  const Component = (props) => <div />
  Component.propTypes = {};
  return {Component};
}

Sadly, I think that to be totally accurate we’ll need to track all possible return values and ensure each is a JSXElement or React.createElement. E.g. lookup return values for both truthyFunc and falsyFunc in the following example:

const truthyFunc = () => <div />;
const falsyFunc = () => <span />;
const Component = (props) => props ? truthyFunc() : falsyFunc();

@sophiebits
Copy link
Member

@iamdustan Those aren't currently valid components but will be (at least the one with null) in 0.15.

@iamdustan
Copy link
Contributor Author

thanks @spicyj. I’ll punt that until 0.15 :)

@fkling
Copy link
Member

fkling commented Oct 7, 2015

In the worst case we can also introduce a doclet, like @react or @react-component to mark functions as React components that would be hard to identify otherwise.

@iamdustan
Copy link
Contributor Author

@fkling this now supports a pretty wide array of use cases. It’s not quite as bullet proof as I’d like, but this is all I have within me for now. Let me know what you think and if you want to block on anything.

};
return helpers.comp();
};
`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function for this isn’t quite recursive enough. While all of these resolve as expected, the following two cases do not resolve correctly yet:

const ComponentG = function(props) {
  var nested = {
    helpers: {
      comp() { return <div>{props.children}</div>; }
    }
  };
  return nested.helpers.comp();
};
const ComponentF = function(props) {
  var other = () => <div>{props.children}</div>;
  var helpers = {
    comp: other
  };
  return helpers.comp();
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works like a champ.

@iamdustan iamdustan changed the title WIP Stateless Components Stateless Components Oct 12, 2015
@iamdustan
Copy link
Contributor Author

@fkling I’m good here unless you come up with additional code examples that fail. A thorough review to undo the cobwebs I’ve created over the past week could likely help clean up the code in here.

@fkling
Copy link
Member

fkling commented Oct 12, 2015

@iamdustan: I'll try to get to it tomorrow or Wednesday!

@iamdustan
Copy link
Contributor Author

Another thought I had while adding so many use cases was: do we even want to support all the possible ways to do stateless components for this? The idea of these stateless function components is for simplicity. Documenting only a subset of these could help encourage folks to keep things simple.

@fkling
Copy link
Member

fkling commented Oct 14, 2015

do we even want to support all the possible ways to do stateless components for this?

Yep, good point. My answer to this is: no. We probably already don't support every possible way to create a React component and that's OK IMO.
A side effect of this project is that people have to follow certain guidelines to make it work, which in turn hopefully positively impacts consistency / familiarity across different projects / code bases.

Not sure I can get to it this week (will be away tomorrow for the rest of the week) and there is another issue I'd like to address before releasing a new major release (and I think this should be a major release because it could pickup components that haven't been considered as such).

@iamdustan
Copy link
Contributor Author

@@ -34,7 +35,18 @@ export default function findAllReactCreateClassCalls(
return false;
}

function statelessVisitor(path) {
if (isStatelessComponent(path)) {
// TODO: normalizeStatelessDefinition to pick up propTypes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done right? (because we use a different way to resolve this information)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why yes I do believe it is!

@jherr
Copy link

jherr commented Oct 23, 2015

👍

@iamdustan
Copy link
Contributor Author

Friendly ping for review.

@fkling
Copy link
Member

fkling commented Oct 29, 2015

Sorry, I thought I would be able to get something else in, but don't really have much time. I'm not so sure anymore this requires a major version bump. In the worst case it just finds more components.

A minor version bumpv should suffice an which case I can get this in now. What do you think?

@iamdustan
Copy link
Contributor Author

I’ve been using it without issues since the PR was opened so it feels generally safe to me. It shouldn’t cause any regressions on createClass or extends React.Component so it should be fine with just a minor, IMO.

@fkling
Copy link
Member

fkling commented Oct 30, 2015

Merged in 9f27006 and released as v2.4.

Thank you so much for this!

@fkling fkling closed this Oct 30, 2015
@iamdustan
Copy link
Contributor Author

Woohoo! Made it. Thanks!

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants