@@ -1696,7 +1696,7 @@ describe('React', () => {
16961696
16971697 it ( 'should not error on valid component with circular structure' , ( ) => {
16981698 const createComp = Tag => {
1699- const Comp = React . forwardRef ( function Comp ( props ) {
1699+ const Comp = React . forwardRef ( function Comp ( props , ref ) {
17001700 return < Tag > { props . count } </ Tag >
17011701 } )
17021702 Comp . __real = Comp
@@ -3111,5 +3111,112 @@ describe('React', () => {
31113111 expect ( rendered . getByTestId ( 'child' ) . dataset . prop ) . toEqual ( 'b' )
31123112 } )
31133113 } )
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+ } )
31143221 } )
31153222} )
0 commit comments