Skip to content
This repository has been archived by the owner on Apr 9, 2020. It is now read-only.

Support Functional Components #57

Closed
jamiebuilds opened this issue Dec 9, 2015 · 64 comments
Closed

Support Functional Components #57

jamiebuilds opened this issue Dec 9, 2015 · 64 comments

Comments

@jamiebuilds
Copy link
Collaborator

Moving from #34.

As of react@0.14, plain functions can be used as components. In the future this will bring performance optimizations in React.

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

Many people are already using these, there's even a Babel plugin (that I wrote) to transform pure component classes to pure component functions.

It would be great if babel-plugin-react-transform supported these. However, there is a pretty big concern.

Since these are just plain functions, with JSX in them there is nothing that designates that it is a react component. i.e. in order to tell that a class is a react component, we can test that it extends React.Component, or that it has a matching interface.

We can attempt to match them by treating "any function that has JSX in it will be treated as a component" i.e.:

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

However, this would also match the inner function in here:

class Component extends React.Component {
  render() {
    const children = this.props.items.map(function(props) {
      return <li>{props.name}</li>;
    });
    return <ul>{children}</ul>;
  }
}

So we can tighten things and say "only top level components", but then we run into wanting to support factories:

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

I could keep going for a while, you can put functions all over the place in JavaScript and no matter what we define as the rule here there will be exceptions. It's a matter of deciding what the heuristics should be that balances:

  1. Rarity - How often people will actually run into exceptions in real world code
  2. Obviousness - How easy it is to understand the reasoning behind an exception

It might be possible to get around the obviousness problem by throwing compile errors when running into ambiguous scenarios.

@kevinrenskers
Copy link

If you only do the top level ones automatically and would require a comment above or inside other functions? // @hmr: true or something

@jamiebuilds
Copy link
Collaborator Author

No, comments are a gross for this.

We get a ton of issues on Babel because of the Angular community using /* @ngInject */ comments to modify their code. Because comments are insignificant syntax they run into all sorts of problems.

I would try everything possible before using comments.

@gaearon
Copy link
Owner

gaearon commented Dec 9, 2015

I'd choose a convention a la "let/const function bindings at top level with capital letter names and JSX immediately inside."

In the first release I'd avoid trying to find factories altogether. I don't think that's a big problem.

Avoiding false positives is more important than finding every possible case. You can look at eslint React plugin as an inspiration for the heuristics.

@Restuta
Copy link

Restuta commented Dec 9, 2015

@thejameskyle I think what @gaearon suggests makes sense and as of next iteration we can support a "manual hint" case when heuristic didn't work. What are other options yo see besides comments? Can generators be used?

@gaearon
Copy link
Owner

gaearon commented Dec 9, 2015

An additional heuristic can be "a function with propTypes assigned to it later". Though React is gradually phasing out propTypes in favor of flow, it seems like a nice heuristic for most code bases.

@shinzui
Copy link

shinzui commented Dec 10, 2015

Can we limit the heuristic to any function that returns JSX and has props as its first argument?

@SpainTrain
Copy link

Can we limit the heuristic to any function that returns JSX and has props as its first argument?

Unfortunately, that would not cover the destructuring use case:

(from https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components)

// Or with destructuring and an implicit return, simply:
var Aquarium = ({species}) => (
  <Tank>
    {getFish(species)}
  </Tank>
);

@jamiebuilds
Copy link
Collaborator Author

The other issue that I didn't point out here is that many people do not use JSX in favor of something like hyperscript.

@shinzui
Copy link

shinzui commented Dec 10, 2015

@SpainTrain good point. I am even using destructuring in some of my stateless components. 😄

@shinzui
Copy link

shinzui commented Dec 10, 2015

@thejameskyle Shouldn't the first pass be focused only on the most common and recommended way (JSX/ES6) of writing stateless components?

@Restuta
Copy link

Restuta commented Dec 10, 2015

