Hardel
Hardel

Reputation: 33

React - Access the state of a parent from the children, without nested function

Hello,
I am coming to you today for the first time because I have not found a solution to my problem.
I have been using react for a few weeks, Don't be too cruel about the quality of my code 😁.

Problem :
I am looking to access the state of a parent from their children.So I want to be able to access the setHeight function and the height variable for example from a child component.

Please note :
However, to keep some flexibility, I don't want to have any Components inside our. I looked at redux to be able to do this, but the problem is that the data is global so creating multiple dropdowns would not be possible. (Unless I didn't understand too much, redux is quite complex)

Diagram :
I have created a diagram to explain it a little better.,
I'd like the children of DropdownMenu to be able to access the state of the latter, Also, the different Dropdowns must have their own state independently.
So ideally I want to keep the same structure as find very flexible, and the possibility to create several dropdown.

enter image description here


Code :
I Share my four components :


export default function Navbar () {
    return (
        <nav className={styles.navbar}>
            <ul className={styles.navbarNav}>
                <NavItem icon={<NotificationsIcon />} />
                <NavItem icon={<AccessTimeFilledIcon />} />
                <NavItem icon={<FileOpenIcon />}>
                    <DropdownMenu>
                        <DropdownSubMenu menuName="Home">
                            <DropdownItem>My Profile</DropdownItem>
                            <DropdownItem leftIcon={<AccessTimeFilledIcon />} rightIcon={<ChevronRightIcon />} goToMenu="pages">Pages</DropdownItem>
                            <DropdownItem>IDK</DropdownItem>
                            <DropdownItem>Test</DropdownItem>
                        </DropdownSubMenu>
                        <DropdownSubMenu menuName="pages">
                            <DropdownItem>Pages</DropdownItem>
                            <DropdownItem leftIcon={<AccessTimeFilledIcon />} rightIcon={<ChevronRightIcon />} goToMenu="home">Home</DropdownItem>
                        </DropdownSubMenu>
                    </DropdownMenu>

                    <DropdownMenu>
                        <DropdownSubMenu menuName="config">
                            <DropdownItem>Foo</DropdownItem>
                            <DropdownItem leftIcon={<AccessTimeFilledIcon />} rightIcon={<ChevronRightIcon />} goToMenu="theme">Configuration</DropdownItem>
                            <DropdownItem>Bar</DropdownItem>
                            <DropdownItem>Baz</DropdownItem>
                        </DropdownSubMenu>
                        <DropdownSubMenu menuName="theme">
                            <DropdownItem>Hi StackOverflow</DropdownItem>
                            <DropdownItem leftIcon={<AccessTimeFilledIcon />} rightIcon={<ChevronRightIcon />} goToMenu="config">Theme</DropdownItem>
                        </DropdownSubMenu>
                    </DropdownMenu>
                </NavItem>
            </ul>
        </nav>
    );
};

type Props = {
    children?: React.ReactNode | React.ReactNode[];
    leftIcon?: React.ReactNode | JSX.Element | Array<React.ReactNode | JSX.Element>;
    rightIcon?: React.ReactNode | JSX.Element | Array<React.ReactNode | JSX.Element>;
    goToMenu?: string;
    goBack?: boolean;
    OnClick?: () => void;
};

export default function DropdownItem({ children, leftIcon, rightIcon, goToMenu, goBack, OnClick }: Props) {
    const handleClick = OnClick === undefined ? () => { } : OnClick;

    return (
        <a className={styles.menuItem} onClick={() => {
            goToMenu && setActiveMenu(goToMenu);
            setDirection(goBack ? 'menu-right' : 'menu-left');
            handleClick();
        }}>
            <span className={styles.iconButton}>{leftIcon}</span>
            {children}
            <span className={styles.iconRight}>{rightIcon}</span>
        </a>
    );
}

type Props = {
    menuName: string;
    children: React.ReactNode | React.ReactNode[];
}

enum Direction {
    LEFT = 'menu-left',
    RIGHT = 'menu-right'
}

