Search code examples
reactjsreact-hooksreact-props

Props not being passed from the parent to the child or grandchild while using useReducer


So I wanted to simplify passing the props to the grandchildren components of a Parent component where change the state of buttons or modals being open or closed such as: [open, setOpen]= useState(), could be simpliefied to a useReducer and passed from the parent Component. Anyhow I am not able to understand why I it is not working, perhaps I understood wrong the logic. Ideally I wanted to approach the DRY principal, and pass the props as simple as possible. The error: AddButtonModal.tsx:36 Warning: Failed prop type: The prop openis marked as required inForwardRef(Modal2), but its value is undefined.

Parent component:

import React, {useReducer, useState} from "react";
import {Ingredient} from "../../models/ingredient";
import {Box} from "@mui/material";
// Importing the 4 components for their respective part that way we have a more concise Page
import AddButtonModal from "./components/IngredientsManagementViewComponents/AddButtonModal";
import IngredientCategoryMenu from "./components/IngredientsManagementViewComponents/IngredeintCategoryMenu";
import SearchBarComponent from "./components/IngredientsManagementViewComponents/SearchBar";
import ingredientReducer, {IngredientActionTypes, initialState} from "./reducers/ingredientReducer";
import modalReducer, {ModalActionTypes, initialModalState} from "./reducers/modalReducer";
import ListOfIngredients from "./components/IngredientsManagementViewComponents/ListOfIngredients";


const IngredientsViewPage: React.FC = () => {
//states
//state of the new ingredient
  const [ingredientState, dispatch] = useReducer(ingredientReducer, initialState);
  console.log("ingredientState:", ingredientState);
// state for the open/close function for modals/buttons
  const [modalState, modalDispatch] = useReducer(modalReducer, initialModalState);
  console.log("modalState: within IVP", modalState);
  console.log("initialModalState within IVP: ", initialModalState);
  const handleOpenModal = () => { 
    console.log("Open Modal");
    modalDispatch({ type: ModalActionTypes.OPEN_MODAL}) };
  const handleCloseModal = () => { 
    console.log("Close Modal");
    modalDispatch({type: ModalActionTypes.CLOSE_MODAL}) };

  /*An object refering to the Ingredient model properties which can later be called to map each Number Input */
  const IngredientFieldsOptions = {
    carbs: {label: "Carbohydrates", value: ingredientState.carbs},
    protein: {label: "Protein", value: ingredientState.protein},
    sugar: {label: "Sugar", value: ingredientState.sugar},
    fat:{label: "Fat", value: ingredientState.fat},
    fiber: {label: "Fiber", value: ingredientState.fiber}
  }

  return (
    <Box sx={{ display: "flex", flexDirection: "column",
    bgcolor: "whitesmoke", borderRadius: "3px", borderBlockColor: "none"}}>
      {/*Search field with search lens*/}
      <Box sx={{display: "flex", flexDirection: "row", width: "100%"}}>
      <SearchBarComponent/>
      {/*Button with the + symbol, opens a modal */}
      <AddButtonModal 
      // IngredientFieldsOptions={IngredientFieldsOptions}
      isModalOpen={modalState.isModalOpen}
      handleOpenModal={handleOpenModal}
      handleCloseModal={handleCloseModal}
      />
      </Box>
      {/* Component with the Categories menu */}
      <IngredientCategoryMenu/>
      {/* Component List of Ingredients */}
      <ListOfIngredients 
        open={modalState.isModalOpen}
        handleOpenModal={handleOpenModal}
        handleCloseModal={handleCloseModal}
        ingredientState={ingredientState}
        dispatch={dispatch}
      />
    </Box>
  )
};

export default IngredientsViewPage;

Child:

