Skip to content

Commit 43bc8cb

Browse files
committed
Fix compatibility with React 16.4
React 16.4 included a change to the behavior of `getDerivedStateFromProps` which is now called after internal state updates in addition to received props. This change updates the comparison logic between current and incoming date values to ensure the local state is not overwritten with the prop value immediately following a state update.
1 parent 64666c0 commit 43bc8cb

5 files changed

Lines changed: 71 additions & 50 deletions

File tree

src/DateInputController.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,29 +98,22 @@ DateInputController.defaultProps = {
9898
};
9999

100100
DateInputController.getDerivedStateFromProps = (props: Props, state: State) => {
101-
const newValue = !areEqualDates(state.value, props.value);
102-
const newMin = !areEqualDates(state.min, props.min);
103-
const newMax = !areEqualDates(state.max, props.max);
104-
const newUTC = state.utc !== props.utc;
101+
const {value = new Date(0), min, max, utc} = props;
102+
103+
const newValue = !areEqualDates(state.props.value, value);
104+
const newMin = !areEqualDates(state.props.min, min);
105+
const newMax = !areEqualDates(state.props.max, max);
106+
const newUTC = state.props.utc !== utc;
105107

106108
// Check if any of the props that can affect state have changed.
107109
if (!newValue && !newMin && !newMax && !newUTC) {
108110
return null;
109111
}
110112

111-
let nextState = Object.assign({}, state);
112-
nextState.utc = props.utc;
113-
114-
if (newMin) {
115-
nextState.min = props.min;
116-
}
117-
118-
if (newMax) {
119-
nextState.max = props.max;
120-
}
113+
let nextState = Object.assign({}, state, {props: {value, min, max, utc}});
121114

122115
const updaters = fieldKeys.map((_, i) =>
123-
updateField(i, getDateField[i](props.value, props.utc)),
116+
updateField(i, getDateField[i](newValue ? value : state.value, utc)),
124117
);
125118

126119
// Calculate a new state from the incoming props.

src/types.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ export type FieldActions = {
3333
};
3434

3535
export type State = {
36+
props: {
37+
value: Date,
38+
min?: Date,
39+
max?: Date,
40+
utc: boolean,
41+
},
3642
value: Date,
37-
min?: Date,
38-
max?: Date,
39-
utc: boolean,
4043

4144
setFields(fields: {
4245
year?: number | string,

src/util.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const fieldKeys = ['year', 'month', 'day', 'hour', 'minute', 'second'];
4040

4141
export const dateValue = (state: State): Date => {
4242
const args = fieldKeys.map((_, i) => state[fieldKeys[i]]);
43-
if (state.utc) {
43+
if (state.props.utc) {
4444
return new Date(Date.UTC.apply(null, args));
4545
}
4646
// $ExpectError Flow doesn't like us touching Date.bind
@@ -53,11 +53,11 @@ const atMin = (state: State, index: number) => {
5353
return true;
5454
}
5555

56-
const min = state.min;
56+
const min = state.props.min;
5757
return (
5858
min &&
5959
((index === 0 ? true : atMin(state, index - 1)) &&
60-
getDateField[index](min, state.utc) >= state[fieldKeys[index]])
60+
getDateField[index](min, state.props.utc) >= state[fieldKeys[index]])
6161
);
6262
};
6363

@@ -66,11 +66,11 @@ const atMax = (state: State, index: number) => {
6666
return true;
6767
}
6868

69-
const max = state.max;
69+
const max = state.props.max;
7070
return (
7171
max &&
7272
((index === 0 ? true : atMax(state, index - 1)) &&
73-
getDateField[index](max, state.utc) <= state[fieldKeys[index]])
73+
getDateField[index](max, state.props.utc) <= state[fieldKeys[index]])
7474
);
7575
};
7676

@@ -84,21 +84,21 @@ const resolveDefaultMax = (state: State, index: number) => {
8484
};
8585

8686
export const getMin = (state: State, index: number) => {
87-
const min = state.min;
87+
const min = state.props.min;
8888
// If min date is defined and the next-largest field is at its min value...
8989
return min && atMin(state, index - 1)
9090
? // return the value of the min date at the current field...
91-
getDateField[index](min, state.utc)
91+
getDateField[index](min, state.props.utc)
9292
: // or return a default min value for the field.
9393
defaultMin[index];
9494
};
9595

9696
export const getMax = (state: State, index: number) => {
97-
const max = state.max;
97+
const max = state.props.max;
9898
// If max date is defined and the next-largest field is at its max value...
9999
return max && atMax(state, index - 1)
100100
? // return the value of the max date at the current field...
101-
getDateField[index](max, state.utc)
101+
getDateField[index](max, state.props.utc)
102102
: // or return a default max value for the field.
103103
resolveDefaultMax(state, index);
104104
};
@@ -144,16 +144,18 @@ export const getInitialState = (
144144
): {state: State, actions: FieldActions} => {
145145
const updaters = [];
146146

147+
const {value = new Date(0), min, max, utc} = target.props;
148+
147149
const actions: $Shape<FieldActions> = {};
148150
let state: $Shape<State> = {
149-
value: target.props.value === undefined ? new Date(0) : target.props.value,
150-
min: target.props.min,
151-
max: target.props.max,
152-
utc: target.props.utc,
151+
value,
152+
props: {value, min, max, utc},
153153
};
154154

155155
fieldKeys.forEach((field, i) => {
156-
updaters.push(updateField(i, getDateField[i](state.value, state.utc)));
156+
updaters.push(
157+
updateField(i, getDateField[i](state.value, state.props.utc)),
158+
);
157159

158160
const setter = (value) => {
159161
const fields = {};

test/DateInputController.test.js

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ describe('<DateInputController/>', () => {
7979
expect(state).toEqual(null);
8080
});
8181

82+
it('should default undefined value to date with value 0', () => {
83+
const instance = getInstance({value: date});
84+
85+
const nextProps = {...instance.props, value: undefined};
86+
87+
const state = DateInputController.getDerivedStateFromProps(
88+
nextProps,
89+
instance.state,
90+
);
91+
92+
expect(state.props.value.getTime()).toEqual(new Date(0).getTime());
93+
});
94+
8295
it('should return updated state if value prop has changed', () => {
8396
const instance = getInstance({value: date});
8497

@@ -99,7 +112,7 @@ describe('<DateInputController/>', () => {
99112
// $ExpectError cast nullable state as any
100113
): any);
101114

102-
const {value, day, second, ...rest} = state;
115+
const {value, day, second, props: _props, ...rest} = state;
103116

104117
expect(day).toEqual(nextDate.getDate());
105118

@@ -132,11 +145,9 @@ describe('<DateInputController/>', () => {
132145
// $ExpectError cast nullable state as any
133146
): any);
134147

135-
const {value, min, hour, hourMin, ...rest} = state;
136-
137-
expect(min).toBe(min);
148+
const {value, hour, hourMin, props, ...rest} = state;
138149

139-
expect(value.getHours()).toBe(min.getHours());
150+
expect(value.getHours()).toBe(props.min.getHours());
140151

141152
expect(hour).toBe(nextMin.getHours());
142153

@@ -166,11 +177,9 @@ describe('<DateInputController/>', () => {
166177
// $ExpectError cast nullable state as any
167178
): any);
168179

169-
const {value, max, hour, hourMax, ...rest} = state;
180+
const {value, hour, hourMax, props, ...rest} = state;
170181

171-
expect(max).toBe(max);
172-
173-
expect(value.getHours()).toBe(max.getHours());
182+
expect(value.getHours()).toBe(props.max.getHours());
174183

175184
expect(hour).toBe(nextMax.getHours());
176185

@@ -192,28 +201,42 @@ describe('<DateInputController/>', () => {
192201
): any);
193202

194203
const {
195-
max,
196204
yearMax,
197205
monthMax,
198206
dayMax,
199207
hourMax,
200208
minuteMax,
201209
secondMax,
210+
props,
202211
...rest
203212
} = state;
204213

205-
expect(max).toBe(date);
214+
expect(props.max).toBe(date);
206215

207-
expect(yearMax).toBe(max.getFullYear());
208-
expect(monthMax).toBe(max.getMonth());
209-
expect(dayMax).toBe(max.getDate());
210-
expect(hourMax).toBe(max.getHours());
211-
expect(minuteMax).toBe(max.getMinutes());
212-
expect(secondMax).toBe(max.getSeconds());
216+
expect(yearMax).toBe(props.max.getFullYear());
217+
expect(monthMax).toBe(props.max.getMonth());
218+
expect(dayMax).toBe(props.max.getDate());
219+
expect(hourMax).toBe(props.max.getHours());
220+
expect(minuteMax).toBe(props.max.getMinutes());
221+
expect(secondMax).toBe(props.max.getSeconds());
213222

214223
// it should leave remaining state value unchanged
215224
expect(shallowIntersect(rest, instance.state)).toBe(true);
216225
});
226+
227+
it('should not overwrite state change with props', () => {
228+
const instance = getInstance({value: date});
229+
230+
// Update internal state.
231+
instance.setFields({year: date.getFullYear() + 1});
232+
233+
const state = DateInputController.getDerivedStateFromProps(
234+
instance.props,
235+
instance.state,
236+
);
237+
238+
expect(state).toEqual(null);
239+
});
217240
});
218241
});
219242

test/util.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ describe('utils', () => {
208208
});
209209

210210
it('should return a Date representation of the state in UTC', () => {
211-
state.utc = true;
211+
state.props.utc = true;
212212

213213
expect(dateValue(state).getTime()).toEqual(
214214
Date.UTC(

0 commit comments

Comments
 (0)