export default function DropdownSubMenu (props: Props) {
    const [direction, setDirection] = useState<Direction>(Direction.LEFT);
    
    const calcHeight = (element: HTMLElement) => {
        if (element) setMenuHeight(element.offsetHeight);
    };

    return (
        <CSSTransition in={activeMenu === props.menuName} unmountOnExit timeout={500} classNames={direction} onEnter={calcHeight}>
            <div className={styles.menu}>
                {props.children}
            </div>
        </CSSTransition>
    );
}

type Props = {
    children: React.ReactNode | React.ReactNode[];
}

export default function DropdownMenu (props: Props) {
    const [activeMenu, setActiveMenu] = useState<string>('home');
    const [menuHeight, setMenuHeight] = useState<number | null>(null);
    const dropdownRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        const child = dropdownRef.current?.firstChild as HTMLElement;
        const height = getHeight(child);

        if (height)
            setMenuHeight(height);
    }, []);

    return (
        <div className={styles.dropdown} style={{ height: `calc(${menuHeight}px + 2rem)` }} ref={dropdownRef}>
            {props.children}
        </div>
    );
}

Conclusion :
More concretely I don't know what to put instead :

In DropdownSubMenu to set the menu height (setMenuHeight), and gets the active menu (activeMenu).
In DropdownItem, set the active menu, (setActiveMenu) and set the direction of the CSS animation (setDirection).

Source :
My code is adapted from these sources, But I want to make this code more professional, flexible and polymorphic : https://github.com/fireship-io/229-multi-level-dropdown

I've been tried :
I tried to look at Redux, but I understood that it was only state global. So it doesn't allow to define a different context for each component.

I tried to look at React 18, without success. I have searched the StackOverflow posts, I have searched the state retrieval from the parents.

The use of components inside a component solves in effect the problem but we lose all the flexibility.

Upvotes: 0

Views: 2663

Answers (2)

Almaju
Almaju

Reputation: 1383

There are multiple ways to access a parent state from its children.

Pass the state as props

The preferred way is to pass the state and/or the change function to the children.

Example :

const App = () => {
    const [open, setOpen] = React.useState(false);

    const handleOpen = () => setOpen(true);
    const handleClose = () => setOpen(false);

    return (
        <div>
            <button onClick={handleOpen}>Open modal</button>
            <Modal onClose={handleClose} open={open} />
        </div>
    );
};

const Modal = ({ open, onClose }) => (
    <div className={open ? "open" : "close"}>
        <h1>Modal</h1>
        <button onClick={onClose}>Close</button>
    </div>
);

ReactDOM.render(<App />, document.querySelector("#app"));

Demo: https://jsfiddle.net/47s28ge5/1/

Use React Context

The first method becomes complicated when the children are deeply nested and you don't want to carry the state along the component tree.

You can then share a state across multiple children by using context.

const AppContext = React.createContext(undefined);

const App = () => {
    const [open, setOpen] = React.useState(false);

    const handleOpen = () => setOpen(true);
    const handleClose = () => setOpen(false);

    return (
        <AppContext.Provider value={{ open, onClose: handleClose }}>
            <div>
                <button onClick={handleOpen}>Open modal</button>
                <Modal />
            </div>
        </AppContext.Provider>
    );
};

const Modal = () => {
    const { open, onClose } = React.useContext(AppContext);

    return (
        <div className={open ? "open" : "close"}>
            <h1>Modal</h1>
            <button onClick={onClose}>Close</button>
        </div>
    );
};

ReactDOM.render(<App />, document.querySelector("#app"));

Demo: https://jsfiddle.net/dho0tmc2/3/

Using a reducer

If your code gets even more complicated, you might consider using a store to share a global state across your components.

You can take a look at popular options such as:

Upvotes: 3

programism
programism

Reputation: 23

I can say welcome to react in this moment and i glad for you

OK, i could understand what is your problem. but there isn't problem and this bug cause from your low experience.

As i understand you want to click on a dropdown and open it. and here we have nested dropdown.

I think it's your answer: You should declare a state on each dropdown and don't declare state in parent.

Upvotes: 0

Related Questions