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

AutoControlledComponent #224

Merged
merged 3 commits into from
Apr 29, 2016
Merged

AutoControlledComponent #224

merged 3 commits into from
Apr 29, 2016

Conversation

levithomason
Copy link
Member

@levithomason levithomason commented Apr 27, 2016

AutoControlledComponent

This PR is a breakout of #206. This PR is actually tiny. It is mostly runtime warnings, tests, and comments. I've committed a "slim" version of the component with no runtime warnings nor comments so it can be reviewed easier. It is only 34 SLOC. I will remove it before merging.

Why this thing?

Many of our components have props that are possibly controlled by the consumer. Many of those props should also be self managed inside the component if there is no prop given.

Example, <Dropdown /> has a value prop. This is the value of the active item. When the user clicks on an item, we set the value on state internally. However, the consumer may have added a value prop to the <Dropdown /> as well. Now, when we want to access the value, we need to conditionally choose between which value we retrieve. The prop or the state?

Also, it would be responsible to never set state for the value if we were passed a value prop. This saves a render. Yet, another conditional, if there is no value prop then set state. This quickly becomes ugly and hard to maintain.

Community Options

We reviewed and tested other libraries solving this same problem:

These didn't work for us for the following:

  • dictates a single handler for each prop
  • dictates that each handler call back with the new prop value as the first argument to update props
  • HOC/decorator pattern causes troubles for our static props/subcomponents
  • HOC/decorator pattern cannot set computed props without a double initial render
  • Overall too restrictive in the implementation

Solution


What we really want is a smart setState() that submits to controlled props.

This solves all our issues. AutoControlledComponent is extended by your component, giving it a trySetState method and static autoControlledProps array.

static autoControlledProps = []

This array defines which props are auto controlled.

props to state
Auto controlled props are made available on state. The value will automatically defer to prop values if present (controlled) or the internal state value if there is no prop (uncontrolled).

trySetState(maybeState, [state])

maybeState
When trying to set maybe state, controlled prop values always win. Unnecessary renders are also prevented.

state
Allows setting regular state while simultaneously trying to set state for controlled values. You often need to set both, this allows a single call to setState and a single render.

Niceties

Auto default props defaultFoo

Each auto controlled prop also accepts a default* prop. It works exactly like vanilla React, only applied on first render.

Runtime Warnings

The majority of the code is actually run time warnings to ensure proper use of the abstraction. Warnings are removed in production by dead code elimination.

  • prevent defaultFoo={} along side foo={} (follows vanilla React)
  • require static autoControlledProps
  • require propTypes for autoControlledProps
  • require propTypes for corresponding default autoControlledProps
  • prevent listing defaultProps in autoControlledProps (automatically added)
  • prevent autoControlledProps in defaultProps (trySetState on construction instead)
  • prevent trySetState for props that are not auto controlled

@@ -35,6 +38,9 @@ module.exports = (karmaConfig) => {
...webpackConfig.module.loaders,
],
},
plugins: [
new webpack.DefinePlugin(config.compiler_globals),
],
Copy link
Member Author

Choose a reason for hiding this comment

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

Needed compiler globals for __PROD__ use.

Copy link
Member

Choose a reason for hiding this comment

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

Recommend using process.env.NODE_ENV. It's more verbose, but is expected in webpack builds nowadays (esp. for React), whereas the former is just an internal helper. This way users won't have to have __PROD__ as a global.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a task to the main PR. Will handle in the final cleanup/refactor.

@@ -0,0 +1,44 @@
/* eslint-disable no-console */
Copy link
Member Author

Choose a reason for hiding this comment

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

Review this file, this is identical to actual component, but with no comments nor run time warnings. It is only the production code. This makes it about 1/3 the size and much easier to grok.

import _ from 'lodash'
import { Component } from 'react'

const toDefault = (prop) => `default${prop.slice(0, 1).toUpperCase() + prop.slice(1)}`
Copy link
Member

Choose a reason for hiding this comment

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

.slice(0, 1) === prop[0], right?

Copy link
Member Author

Choose a reason for hiding this comment

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

don't school me publicly.... 😝

Copy link
Member Author

Choose a reason for hiding this comment

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

updated!

}

componentWillReceiveProps(nextProps) {
if (super.componentWillReceiveProps) super.componentWillReceiveProps(nextProps)
Copy link

@ghost ghost Apr 27, 2016

Choose a reason for hiding this comment

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

Is there ever a case where super.componentWillReceiveProps would ever return falsy given super represents React.Component here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, this seems intuitive. However, when removing the checks we get this in tests:

undefined is not an object (evaluating '_get(Object.getPrototypeOf(AutoControlledComponent.prototype), 'componentWillReceiveProps', this).call')

I'm not sure how React is constructing these classes, but apparently they do not have all their methods at least in tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

Note, I added the missing check/call to the componentWillMount() as well.

@levithomason levithomason force-pushed the feature/auto-controlled branch 3 times, most recently from db5150b to 86892f6 Compare April 27, 2016 20:52
@ghost
Copy link

ghost commented Apr 27, 2016

if (super.componentWillMount) super.componentWillMount()
const { autoControlledProps } = this.constructor

this.state = _.transform(autoControlledProps, (res, prop) => {
Copy link
Member

Choose a reason for hiding this comment

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

Wouldw be cool to get some comments around this. It took me a couple minutes to fully grok it especially due to the generically named to toDefault. Not at all a blocker, just a note.

Copy link
Member Author

Choose a reason for hiding this comment

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

react-controllables went with fromDefaultName, I'm indifferent but want it to make the most sense to the most people.

@dvdzkwsk
Copy link
Member

👻 - still a bit weary of the state overrides in trySetState, but you're the designer here and probably have a good reason. Merge at will, but would be happy to continue the discussion.

@levithomason
Copy link
Member Author

Addressed all comments in slim file and removed it, merging after tests pass.

@levithomason levithomason merged commit 326aaee into master Apr 29, 2016
@levithomason levithomason deleted the feature/auto-controlled branch April 29, 2016 22:43
@levithomason levithomason mentioned this pull request May 24, 2016
24 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants