Flashcard Challenge Project ( redux) : TypeError: Cannot read properties of undefined (reading 'quizIds')

Hello world,

I am stuck at step 14 of the Flashcards challenge project and I hope to get some help here.
I get this error, and I just can’t figure out what is the source

Blockquote
TypeError: Cannot read properties of undefined (reading ‘quizIds’)
Blockquote

my code is:

  1. topicsSlice

import {createSlice} from '@reduxjs/toolkit';

const topicsSlice = createSlice({

    name: 'topics',

    initialState: {

        topics: {}

    },

    reducers: {

        addTopic:(state, action) =>  {            

            const {id, name, icon} = action.payload;

            state.topics.id = {

                id,

                name,

                icon,

                quizIds: []

            }  

            console.log(action.payload);          

        },

        addQuizId:(state, action) => {

            console.log(action.payload);    

            const {quizId, topicId} = action.payload;

            state.topics[topicId].quizIds.push(quizId);                

        }

    }

});

export const selectTopics = state => state.topics.topics;

export const selectQuizIds = state => state.topics.quizIds;

export const {addTopic, addQuizId} = topicsSlice.actions;

export default topicsSlice.reducer;
  1. quizzesSlice.js
import { createSlice } from "@reduxjs/toolkit";
import {addQuizId} from '../topics/topicsSlice';


const quizzesSlice = createSlice({
    name: 'quizzes',
    initialState: {
        quizzes: {}
    },
    reducers: {        
        addQuiz: (state, action) => {
            const {quizId, name, topicId} = action.payload;
            state.quizzes[quizId] = {
                quizId,
                name,
                topicId,
                cardsdId: []
            }            
        }
    }
})
//action creator
export const thunkAddQuizCreator = (payload) => {
    const {quizId, topicId} = payload;
    return (dispatch) => {        
        dispatch(addQuiz(payload));
        dispatch(addQuizId({quizId: quizId, topicId: topicId}));
    };
};


//selectors
export const selectQuizzes = state => state.quizzes.quizzes;
export const {addQuiz} = quizzesSlice.actions;
export default quizzesSlice.reducer;

  1. newQuizForm.js
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import {selectTopics } from "../features/topics/topicsSlice";
import { thunkAddQuizCreator } from "../features/quizzes/quizzesSlice";

export default function NewQuizForm() {
  const dispatch = useDispatch();
  const [name, setName] = useState("");
  const [cards, setCards] = useState([]);
  const [topicId, setTopicId] = useState("");
  const history = useHistory();
  const topics = useSelector(selectTopics);
  const quizId = uuidv4();
  

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length === 0) {
      return;
    }

    const cardIds = [];
   
    const quizObj = {
      id: quizId,
      name: name,
      topicId: topicId,
      cardIds: cardIds
    }

    // create the new cards here and add each card's id to cardIds
    // create the new quiz here
    dispatch( thunkAddQuizCreator(quizObj));
   

    history.push(ROUTES.quizzesRoute());
  };

  const addCardInputs = (e) => {
    e.preventDefault();
    setCards(cards.concat({ front: "", back: "" }));
  };

  const removeCard = (e, index) => {
    e.preventDefault();
    setCards(cards.filter((card, i) => index !== i));
  };

  const updateCardState = (index, side, value) => {
    const newCards = cards.slice();
    newCards[index][side] = value;
    setCards(newCards);
  };

  return (
    <section>
      <h1>Create a new quiz</h1>
      <form onSubmit={handleSubmit}>
        <input
          id="quiz-name"
          value={name}
          onChange={(e) => setName(e.currentTarget.value)}
          placeholder="Quiz Title"
        />
        <select
          id="quiz-topic"
          onChange={(e) => setTopicId(e.currentTarget.value)}
          placeholder="Topic"
        >
          <option value="">Topic</option>
          {Object.values(topics).map((topic) => (
            <option key={topic.id} value={topic.id}>
              {topic.name}
            </option>
          ))}
        </select>
        {cards.map((card, index) => (
          <div key={index} className="card-front-back">
            <input
              id={`card-front-${index}`}
              value={cards[index].front}
              onChange={(e) =>
                updateCardState(index, "front", e.currentTarget.value)
              }
              placeholder="Front"
            />

            <input
              id={`card-back-${index}`}
              value={cards[index].back}
              onChange={(e) =>
                updateCardState(index, "back", e.currentTarget.value)
              }
              placeholder="Back"
            />

            <button
              onClick={(e) => removeCard(e, index)}
              className="remove-card-button"
            >
              Remove Card
            </button>
          </div>
        ))}
        <div className="actions-container">
          <button onClick={addCardInputs}>Add a Card</button>
          <button>Create Quiz</button>
        </div>
      </form>
    </section>
  );
}