So far proposed heuristics are:

  • let/const function bindings at top level with capital letter names and JSX immediately inside
  • a function with propTypes assigned to it later

This doesn't cover:

  • non-JSX users that don't use propTypes
  • factories and some HOCs

And it's sounds like it's okay for now. Subjectively this covers majority of the cases.

We haven't yet discussed manual hinting, since I feel like it's a nice option.

@ghost
Copy link

ghost commented Dec 10, 2015

I think that going with something as basic as "let/const function bindings at top level with capital letter names and JSX immediately inside." is probably a great idea.

Right now, people are just saying "Please support functional components!"

Once you can say "They are supported as long as... xyz" then you will probably start getting people with issues letting you know when there is a scenario that "xyz" just doesn't work.

@insin
Copy link

insin commented Dec 10, 2015

How about TitleCase functions with a return statement which isn't guarded by a !(this instanceof ...) check, and React either imported or in function scope?

False positives would be constructors which handle creating more specialised instances which also happen to do something with React/JSX.

@mattkrick
Copy link

Could be wrong, but a functional component could be defined as:

  • name starts with a capital
  • has JSX directly inside
  • does not contain a render method
    Would that work for factories?

@Jadaw1n
Copy link

Jadaw1n commented Dec 11, 2015

Regarding manual hinting, would something like this work:

"HMR"; var Component = function() {
    return <div />;
}
// or:
var Component = function() { 
    "HMR"; 
    return <div />;
}

It's not a comment, and this pattern is used for strict mode ("use strict";), too.

@Restuta
Copy link

Restuta commented Dec 11, 2015

@insin

with a return statement which isn't guarded by a !(this instanceof ...) check

Can you elaborate a little, why return statement that isn't guarded with mentioned check is a good indicator of a component?

@satya164
Copy link

How about the following for the heuristics?

  1. The name is PascalCase
  2. Returns JSX or null
  3. Accepts only one argument (even with destructuring it's just one argument)
  4. [Bonus] Has contextTypes, propTypes, defaultProps

@ericclemmons
Copy link

  1. 👍 Convention usually has factory functions & decorators as camelCase.
  2. 👍 The presence of JSX seems like the best indicator to me, based on documentation, boilerplates, etc.
  3. I thought that pure functions received the signature (props, context)? So maybe not a reliable indicator.
  4. Even though it negates the savings of a "pure" component, opting in via propTypes would encourage a better development experience via both HMR & prop validation. Even with the slow movement towards Flow, it doesn't seem to have permeated enough to not use propTypes as an indicator.

My rationale is:

  • Pure components are the clearest way of defining a UI regarding best-practices. Minimizing hints (e.g. having to thoughtfully placing "HMR"; & even propTypes to improve the DX) is the ideal.

(Subscribed!)

@Restuta
Copy link

Restuta commented Dec 28, 2015

@ericclemmons agree with points, except propTypes. I think it should be optional and used as "manual hinting" when nothing else works. One of the great benefits of Functional Components is their ergonomics, simplicity to define and use. Having a need to think about propTypes all the time reduces that feel of "lightweightness".

@jamiebuilds
Copy link
Collaborator Author

One could poke holes through all of the heuristics listed in this thread. I'm guessing everyone here is basing it off of how they choose to personally write these components which is bound to cause issues. I'm convinced that this is a bad idea to support.

@jamiebuilds
Copy link
Collaborator Author

I'm thinking it'd be a better idea to tell people not to use functional components and just run their code through: https://github.com/thejameskyle/babel-plugin-react-pure-components which will optimize it to functional components for them. I'll need to put more work into that though

@VasilyShelkov
Copy link

👍 @gaearon, works like a charm and recommend it to others for hotloading functional components as well as redux !

VasilyShelkov added a commit to Winwardo/solid-octo-disco that referenced this issue Mar 5, 2016
This is because it doesn't work with functrional components, instead using the recommendation made here gaearon/babel-plugin-react-transform#57 since it works with functional React components which is what the React team recommends themselves !
@amcsi
Copy link

amcsi commented Mar 5, 2016

I don't understand. Has this answer been in front of us all along? And wasn't hmre needed for hot module replacement?

And if I get it straight, I just remove the react-hmre babel preset and then HMR will still work, functional components will also work, but in exchange local state won't work?

I wish I knew exactly how HMR worked internally.

@MoOx
Copy link
Contributor

MoOx commented Mar 5, 2016

HMR is just something that allow you to write this kind of code

if (module.hot) {
  // do something when a file has been hot loaded
}

babel-plugin-react-transform just insert some code using this module.hot test to force rerender your component when they have been updated.

Without this plugin, you can still use hot loading (as explained before) to rerender your entire app. This will be "ok" if you have a global store like redux (since you are not supposed with this approach to rely too much on local state).

@gaearon
Copy link
Owner

gaearon commented Mar 7, 2016

OK maybe we’ve been doing it from the wrong end all along:
https://twitter.com/dan_abramov/status/706681477157752835

@tonyxiao
Copy link

@gaearon didn't quite follow your last twitter comment. Could you explain how it's relevant?

Edit: Found this medium post. Awesome read and all is clear now. :) https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf#.fbkvfqotn

