reactjstypescriptreduxredux-toolkit

Mapping in component with Redux and getting error noticesType.map is not a function


I have all the needed working pieces of the React Redux-toolkit working but I can't seem to get my data to display in the component. I'm getting the error:

TypeError: noticesType.map is not a function
 31 |             {noticesType.map((post, index) => { 
    |                          ^
 32 |               <li key={index}>

When I see is that I'm most likely passing an object when it's looking for an array. I am pretty sure I have it set up correctly. I have The Redux-DevTools open, and it shows me the correct data coming back from the API call. After 3 days of searching the web, this is where I'm at. If there is more code or anything I've missed please let me know.

page.tsx

export default function page() {
  const dispatch = useAppDispatch();
  const { Notices, loading, error } = useAppSelector(
    state => state.noticesReducer
  )
  console.log('Values: ')
  console.log(Notices)
  
  useEffect(() => { 
    dispatch(fetchNotices())
  }, [dispatch])
  
  return (
    <>
      <div> 
        <h1>Notices</h1>
        <ul>  
          <>    
            {Notices.map((post, index) => { 
              <li key={index}>
                {post.Title}
              </li>
            })}                
          
          </>                
        </ul>
      </div>
    </>
  )
}

noticesTypes.ts Updated with JSON to TypeScript tool.

export interface Root {
   Notices: Notice[]
   CreatedDateTime: string
   Version: number
   Title: string
}
  
export interface Notice {
 id: string
 Title: string
 Description: string
 ChangeRecordNumber: any
 StartDateTime: string
 EndDateTime: string
 Planned: boolean
 ImpactType: string    
}

notice.ts / noticeSlice

type NoticeState = Root & {
  loading: boolean,
  error: boolean,
}

const initialState: NoticeState = {
  Notices: [],
  loading: false,
  error: false
}

export const fetchNotices = createAsyncThunk(
  'fetchNotices',
  async (thunkApi, { rejectWithValue }) => {
    try {
      const response = await fetch("https://API_CALL/notice")
      return await response.json();
    } catch (error) {
      return rejectWithValue("Something isn't working");
    }
  }
);

export const noticeSlice = createSlice({
  name: 'notices',
  initialState,
  reducers: {
    setNotice: (state, action) => { 
      state.Notices = action.payload
    }, 
    getNotice: (state, action) => { 
      return action.payload.notices
    },         
  }, 
  extraReducers: (builder) => { 
    builder
      .addCase(fetchNotices.pending, (state, action) => {            
        state.loading = true;
      })
      .addCase(fetchNotices.fulfilled, (state, action) => {             
        state.loading = false, 
        state.Notices = action.payload
      })
      .addCase(fetchNotices.rejected, (state, { payload }) => { 
        state.loading = true                       
      })
  }
})

export const { 
  getNotice,
  setNotice,     
} = noticeSlice.actions

export default noticeSlice.reducer

index.ts / store

export const store = configureStore({ 
  reducer: { 
    noticesReducer, 
  }, 
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware({ serializableCheck: false })
})

console.log(store.getState()); 

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

JSON coming back from API call.

{
  "Notices": [
    {
      "id": "1",
      "Title": "test title 1",
      "Description": "Fake Notice: this is fake",            
      "StartDateTime": "2024-09-18",
      "EndDateTime": "2024-09-26",
      "Planned": true,            
      "ImpactedApps": [
        {
          "Id": "1",
          "Name": "Api 1"
        }
      ],       
      "IsActive": true
    }, 
    {
      "id": "2",
      "Title": "test title 2",
      "Description": "Fake Notice: this is fake",            
      "StartDateTime": "2024-09-18",
      "EndDateTime": "2024-09-26",
      "Planned": true,            
      "ImpactedApps": [
        {
          "Id": "2",
          "Name": "Api 2"
        }
      ],       
      "IsActive": true
    }
  ],
  "CreatedDateTime": "2024-09-18",
  "Version": 1,
  "Title": "Notification Feed"
}

Here is an image of what's in the console when I console.log() it out.

console.log data

Here is the Notices expanded

enter image description here


Solution

  • Issue

    The issue is that you are not maintaining a valid state invariant. At some point during the execution of your code you modified state.Notices from an array type to something else, likely the entire javascript object that is the response value from the fetchNotices action.

    Given the following response value:

    {
      "Notices": [
        {
          "id": "1",
          "Title": "test title 1",
          "Description": "Fake Notice: this is fake",            
          "StartDateTime": "2024-09-18",
          "EndDateTime": "2024-09-26",
          "Planned": true,            
          "ImpactedApps": [
            {
              "Id": "1",
              "Name": "Api 1"
            }
          ],       
          "IsActive": true
        }, 
        {
          "id": "2",
          "Title": "test title 2",
          "Description": "Fake Notice: this is fake",            
          "StartDateTime": "2024-09-18",
          "EndDateTime": "2024-09-26",
          "Planned": true,            
          "ImpactedApps": [
            {
              "Id": "2",
              "Name": "Api 2"
            }
          ],       
          "IsActive": true
        }
      ],
      "CreatedDateTime": "2024-09-18",
      "Version": 1,
      "Title": "Notification Feed"
    }
    

    The featchNotices.fulfilled reducer case is passing the entire response payload object and this breaks the invaint.

    .addCase(fetchNotices.fulfilled, (state, action) => {             
      state.loading = false;
      state.Notices= action.payload; // <-- not an array! 😞
    })
    

    Solution Suggestions

    The code should pass the nested Notices array property to the state value.

    .addCase(fetchNotices.fulfilled, (state, action) => {             
      state.loading = false;
      state.Notices = action.payload.Notice; // <-- an array 🙂
    })
    

    Alternatively you could return the notices array directly from the Thunk action if you have no need for the other response values.

    export const fetchNotices = createAsyncThunk('fetchNotices', async (thunkApi, { rejectWithValue }) => {
      try {
        const response = await fetch("https://API_CALL/notice")                          
        const { Notices } = await response.json(); 
        return Notices;
      } catch (error) {
        return rejectWithValue("Something isn't working"); 
      }
    }); 
    
    .addCase(fetchNotices.fulfilled, (state, action) => {             
      state.loading = false;
      state.Notices = action.payload; // <-- an array 🙂
    })