4.console message

Blockquote
log.js:24 [HMR] Waiting for update signal from WDS…
topicsSlice.js:17 {topicId: ‘90eb5d78-7fef-47af-b8fd-6502eed61f11’, name: ‘mama’, icon: ‘’}
index.js:1 Warning: Each child in a list should have a unique “key” prop.

Check the render method of Topics. See https://reactjs.org/link/warning-keys for more information.
at li
at Topics (http://localhost:3000/static/js/main.chunk.js:2033:81)
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:39483:29)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:39685:29)
at TopicsRoutes (http://localhost:3000/static/js/main.chunk.js:139:85)
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:39483:29)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:39685:29)
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:39118:30)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:38738:35)
at App
at Provider (http://localhost:3000/static/js/vendors~main.chunk.js:36354:20)
console. @ index.js:1
overrideMethod @ react_devtools_backend.js:3973
printWarning @ react-jsx-dev-runtime.development.js:117
error @ react-jsx-dev-runtime.development.js:93
validateExplicitKey @ react-jsx-dev-runtime.development.js:986
validateChildKeys @ react-jsx-dev-runtime.development.js:1013
jsxWithValidation @ react-jsx-dev-runtime.development.js:1184
Topics @ Topics.js:12
renderWithHooks @ react-dom.development.js:14985
mountIndeterminateComponent @ react-dom.development.js:17811
beginWork @ react-dom.development.js:19049
beginWork$1 @ react-dom.development.js:23940
performUnitOfWork @ react-dom.development.js:22776
workLoopSync @ react-dom.development.js:22707
renderRootSync @ react-dom.development.js:22670
performSyncWorkOnRoot @ react-dom.development.js:22293
(anonymous) @ react-dom.development.js:11327
unstable_runWithPriority @ scheduler.development.js:646
runWithPriority$1 @ react-dom.development.js:11276
flushSyncCallbackQueueImpl @ react-dom.development.js:11322
flushSyncCallbackQueue @ react-dom.development.js:11309
discreteUpdates$1 @ react-dom.development.js:22420
discreteUpdates @ react-dom.development.js:3756
dispatchDiscreteEvent @ react-dom.development.js:5889
index.js:1 Warning: Each child in a list should have a unique “key” prop.

Check the render method of NewQuizForm. See https://reactjs.org/link/warning-keys for more information.
at option
at NewQuizForm (http://localhost:3000/static/js/main.chunk.js:565:83)
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:39483:29)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:39685:29)
at QuizRoutes (http://localhost:3000/static/js/main.chunk.js:193:85)
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:39483:29)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:39685:29)
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:39118:30)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:38738:35)
at App
at Provider (http://localhost:3000/static/js/vendors~main.chunk.js:36354:20)
console. @ index.js:1
overrideMethod @ react_devtools_backend.js:3973
printWarning @ react-jsx-dev-runtime.development.js:117
error @ react-jsx-dev-runtime.development.js:93
validateExplicitKey @ react-jsx-dev-runtime.development.js:986
validateChildKeys @ react-jsx-dev-runtime.development.js:1013
jsxWithValidation @ react-jsx-dev-runtime.development.js:1174
NewQuizForm @ NewQuizForm.js:62
renderWithHooks @ react-dom.development.js:14985
mountIndeterminateComponent @ react-dom.development.js:17811
beginWork @ react-dom.development.js:19049
beginWork$1 @ react-dom.development.js:23940
performUnitOfWork @ react-dom.development.js:22776
workLoopSync @ react-dom.development.js:22707
renderRootSync @ react-dom.development.js:22670
performSyncWorkOnRoot @ react-dom.development.js:22293
(anonymous) @ react-dom.development.js:11327
unstable_runWithPriority @ scheduler.development.js:646
runWithPriority$1 @ react-dom.development.js:11276
flushSyncCallbackQueueImpl @ react-dom.development.js:11322
flushSyncCallbackQueue @ react-dom.development.js:11309
discreteUpdates$1 @ react-dom.development.js:22420
discreteUpdates @ react-dom.development.js:3756
dispatchDiscreteEvent @ react-dom.development.js:5889
topicsSlice.js:20 {quizId: undefined, topicId: ‘mama’}
topicsSlice.js:22 Uncaught TypeError: Cannot read properties of undefined (reading ‘quizIds’)
at addQuizId (topicsSlice.js:22:1)
at createReducer.ts:247:1
at e.i.produce (immerClass.ts:96:1)
at createReducer.ts:246:1
at Array.reduce ()
at createReducer.ts:213:1
at combination (redux.js:459:1)
at k (:2235:16)
at D (:2251:13)
at :2464:20
at Object.dispatch (redux.js:213:1)
at e (:2494:20)
at serializableStateInvariantMiddleware.ts:190:1
at index.js:11:1
at immutableStateInvariantMiddleware.ts:262:1
at dispatch (redux.js:638:1)
at quizzesSlice.js:27:1
at index.js:8:1
at Object.dispatch (immutableStateInvariantMiddleware.ts:262:1)
at dispatch (:3665:80)
at handleSubmit (NewQuizForm.js:36:1)
at HTMLUnknownElement.callCallback (react-dom.development.js:3945:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js:3994:1)
at invokeGuardedCallback (react-dom.development.js:4056:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4070:1)
at executeDispatch (react-dom.development.js:8243:1)
at processDispatchQueueItemsInOrder (react-dom.development.js:8275:1)
at processDispatchQueue (react-dom.development.js:8288:1)
at dispatchEventsForPlugins (react-dom.development.js:8299:1)
at react-dom.development.js:8508:1
at batchedEventUpdates$1 (react-dom.development.js:22396:1)
at batchedEventUpdates (react-dom.development.js:3745:1)
at dispatchEventForPluginEventSystem (react-dom.development.js:8507:1)
at attemptToDispatchEvent (react-dom.development.js:6005:1)
at dispatchEvent (react-dom.development.js:5924:1)
at unstable_runWithPriority (scheduler.development.js:646:1)
at runWithPriority$1 (react-dom.development.js:11276:1)
at discreteUpdates$1 (react-dom.development.js:22413:1)
at discreteUpdates (react-dom.development.js:3756:1)
at dispatchDiscreteEvent (react-dom.development.js:5889:1)
addQuizId @ topicsSlice.js:22
(anonymous) @ createReducer.ts:247
i.produce @ immerClass.ts:96
(anonymous) @ createReducer.ts:246
(anonymous) @ createReducer.ts:213
combination @ redux.js:459
k @ VM553:2235
D @ VM553:2251
(anonymous) @ VM553:2464
dispatch @ redux.js:213
e @ VM553:2494
(anonymous) @ serializableStateInvariantMiddleware.ts:190
(anonymous) @ index.js:11
(anonymous) @ immutableStateInvariantMiddleware.ts:262
dispatch @ redux.js:638
(anonymous) @ quizzesSlice.js:27
(anonymous) @ index.js:8
(anonymous) @ immutableStateInvariantMiddleware.ts:262
dispatch @ VM553:3665
handleSubmit @ NewQuizForm.js:36
callCallback @ react-dom.development.js:3945
invokeGuardedCallbackDev @ react-dom.development.js:3994
invokeGuardedCallback @ react-dom.development.js:4056
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:4070
executeDispatch @ react-dom.development.js:8243
processDispatchQueueItemsInOrder @ react-dom.development.js:8275
processDispatchQueue @ react-dom.development.js:8288
dispatchEventsForPlugins @ react-dom.development.js:8299
(anonymous) @ react-dom.development.js:8508
batchedEventUpdates$1 @ react-dom.development.js:22396
batchedEventUpdates @ react-dom.development.js:3745
dispatchEventForPluginEventSystem @ react-dom.development.js:8507
attemptToDispatchEvent @ react-dom.development.js:6005
dispatchEvent @ react-dom.development.js:5924
unstable_runWithPriority @ scheduler.development.js:646
runWithPriority$1 @ react-dom.development.js:11276
discreteUpdates$1 @ react-dom.development.js:22413
discreteUpdates @ react-dom.development.js:3756
dispatchDiscreteEvent @ react-dom.development.js:5889
Show 9 more frames
react-dom.development.js:4091 Uncaught TypeError: Cannot read properties of undefined (reading ‘quizIds’)
at addQuizId (topicsSlice.js:22:1)
at createReducer.ts:247:1
at e.i.produce (immerClass.ts:96:1)
at createReducer.ts:246:1
at Array.reduce ()
at createReducer.ts:213:1
at combination (redux.js:459:1)
at k (:2235:16)
at D (:2251:13)
at :2464:20
at Object.dispatch (redux.js:213:1)
at e (:2494:20)
at serializableStateInvariantMiddleware.ts:190:1
at index.js:11:1
at immutableStateInvariantMiddleware.ts:262:1
at dispatch (redux.js:638:1)
at quizzesSlice.js:27:1
at index.js:8:1
at Object.dispatch (immutableStateInvariantMiddleware.ts:262:1)
at dispatch (:3665:80)
at handleSubmit (NewQuizForm.js:36:1)
at HTMLUnknownElement.callCallback (react-dom.development.js:3945:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js:3994:1)
at invokeGuardedCallback (react-dom.development.js:4056:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4070:1)
at executeDispatch (react-dom.development.js:8243:1)
at processDispatchQueueItemsInOrder (react-dom.development.js:8275:1)
at processDispatchQueue (react-dom.development.js:8288:1)
at dispatchEventsForPlugins (react-dom.development.js:8299:1)
at react-dom.development.js:8508:1
at batchedEventUpdates$1 (react-dom.development.js:22396:1)
at batchedEventUpdates (react-dom.development.js:3745:1)
at dispatchEventForPluginEventSystem (react-dom.development.js:8507:1)
at attemptToDispatchEvent (react-dom.development.js:6005:1)
at dispatchEvent (react-dom.development.js:5924:1)
at unstable_runWithPriority (scheduler.development.js:646:1)
at runWithPriority$1 (react-dom.development.js:11276:1)
at discreteUpdates$1 (react-dom.development.js:22413:1)
at discreteUpdates (react-dom.development.js:3756:1)
at dispatchDiscreteEvent (react-dom.development.js:5889:1)
addQuizId @ topicsSlice.js:22
(anonymous) @ createReducer.ts:247
i.produce @ immerClass.ts:96
(anonymous) @ createReducer.ts:246
(anonymous) @ createReducer.ts:213
combination @ redux.js:459
k @ VM553:2235
D @ VM553:2251
(anonymous) @ VM553:2464
dispatch @ redux.js:213
e @ VM553:2494
(anonymous) @ serializableStateInvariantMiddleware.ts:190
(anonymous) @ index.js:11
(anonymous) @ immutableStateInvariantMiddleware.ts:262
dispatch @ redux.js:638
(anonymous) @ quizzesSlice.js:27
(anonymous) @ index.js:8
(anonymous) @ immutableStateInvariantMiddleware.ts:262
dispatch @ VM553:3665
handleSubmit @ NewQuizForm.js:36
callCallback @ react-dom.development.js:3945
invokeGuardedCallbackDev @ react-dom.development.js:3994
invokeGuardedCallback @ react-dom.development.js:4056
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:4070
executeDispatch @ react-dom.development.js:8243
processDispatchQueueItemsInOrder @ react-dom.development.js:8275
processDispatchQueue @ react-dom.development.js:8288
dispatchEventsForPlugins @ react-dom.development.js:8299
(anonymous) @ react-dom.development.js:8508
batchedEventUpdates$1 @ react-dom.development.js:22396
batchedEventUpdates @ react-dom.development.js:3745
dispatchEventForPluginEventSystem @ react-dom.development.js:8507
attemptToDispatchEvent @ react-dom.development.js:6005
dispatchEvent @ react-dom.development.js:5924
unstable_runWithPriority @ scheduler.development.js:646
runWithPriority$1 @ react-dom.development.js:11276
discreteUpdates$1 @ react-dom.development.js:22413
discreteUpdates @ react-dom.development.js:3756
dispatchDiscreteEvent @ react-dom.development.js:5889
Show 9 more frames

Blockquote

This state setter doesn’t look right in this context: You always overwrite the value of topics.id with a new topic. But you would want to add a new topic instead. That topic should be keyed by the id of that particular topic and not by the string ‘id’. That means you need to use bracket notation with the payload id as you did with ‘addQuizId’.

This also seems to be not existent: Your state in ‘topics’ is initialized with only one property: topics. Inside topics, you would have several topic objects keyed by their ids that hold a property quizIds. In order for state.topics.quizIds to be defined, your state would have to look like this:

initialState: {

        topics: {},
        quizIds: []

    },

HI Mirja,

Many thanks for taking the time to review and reply .

If i understand right , in topics Slice.js I should revise

  1. state.topics.id to state.topics[id]
  2. initialState to
initialState: {
topics: {},
quizIds: []
}

I have tried this and I still get the same error , so please could you let me know if I am misunderstanding the advice?

Thank you!

Only if you extract ‘id’ from action.payload beforehand, otherwise ‘id’ will be undefined.

No, that was just a demonstration what you tried to export as selector here:

The fact that your state doesn’t look like that (and shouldn’t do) is the reason you get the reference error.
Your state – after the actions took place – should look something like this:

initialState: {
        topics: {
            idOfTopic1: {
                  id: idOfTopic1,
                  ...,
                  quizIds: [id1, id2, id3]
            },
            idOfTopic2: {
                  id: idOfTopic2,
                  ...,
                  quizIds: [id1, id2, id3]
            }
        }
    }

Your reducers and exports should mimic that structure. That means that you cannot export quizIds the way you do it (and why do you want to export it in the first place?).

Thanks for the reply!

I took out the selector for quizIds , thanks for that.

I believe the id is extracted from the action payload like this ? Should I do something further? :

const {topicId, name, icon} = action.payload;

I have revised my code as follows . but I am still getting the error

import {createSlice} from '@reduxjs/toolkit';

const topicsSlice = createSlice({

    name: 'topics',

    initialState: {

        topics: {}  

    },

    reducers: {

        addTopic:(state, action) =>  {    

            const {topicId, name, icon} = action.payload;

            state.topics[topicId] = {

                topicId,

                name,

                icon,

                quizIds: []

            };                    

        },

        addQuizId:(state, action) => {

            console.log(action.payload);

            const {quizId, topicId} = action.payload;

            state.topics[topicId].quizIds.push(quizId);                

        }

    }

});

export const selectTopics = state => state.topics.topics;

export const {addTopic, addQuizId} = topicsSlice.actions;

export default topicsSlice.reducer;

in Topics.js , my new topic looks like this :
image

while in quizzesSlice.js and topicsSlice.js it looks like this
image

Yes, your destructuring looks correct.

I don’t see what you’re doing in quizzesSlice. But apparently, you’re passing the wrong arguments as payload: Instead of topicId you receive the name of the topic in quizzesSlice as well as topicSlice. Where are you dispatching the actions?
Which component throws the error?

The error is thrown by topicsSlice .
Here is the quizzesSlice

import { createSlice } from "@reduxjs/toolkit";
import {addQuizId} from '../topics/topicsSlice';


const quizzesSlice = createSlice({
    name: 'quizzes',
    initialState: {
        quizzes: {}
    },
    reducers: {        
        addQuiz: (state, action) => {
            const {quizId, name, topicId} = action.payload;
            state.quizzes[quizId] = {
                quizId,
                name,
                topicId,
                cardIds: []
            }          
        }
    }
})
//action creator
export const thunkAddQuizCreator = (payload) => {
    console.log(payload);   
    const {quizId, topicId } = payload;
    return (dispatch) => {        
        dispatch(addQuiz(payload));
        dispatch(addQuizId({quizId, topicId}));
    };
};

//selectors
export const selectQuizzes = state => state.quizzes.quizzes;
export const {addQuiz} = quizzesSlice.actions;
export default quizzesSlice.reducer;

here is nowTopicForm.js

import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import { ALL_ICONS } from "../data/icons";
import { addTopic } from "../features/topics/topicsSlice";

export default function NewTopicForm() {
  const dispatch = useDispatch();
  const [name, setName] = useState("");
  const [icon, setIcon] = useState("");
  const history = useHistory();
  const topicId = uuidv4();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length === 0) {
      return;
    }

    // dispatch your add topic action here  
    const topicObj = {
      icon,
      name,
      topicId
    }
    dispatch(addTopic(topicObj));

    history.push(ROUTES.topicsRoute());
  };

  return (
    <section>
      <form onSubmit={handleSubmit}>
        <h1 className="center">Create a new topic</h1>
        <div className="form-section">
          <input
            id="topic-name"
            type="text"
            value={name}
            onChange={(e) => setName(e.currentTarget.value)}
            placeholder="Topic Name"
          />
          <select
            onChange={(e) => setIcon(e.currentTarget.value)}
            required
            defaultValue="default"
          >
            <option value="default" disabled hidden>
              Choose an icon
            </option>
            {ALL_ICONS.map(({ name, url }) => (
              <option key={url} value={url}>
                {name}
              </option>
            ))}
          </select>
        </div>
        <button className="center">Add Topic</button>
      </form>
    </section>
  );
}