@tomduncalf
Copy link

As mentioned in https://github.com/reactjs/redux/pull/1455/files#r56386442, I would love to see a real world example of the "basic" (non-proxied) hot reloading with functional components in a project with Redux and react-router. I can get it to work for a single component but can't work out how to get the updates to bubble up to a single parent component properly and am generally a bit confused :) but would like to work with stateless/functional components and some kind of HMR if possible

@imranismail
Copy link

@tomduncalf don't have any open source project to show you at the moment, but you can take a look at this

// src/index.js
import 'babel-polyfill'
import 'whatwg-fetch'
import React from 'react'
import ReactDOM from 'react-dom'

const mountEl = document.getElementById('root')

let render = () => {
  const Root = require('./Root').default
  ReactDOM.render(<Root />, mountEl)
}

if (module.hot) {
  const renderApp = render

  const renderError = (error) => {
    const RedBox = require('redbox-react')
    ReactDOM.render(
      <RedBox error={ error } />, mountEl
    )
  }

  render = () => {
    try {
      renderApp()
    } catch (error) {
      renderError(error)
    }
  }

  module.hot.accept('./Root', () => {
    setTimeout(render)
  })
}

render()
// src/Root.js
import React from 'react'
import { Provider } from 'react-redux'
import { Router } from 'react-router'
import { hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import pure from 'recompose/pure'

import createStore from './store'
import routes from './routes'
import './styles/styles.scss'

const store = createStore()
const history = syncHistoryWithStore(hashHistory, store)

const Root = () =>
  <Provider store={ store }>
    <Router history={ history } routes={ routes } />
  </Provider>

export default pure(Root)
// src/routes.js
import React from 'react'
import { IndexRoute, Route } from 'react-router'
import { App, Home, NotFound } from './components/app'

export default (
  <Route path="/" component={ App }>
    { /* Main route */ }
    <IndexRoute component={ Home } />

    { /* Routes */ }

    { /* Catch all route */ }
    <Route path="*" component={ NotFound } status={ 404 } />
  </Route>
)

@tomduncalf
Copy link

Thanks imran! I thought that was exactly what I did but there must be some subtle differences so I will try again :)

On 16 Mar 2016, at 18:36, Imran Ismail notifications@github.com wrote:

@tomduncalf don't have any open source project to show you at the moment, but you can take a look at this

// src/index.js
import 'babel-polyfill'
import 'whatwg-fetch'
import React from 'react'
import ReactDOM from 'react-dom'

const mountEl = document.getElementById('root')

let render = () => {
const Root = require('./Root').default
ReactDOM.render(, mountEl)
}

