@@ -28,46 +28,43 @@ import { Component } from 'react'
2828
2929const getDefaultPropName = ( prop ) => `default${ prop [ 0 ] . toUpperCase ( ) + prop . slice ( 1 ) } `
3030
31+ /**
32+ * Return the auto controlled state value for a give prop. The initial value is chosen in this order:
33+ * - default props
34+ * - then, regular props
35+ * - then, `checked` defaults to false
36+ * - then, `value` defaults to '' or [] if props.multiple
37+ * - else, undefined
38+ *
39+ * @param {object } props A props object
40+ * @param {string } propName A prop name
41+ * @param {boolean } [includeDefaultProps=false] Whether or not to heed the default prop value
42+ */
43+ export const getAutoControlledStateValue = ( props , propName , includeDefaultProps = false ) => {
44+ const defaultPropName = getDefaultPropName ( propName )
45+ const prop = props [ propName ]
46+ const defaultProp = props [ defaultPropName ]
47+
48+ const hasProp = prop !== undefined
49+ const hasDefaultProp = defaultProp !== undefined
50+
51+ // defaultProps & props
52+ if ( includeDefaultProps && ! hasProp && hasDefaultProp ) return defaultProp
53+ if ( hasProp ) return prop
54+
55+ // React doesn't allow changing from uncontrolled to controlled components,
56+ // default checked/value if they were not present.
57+ if ( propName === 'checked' ) return false
58+ if ( propName === 'value' ) return props . multiple ? [ ] : ''
59+
60+ // otherwise, undefined
61+ }
62+
3163export default class AutoControlledComponent extends Component {
3264 componentWillMount ( ) {
3365 if ( super . componentWillMount ) super . componentWillMount ( )
3466 const { autoControlledProps } = this . constructor
3567
36- // Auto controlled props are copied to state.
37- // Set initial state by copying auto controlled props to state.
38- // Also look for the default prop for any auto controlled props (foo => defaultFoo)
39- // so we can set initial values from defaults.
40- this . state = _ . transform ( autoControlledProps , ( res , prop ) => {
41- const defaultPropName = getDefaultPropName ( prop )
42-
43- // try to set initial state in this order:
44- // - default props
45- // - then, regular props
46- // - then, `checked` defaults to false
47- // - then, `value` defaults to null
48- // React doesn't allow changing from uncontrolled to controlled components
49- // this is why we default checked/value if they are not present.
50- if ( _ . has ( this . props , defaultPropName ) ) {
51- res [ prop ] = this . props [ defaultPropName ]
52- } else if ( _ . has ( this . props , prop ) ) {
53- res [ prop ] = this . props [ prop ]
54- } else if ( prop === 'checked' ) {
55- res [ prop ] = false
56- } else if ( prop === 'value' ) {
57- res [ prop ] = this . props . multiple ? [ ] : '' // eslint-disable-line react/prop-types
58- }
59-
60- if ( process . env . NODE_ENV !== 'production' ) {
61- const { name } = this . constructor
62- // prevent defaultFoo={} along side foo={}
63- if ( defaultPropName in this . props && prop in this . props ) {
64- console . error (
65- `${ name } prop "${ prop } " is auto controlled. Specify either ${ defaultPropName } or ${ prop } , but not both.`
66- )
67- }
68- }
69- } , { } )
70-
7168 if ( process . env . NODE_ENV !== 'production' ) {
7269 const { defaultProps, name, propTypes } = this . constructor
7370 // require static autoControlledProps
@@ -116,14 +113,48 @@ export default class AutoControlledComponent extends Component {
116113 ] . join ( ' ' ) )
117114 }
118115 }
116+
117+ // Auto controlled props are copied to state.
118+ // Set initial state by copying auto controlled props to state.
119+ // Also look for the default prop for any auto controlled props (foo => defaultFoo)
120+ // so we can set initial values from defaults.
121+ this . state = autoControlledProps . reduce ( ( acc , prop ) => {
122+ acc [ prop ] = getAutoControlledStateValue ( this . props , prop , true )
123+
124+ if ( process . env . NODE_ENV !== 'production' ) {
125+ const defaultPropName = getDefaultPropName ( prop )
126+ const { name } = this . constructor
127+ // prevent defaultFoo={} along side foo={}
128+ if ( defaultPropName in this . props && prop in this . props ) {
129+ console . error (
130+ `${ name } prop "${ prop } " is auto controlled. Specify either ${ defaultPropName } or ${ prop } , but not both.`
131+ )
132+ }
133+ }
134+
135+ return acc
136+ } , { } )
119137 }
120138
121139 componentWillReceiveProps ( nextProps ) {
122140 if ( super . componentWillReceiveProps ) super . componentWillReceiveProps ( nextProps )
141+ const { autoControlledProps } = this . constructor
142+
143+ // Solve the next state for autoControlledProps
144+ const newState = autoControlledProps . reduce ( ( acc , prop ) => {
145+ const isNextUndefined = _ . isUndefined ( nextProps [ prop ] )
146+ const propWasRemoved = ! _ . isUndefined ( this . props [ prop ] ) && isNextUndefined
147+
148+ // if next is defined then use its value
149+ if ( ! isNextUndefined ) acc [ prop ] = nextProps [ prop ]
123150
124- // props always win, update state with all auto controlled prop
125- const newState = _ . pick ( nextProps , this . constructor . autoControlledProps )
126- if ( ! _ . isEmpty ( newState ) ) this . setState ( newState )
151+ // reinitialize state for props just removed / set undefined
152+ else if ( propWasRemoved ) acc [ prop ] = getAutoControlledStateValue ( nextProps , prop )
153+
154+ return acc
155+ } , { } )
156+
157+ if ( Object . keys ( newState ) . length > 0 ) this . setState ( newState )
127158 }
128159
129160 /**
@@ -147,12 +178,19 @@ export default class AutoControlledComponent extends Component {
147178 }
148179 }
149180
150- // pick auto controlled props
151- // omit props from parent
152- let newState = _ . omit ( _ . pick ( maybeState , autoControlledProps ) , _ . keys ( this . props ) )
181+ let newState = Object . keys ( maybeState ) . reduce ( ( acc , prop ) => {
182+ // ignore props defined by the parent
183+ if ( this . props [ prop ] !== undefined ) return acc
184+
185+ // ignore props not listed in auto controlled props
186+ if ( autoControlledProps . indexOf ( prop ) === - 1 ) return acc
187+
188+ acc [ prop ] = maybeState [ prop ]
189+ return acc
190+ } , { } )
153191
154192 if ( state ) newState = { ...newState , ...state }
155193
156- if ( ! _ . isEmpty ( newState ) ) this . setState ( newState )
194+ if ( Object . keys ( newState ) . length > 0 ) this . setState ( newState )
157195 }
158196}
0 commit comments