and here is newQuizzForm.js

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import {selectTopics } from "../features/topics/topicsSlice";
import { thunkAddQuizCreator } from "../features/quizzes/quizzesSlice";

export default function NewQuizForm() {
  const dispatch = useDispatch();
  const [name, setName] = useState("");
  const [cards, setCards] = useState([]);
  const [topicId, setTopicId] = useState("");
  const history = useHistory();
  const topics = useSelector(selectTopics);
  const quizId = uuidv4();
  

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length === 0) {
      return;
    }

    const cardIds = [];
   
    const quizObj = {
      quizId,
      name,
      topicId,
      cardIds
    }

    // create the new cards here and add each card's id to cardIds
    // create the new quiz here
    dispatch(thunkAddQuizCreator(quizObj));
   

    history.push(ROUTES.quizzesRoute());
  };

  const addCardInputs = (e) => {
    e.preventDefault();
    setCards(cards.concat({ front: "", back: "" }));
  };

  const removeCard = (e, index) => {
    e.preventDefault();
    setCards(cards.filter((card, i) => index !== i));
  };

  const updateCardState = (index, side, value) => {
    const newCards = cards.slice();
    newCards[index][side] = value;
    setCards(newCards);
  };

  return (
    <section>
      <h1>Create a new quiz</h1>
      <form onSubmit={handleSubmit}>
        <input
          id="quiz-name"
          value={name}
          onChange={(e) => setName(e.currentTarget.value)}
          placeholder="Quiz Title"
        />
        <select
          id="quiz-topic"
          onChange={(e) => setTopicId(e.currentTarget.value)}
          placeholder="Topic"
        >
          <option value="">Topic</option>
          {Object.values(topics).map((topic) => (
            <option key={topic.id} value={topic.id}>
              {topic.name}
            </option>
          ))}
        </select>
        {cards.map((card, index) => (
          <div key={index} className="card-front-back">
            <input
              id={`card-front-${index}`}
              value={cards[index].front}
              onChange={(e) =>
                updateCardState(index, "front", e.currentTarget.value)
              }
              placeholder="Front"
            />

            <input
              id={`card-back-${index}`}
              value={cards[index].back}
              onChange={(e) =>
                updateCardState(index, "back", e.currentTarget.value)
              }
              placeholder="Back"
            />

            <button
              onClick={(e) => removeCard(e, index)}
              className="remove-card-button"
            >
              Remove Card
            </button>
          </div>
        ))}
        <div className="actions-container">
          <button onClick={addCardInputs}>Add a Card</button>
          <button>Create Quiz</button>
        </div>
      </form>
    </section>
  );
}

