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 in
ForwardRef(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
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>
);
};