import React, {useReducer, useState} from "react";
import {Box, Button, Modal, Typography} from "@mui/material";
import Add from "@mui/icons-material/Add";
import ingredientReducer, {IngredientActionTypes, initialState} from "../../reducers/ingredientReducer.ts";
import {Ingredient} from "../../../../models/ingredient.ts";
import XButton from "./XButton.tsx";
import IngredientFields from "./IngredientFields.tsx";
import DropDowns from "./DropDown.tsx";
import CheckBoxSection from "./CheckBoxSection.tsx";
import NumberInputFields from "./NumberInputFields.tsx";
import SaveButton from "./SaveButton.tsx";
import { dummy_allergens } from "../../../../dummy_data/dummy_allergens.ts";
import { dummy_ingredients } from "../../../../dummy_data/dummy_ingredients.ts";
import { number } from "yup";


//defined types of the props 
interface AddButtonModalProps {
  isModalOpen: boolean;
  handleOpenModal: () => void;
  handleCloseModal: () => void;
} 
const AddButtonModal: React.FC<AddButtonModalProps> = ({isModalOpen, handleOpenModal, handleCloseModal}) => {
const [ingredientState, dispatch] = useReducer(ingredientReducer, initialState);
console.log("modalState: within ABM", isModalOpen);
return (
    <Box sx={{marginBottom: 2, }}>
        <Button onClick={handleOpenModal} sx={{
          minWidth: 100,
          height: "auto",
          display: "flex",
          }}
          startIcon={<Add />}
          id="Container-search bar & + button">
          Add new
          <Modal
            open={isModalOpen}
            onClose={handleCloseModal}
            aria-labelledby="modal-modal-title"
            aria-describedby="modal-modal-description"
          >
            <Box sx={{
              position: "absolute",
              top: "50%",
              left: "50%",
              transform: "translate(-50%, -50%)",
              width: "70vw",
              bgcolor: "whitesmoke",
              boxShadow: 24,
              borderRadius: "3px",
              p: 4,
              overflowY: "auto", // Scroll for overflow content
              maxHeight: "90vh",
              }} id="main container"
            onClick={(event) => event.stopPropagation()}
            >
              {/* Label & X icon button */}
              <Box sx={{display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center"}} 
                  id="container-title & close-icon">
              <Typography id="modal-modal-title" variant="h2" component="h2" sx={{fontSize: "1.2rem"}}>
                    ADD INGREDIENT
              </Typography>
              <XButton handleCloseModal={handleCloseModal}/>
              </Box>
              {/* Ingredients Fields Components*/}
              // name of the ingredient
              <IngredientFields />
              {/* Dropdowns components */}
              <DropDowns />
              {/* Checkboxes components */}
              <CheckBoxSection/>
              {/* Number Input */}
              <NumberInputFields 
              ingredientState={ingredientState}
              dispatch={dispatch}
              // IngredientFieldsOptions={IngredientFieldsOptions}
               />
              {/*  Save Button */}
              <SaveButton handleCloseModal={handleCloseModal}/>
            </Box>
          </Modal>
        </Button>
      </Box>
  );
};

export default AddButtonModal;

Grandchild:

import React from "react";
import {Box, IconButton} from "@mui/material";
import ClearIcon from '@mui/icons-material/Clear';

//defined types of the props
interface XButtonProps {
  handleCloseModal: () => void;
}
const XButton: React.FC<XButtonProps> = ({handleCloseModal}) => {
    return (
            <Box>
                  {/* Icon that closes the modal */}
                  <IconButton onClick={handleCloseModal} style={{ cursor: 'pointer' }}>
                    <ClearIcon />
                  </IconButton>
                  </Box>
    );
};

export default XButton;

Recuder:

// Define action types related to modal operation
export enum ModalActionTypes {
OPEN_MODAL = 'OPEN_MODAL',
CLOSE_MODAL = 'CLOSE_MODAL'
}
// set opening action
type OpenModalAction = {
    type: ModalActionTypes.OPEN_MODAL
};
// set closing action
type CloseModalAction = {
    type: ModalActionTypes.CLOSE_MODAL
}
// Union type for modal-related actions
type ModalAction = OpenModalAction | CloseModalAction;
// initial state of the modal
export const initialModalState = {
    isModalOpen: false
};
/** reducer function for handling of modal state changes 
    @params {object} state: current state
    @param {ModalAction} action: action to be handled
    @returns {object} updated state of modal
    */
function modalReducer(state = initialModalState, action: ModalAction) {
    switch (action.type) {
        case ModalActionTypes.OPEN_MODAL:
            return {...state, isModalOpen: true};
        case ModalActionTypes.CLOSE_MODAL:
            return {...state, isModalOpen: false};
        default:
            return state;
        // throw Error("Unknown action: " + action.type);
    }
}

export default modalReducer;

I tried it out with console.log to see what the state is in each Component: the Parent component displays:

modalState: within IVP , isModalOpen: false line 22,
modalState: within IVP , isModalOpen: false line 23,

after clicking the button which should open the modal, I can see in the console

OpenModal ln 25,
modalState: within IVP , isModalOpen: true line 22,
modalState: within IVP , isModalOpen: false line 23.

I tried as well to hard code: isModalOpen={modalState.isModalOpen} in line 49 in the parent component. I can distinguish that the parent does have the state but will not be passed along.

Additionally I read this post but I am not really sure that helped. Warning: Failed prop type: The prop open is marked as required in Snackbar, but its value is undefined


Solution

  • The Modal component that AddButtonModal renders it seems requires the open prop. AddButtonModal passes it's isModalOpen prop through, but AddButtonModal itself is not passed any isModalOpen prop.

    const IngredientsViewPage: React.FC = () => {
      ....
    
      return (
        <Box .... >
          <Box sx={{ .... }}>
            <SearchBarComponent />
            <AddButtonModal 
              open={true}
              handleOpenModal={handleOpenModal}
              handleCloseModal={handleCloseModal}
              // <-- no isModalOpen prop
            />
          </Box>
          ....
        </Box>
      )
    };
    
    interface AddButtonModalProps {
      isModalOpen: boolean;         // <-- 
      handleOpenModal: () => void;
      handleCloseModal: () => void;
    }
    
    const AddButtonModal: React.FC<AddButtonModalProps> = ({
      isModalOpen, // <-- undefined
      handleOpenModal,
      handleCloseModal
    }) => {
      ....
    
      return (
        <Box sx={{marginBottom: 2 }}>
          <Button .... >
            Add new
            <Modal
              open={isModalOpen} // <-- undefined
              onClose={handleCloseModal}
              aria-labelledby="modal-modal-title"
              aria-describedby="modal-modal-description"
            >
              ....
            </Modal>
          </Button>
        </Box>
      );
    };
    

    I suspect you meant to pass isModalOpen, e.g. isModalOpen={true} or isModalOpen={modalState.isModalOpen} like what was done for the ListOfIngredients component.

    <AddButtonModal 
      isModalOpen
      handleOpenModal={handleOpenModal}
      handleCloseModal={handleCloseModal}
    />
    
    <AddButtonModal 
      isModalOpen={modalState.isModalOpen}
      handleOpenModal={handleOpenModal}
      handleCloseModal={handleCloseModal}
    />
    

    I suggest also moving the Modal component outside the button so any clicks within the modal don't trigger the button's onClick handler and toggle any state, e.g. you can remove the onClick hander that stops the click event propagation on the Box component that Modal renders.

    const AddButtonModal = ({
      isModalOpen,
      handleOpenModal,
      handleCloseModal
    }: AddButtonModalProps) => {
      ....
    
      return (
        <Box sx={{marginBottom: 2 }}>
          <Button
            onClick={handleOpenModal}
            sx={{ .... }}
            startIcon={<Add />}
            id="Container-search bar & + button"
          >
            Add new
          </Button>
    
          <Modal // <-- outside button
            open={isModalOpen}
            onClose={handleCloseModal}
            aria-labelledby="modal-modal-title"
            aria-describedby="modal-modal-description"
          >
            <Box
              sx={{ .... }}
              id="main container"
              // <-- no onClick
            >
              ....
            </Box>
          </Modal>
        </Box>
      );
    };