Have you logged what you dispatch here?

Here you set the name of the topic as topicId.

OMG this is it! It hadn’t even crossed my mind to look at the key, and all this time it was staring me in the face .

Thank you so much mirja_t. You have saved my mental sanity. :heartbeat:

1 Like

I got excited a bit too early , as the problem is only partially solved .

The code works fine when I create a new quiz under a created topic, but when I create a new quiz without choosing a topic from the list then I get the same error .

so this is the working version :
app:

console:
image

and this is the non working version :
image

console:
image

code:
topicsSlice:

import {createSlice} from '@reduxjs/toolkit';

const topicsSlice = createSlice({
    name: 'topics',
    initialState: {
        topics: {}   
    }, 
    reducers: {
        addTopic:(state, action) =>  {  
            console.log(action.payload);
            const {topicId, name, icon} = action.payload;
            state.topics[topicId] = {
                topicId,
                name,
                icon,
                quizIds: []
            };                     
        },
        addQuizId:(state, action) => {
            const {quizId, topicId} = action.payload;
            state.topics[topicId].quizIds.push(quizId);                
        }
    }
});

export const selectTopics = state => state.topics.topics;
export const {addTopic, addQuizId} = topicsSlice.actions;
export default topicsSlice.reducer;

Topics.js

import NewTopicForm from "../../components/NewTopicForm";
import { Link } from "react-router-dom";
import ROUTES from "../../app/routes";
import { selectTopics } from "./topicsSlice";
import { useSelector } from "react-redux";

