@@ -1696,7 +1696,7 @@ describe('React', () => {
1696
1696
1697
1697
it ( 'should not error on valid component with circular structure' , ( ) => {
1698
1698
const createComp = Tag => {
1699
- const Comp = React . forwardRef ( function Comp ( props ) {
1699
+ const Comp = React . forwardRef ( function Comp ( props , ref ) {
1700
1700
return < Tag > { props . count } </ Tag >
1701
1701
} )
1702
1702
Comp . __real = Comp
@@ -3111,5 +3111,112 @@ describe('React', () => {
3111
3111
expect ( rendered . getByTestId ( 'child' ) . dataset . prop ) . toEqual ( 'b' )
3112
3112
} )
3113
3113
} )
3114
+
3115
+ it ( "should enforce top-down updates to ensure a deleted child's mapState doesn't throw errors" , ( ) => {
3116
+ const initialState = {
3117
+ a : { id : 'a' , name : 'Item A' } ,
3118
+ b : { id : 'b' , name : 'Item B' } ,
3119
+ c : { id : 'c' , name : 'Item C' }
3120
+ }
3121
+
3122
+ const reducer = ( state = initialState , action ) => {
3123
+ switch ( action . type ) {
3124
+ case 'DELETE_B' : {
3125
+ const newState = { ...state }
3126
+ delete newState . b
3127
+ return newState
3128
+ }
3129
+ default :
3130
+ return state
3131
+ }
3132
+ }
3133
+
3134
+ const store = createStore ( reducer )
3135
+
3136
+ const ListItem = ( { name } ) => < div > Name: { name } </ div >
3137
+
3138
+ let thrownError = null
3139
+
3140
+ const listItemMapState = ( state , ownProps ) => {
3141
+ try {
3142
+ const item = state [ ownProps . id ]
3143
+ // If this line executes when item B has been deleted, it will throw an error.
3144
+ // For this test to succeed, we should never execute mapState for item B after the item
3145
+ // has been deleted, because the parent should re-render the component out of existence.
3146
+ const { name } = item
3147
+ return { name }
3148
+ } catch ( e ) {
3149
+ thrownError = e
3150
+ }
3151
+ }
3152
+
3153
+ const ConnectedListItem = connect ( listItemMapState ) ( ListItem )
3154
+
3155
+ const appMapState = state => {
3156
+ const itemIds = Object . keys ( state )
3157
+ return { itemIds }
3158
+ }
3159
+
3160
+ function App ( { itemIds, deleteB } ) {
3161
+ const items = itemIds . map ( id => < ConnectedListItem key = { id } id = { id } /> )
3162
+
3163
+ return (
3164
+ < div className = "App" >
3165
+ { items }
3166
+ < button data-testid = "deleteB" > Delete B</ button >
3167
+ </ div >
3168
+ )
3169
+ }
3170
+
3171
+ const ConnectedApp = connect ( appMapState ) ( App )
3172
+
3173
+ const tester = rtl . render (
3174
+ < ProviderMock store = { store } >
3175
+ < ConnectedApp />
3176
+ </ ProviderMock >
3177
+ )
3178
+
3179
+ // This should execute without throwing an error by itself
3180
+ rtl . act ( ( ) => {
3181
+ store . dispatch ( { type : 'DELETE_B' } )
3182
+ } )
3183
+
3184
+ expect ( thrownError ) . toBe ( null )
3185
+ } )
3186
+
3187
+ it ( 'should re-throw errors that occurred in a mapState/mapDispatch function' , ( ) => {
3188
+ const counter = ( state = 0 , action ) =>
3189
+ action . type === 'INCREMENT' ? state + 1 : state
3190
+
3191
+ const store = createStore ( counter )
3192
+
3193
+ const appMapState = state => {
3194
+ if ( state >= 1 ) {
3195
+ throw new Error ( 'KABOOM!' )
3196
+ }
3197
+
3198
+ return { counter : state }
3199
+ }
3200
+
3201
+ const App = ( { counter } ) => < div > Count: { counter } </ div >
3202
+ const ConnectedApp = connect ( appMapState ) ( App )
3203
+
3204
+ const tester = rtl . render (
3205
+ < ProviderMock store = { store } >
3206
+ < ConnectedApp />
3207
+ </ ProviderMock >
3208
+ )
3209
+
3210
+ // Turn off extra console logging
3211
+ const spy = jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
3212
+
3213
+ expect ( ( ) => {
3214
+ rtl . act ( ( ) => {
3215
+ store . dispatch ( { type : 'INCREMENT' } )
3216
+ } )
3217
+ } ) . toThrow ( 'KABOOM!' )
3218
+
3219
+ spy . mockRestore ( )
3220
+ } )
3114
3221
} )
3115
3222
} )
0 commit comments