if (module.hot) {
const renderApp = render

const renderError = (error) => {
const RedBox = require('redbox-react')
ReactDOM.render(
<RedBox error={ error } />, mountEl
)
}

render = () => {
try {
renderApp()
} catch (error) {
renderError(error)
}
}

module.hot.accept('./Root', () => {
setTimeout(render)
})
}

render()
// src/Root.js
import React from 'react'
import { Provider } from 'react-redux'
import { Router } from 'react-router'
import { hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import pure from 'recompose/pure'

import createStore from './store'
import routes from './routes'
import './styles/styles.scss'

const store = createStore()
const history = syncHistoryWithStore(hashHistory, store)

const Root = () =>
<Provider store={ store }>
<Router history={ history } routes={ routes } />

export default pure(Root)
// src/routes.js
import React from 'react'
import { IndexRoute, Route } from 'react-router'
import { App, Home, NotFound } from './components/app'

export default (
<Route path="/" component={ App }>
{ /* Main route */ }
<IndexRoute component={ Home } />

{ /* Routes */ }

{ /* Catch all route */ }
<Route path="*" component={ NotFound } status={ 404 } />
) — You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub

@drawveloper
Copy link

Sorry if I might be polluting this discussion with my lacking knowledge of how HMR is supposed to work, but could anyone explain to me why my attempt at a wrapper failed to solve this problem? https://gist.github.com/firstdoit/1fe09563cfe4dbb04246

The intended use was to simply wrap the export of any functional component and giving HMR a full React 'class' to play with.

@webyak
Copy link

webyak commented Apr 4, 2016

Any updates on this? It would be awesome to use functional components without ditching the benefits of this plugin.

@MoOx
Copy link
Contributor

MoOx commented Apr 4, 2016

@webyak This will likely never happen. See #57 (comment)

@gaearon
Copy link
Owner

gaearon commented Apr 4, 2016

Not entirely “will never happen”. As I said in #57 (comment), supporting functional components will require a different approach but this doesn’t mean the benefits of this plugin are going away. You can track gaearon/react-transform-boilerplate#125 as something might happen there eventually, or read https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf for more context.

@gaearon
Copy link
Owner

gaearon commented Apr 18, 2016

React Hot Loader 3 supports functional components without destroying the state.
It supersedes both React Hot Loader and React Transform, and is built on lessons learned from both.
Check it out: gaearon/react-hot-boilerplate#61

@gaearon gaearon closed this as completed Apr 18, 2016
@tomduncalf
Copy link

Thanks @gaearon, can't wait to try it out!

@amcsi
Copy link

amcsi commented Apr 18, 2016

So let me get it straight: react hot loader was deprecated in favor of react transform, which in turn is deprecated back in favor of react hot loader again?

@thangngoc89
Copy link

@amcsi React hot loader 3.0 is a combination of both React hot loader 1.0 and react transform. And it works with function component

@amcsi
Copy link

amcsi commented Apr 18, 2016

I don't quite get it, but all right.
Also, will this along with syntax error catching work just with webpack-dev-server and not having to do things manually with webpack-dev-middleware?

@gaearon
Copy link
Owner

gaearon commented Apr 18, 2016

So let me get it straight: react hot loader was deprecated in favor of react transform, which in turn is deprecated back in favor of react hot loader again?

Yes. Both approaches were bad in different ways. I’m trying a third, mixed, approach, and it makes sense to brand it as React Hot Loader, as it doesn’t allow arbitrary transforms anymore.

Also, will this along with syntax error catching work just with webpack-dev-server and not having to do things manually with webpack-dev-middleware?

It is absolutely orthogonal: you can use either webpack-dev-server and webpack-hot-middleware with either of these three projects 😄 . That said the syntax error overlay is a feature of webpack-hot-middleware. If you’d like to see it in webpack-dev-server, feel free to file an issue there—none of my projects have any say in this at all, I only work on hot reloading React code itself, not the transport or middleware.

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

No branches or pull requests