export default function Topics() {
  const topics = useSelector(selectTopics); // replace this with a call to your selector to select all the topics in state
  console.log(topics);
  
  return (
    <section className="center">
      <h1>Topics</h1>
      <ul className="topics-list">
        {Object.values(topics).map((topic) => (
          <li className="topic" key={topic.topicId}>
          <Link to={ROUTES.topicRoute(topic.topicId)} className="topic-link">
           <div className="topic-container">
             <img src={topic.icon} alt="" />
             <div className="text-content">
               <h2>{topic.name}</h2>
               <p>{topic.quizIds.length} Quizzes</p>
             </div>
           </div>
         </Link>
          </li>
        ))}
      </ul>
      <Link
        to={ROUTES.newTopicRoute()}
        className="button create-new-topic-button"
      >
        Create New Topic
      </Link>
    </section>
  );
}

quizzesSlice :

import { createSlice } from "@reduxjs/toolkit";
import {addQuizId} from '../topics/topicsSlice';


const quizzesSlice = createSlice({
    name: 'quizzes',
    initialState: {
        quizzes: {}
    },
    reducers: {        
        addQuiz: (state, action) => {
            const {quizId, name, topicId} = action.payload;
            state.quizzes[quizId] = {
                quizId,
                name,
                topicId,
                cardIds: []
            }          
        }
    }
})
//action creator
export const thunkAddQuizCreator = (payload) => {  
    const {quizId, topicId } = payload;
    return (dispatch) => {        
        dispatch(addQuiz(payload));
        dispatch(addQuizId({quizId, topicId}));
    };
};

//selectors
export const selectQuizzes = state => state.quizzes.quizzes;
export const {addQuiz} = quizzesSlice.actions;
export default quizzesSlice.reducer;

Quizzes.js

import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import ROUTES from "../../app/routes";
import { selectQuizzes } from "./quizzesSlice";

export default function Quizzes() {
  const quizzes = useSelector(selectQuizzes); // replace this with a call to your selector to get all the quizzes in state
  return (
    <section className="center">
      <h1>Quizzes</h1>
      <ul className="quizzes-list">
        {Object.values(quizzes).map((quiz) => (
          <Link key={quiz.quizId} to={ROUTES.quizRoute(quiz.quizId)}>
            <li className="quiz">{quiz.name}</li>
          </Link>
        ))}
      </ul>
      <Link to={ROUTES.newQuizRoute()} className="button">
        Create New Quiz
      </Link>
    </section>
  );
}

newQuizForm

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import {selectTopics } from "../features/topics/topicsSlice";
import { thunkAddQuizCreator } from "../features/quizzes/quizzesSlice";

export default function NewQuizForm() {
  const dispatch = useDispatch();
  const [name, setName] = useState("");
  const [cards, setCards] = useState([]);
  const [topicId, setTopicId] = useState("");
  const history = useHistory();
  const topics = useSelector(selectTopics);
  const quizId = uuidv4();
  

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length === 0) {
      return;
    }

    const cardIds = [];
   
    const quizObj = {
      quizId,
      name,
      topicId,
      cardIds
    }
  console.log(quizObj);
    // create the new cards here and add each card's id to cardIds
    // create the new quiz here
    dispatch(thunkAddQuizCreator(quizObj));
   

    history.push(ROUTES.quizzesRoute());
  };

  const addCardInputs = (e) => {
    e.preventDefault();
    setCards(cards.concat({ front: "", back: "" }));
  };

  const removeCard = (e, index) => {
    e.preventDefault();
    setCards(cards.filter((card, i) => index !== i));
  };

  const updateCardState = (index, side, value) => {
    const newCards = cards.slice();
    newCards[index][side] = value;
    setCards(newCards);
  };

  return (
    <section>
      <h1>Create a new quiz</h1>
      <form onSubmit={handleSubmit}>
        <input
          id="quiz-name"
          value={name}
          onChange={(e) => setName(e.currentTarget.value)}
          placeholder="Quiz Title"
        />
        <select
          id="quiz-topic"
          onChange={(e) => setTopicId(e.currentTarget.value)}
          placeholder="Topic"
        >
          <option value="">Topic</option>
          {Object.values(topics).map((topic) => (
            <option key={topic.topicId} value={topic.topicId}>
              {topic.name}
            </option>
          ))}
        </select>
        {cards.map((card, index) => (
          <div key={index} className="card-front-back">
            <input
              id={`card-front-${index}`}
              value={cards[index].front}
              onChange={(e) =>
                updateCardState(index, "front", e.currentTarget.value)
              }
              placeholder="Front"
            />

            <input
              id={`card-back-${index}`}
              value={cards[index].back}
              onChange={(e) =>
                updateCardState(index, "back", e.currentTarget.value)
              }
              placeholder="Back"
            />

            <button
              onClick={(e) => removeCard(e, index)}
              className="remove-card-button"
            >
              Remove Card
            </button>
          </div>
        ))}
        <div className="actions-container">
          <button onClick={addCardInputs}>Add a Card</button>
          <button>Create Quiz</button>
        </div>
      </form>
    </section>
  );
}

newTopicForm

import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import { ALL_ICONS } from "../data/icons";
import { addTopic } from "../features/topics/topicsSlice";

export default function NewTopicForm() {
  const dispatch = useDispatch();
  const [name, setName] = useState("");
  const [icon, setIcon] = useState("");
  const history = useHistory();
  const topicId = uuidv4();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length === 0) {
      return;
    }

    // dispatch your add topic action here  
    const topicObj = {
      icon,
      name,
      topicId
    }
    
    dispatch(addTopic(topicObj));

    history.push(ROUTES.topicsRoute());
  };

  return (
    <section>
      <form onSubmit={handleSubmit}>
        <h1 className="center">Create a new topic</h1>
        <div className="form-section">
          <input
            id="topic-name"
            type="text"
            value={name}
            onChange={(e) => setName(e.currentTarget.value)}
            placeholder="Topic Name"
          />
          <select
            onChange={(e) => setIcon(e.currentTarget.value)}
            required
            defaultValue="default"
          >
            <option value="default" disabled hidden>
              Choose an icon
            </option>
            {ALL_ICONS.map(({ name, url }) => (
              <option key={url} value={url}>
                {name}
              </option>
            ))}
          </select>
        </div>
        <button className="center">Add Topic</button>
      </form>
    </section>
  );
}

Ok, when you do not chose a topic from the list the topicId will either be an empty string. Meaning this is what is dispatched to addQuizId in topicSlice:
state.topics[""].quizIds
state.topics[""] doesn’t exist in state.topics and is therefore undefined.
I doesn’t really make sense to dispatch the addQuizId action if there is no related topic.
What do the instructions say about this case?

I understand that a quiz without a chosen topic shouldn’t be allowed to be created in the first place . I haven’t found a particular instruction for it (but it is possible I have missed it) . I added an extra condition to newQuizForm handleSubmit as seen below , and this seems to do the trick .

Thank you again for all you help !

const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length === 0 || !topicId ) {
      return;
    }
    const cardIds = [];
   
    const quizObj = {
      quizId,
      name,
      topicId,
      cardIds
    }
    // create the new cards here and add each card's id to cardIds
    // create the new quiz here
    dispatch(thunkAddQuizCreator(quizObj));
   

    history.push(ROUTES.quizzesRoute());
  };
1 Like

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.