srWebDev
srWebDev

Reputation: 414

React.js component not re-rendering children when the useState hook changes state

I have a parent component, NavBar, which has two children: NavItems and Icon. When either of those children are clicked, their appearance should both change. A very standard functionality.

What I want in addition is to be able to set the initial state based on the screen width. For this I have created a custom hook that I have called useMatchMedia. Every time useMatchMedia is called, I need the state to be overridden, regardless of what state it currently has and the child components to be re-rendered with this new state.

I believe the problem lies within NavBar before the return statement. But just in case it doesn't, I have provided the code for my custom hook as well.

(Scroll to below my code to see my attempts to fix the issue).

Here is the parent:

function NavBar() {
    const matchMedia = useMatchMedia(false, "tablet"); // returns true or false if the screen width is that of a tablet or not
    const [hideNavItems, setHideNavItems] = useState(matchMedia);

    let onHandleHideNavItems = () => {
        setHideNavItems(!hideNavItems);
    };

    return (
        <Nav>
            <NavItems
                hideNavItems={hideNavItems}
                onHandleHideNavItems={onHandleHideNavItems} // onClick={() => onHandleHideNavItems(!hideNavItems)}
            />
            <Icon
                hideNavItems={hideNavItems}
                onHandleHideNavItems={onHandleHideNavItems} // onClick={() => onHandleHideNavItems(!hideNavItems)}
            />
        </Nav>
    );
}

export default NavBar;

Here is my custom hook:

function useMatchMedia(initialState, device) {
    const [passQuery, setPassQuery] = useState(initialState);
    const matchMediaRef = useRef(null);

    let widthType = null;
    let chosenDevice = device;

    switch (chosenDevice) {
        case "phone":
            chosenDevice = 425;
            widthType = "max";
            break;
        case "tablet":
            chosenDevice = 768;
            widthType = "max";
            break;
        case "smallLaptop":
            chosenDevice = 1024;
            widthType = "max";
            break;
        case "largeLaptop":
            chosenDevice = 1440;
            widthType = "max";
            break;
        default:
            chosenDevice = 1800;
            widthType = "min";
    }

    useEffect(() => {
        matchMediaRef.current = window.matchMedia(
            `(${widthType}-width: ${chosenDevice}px)`
        );
        const initialMatch = matchMediaRef.current.matches;

        if (initialMatch) {
            setPassQuery(true);
        } else {
            setPassQuery(false);
        }

        const target = (event) => {
            if (event.matches) {
                setPassQuery(true);
            } else {
                setPassQuery(false);
            }
        };

        matchMediaRef.current.addListener(target);

        return () => {
            matchMediaRef.current.removeListener(target);
        };
    }, [widthType, chosenDevice]);

    return passQuery;
}

export default useMatchMedia;

I have tried to use useEffect within my NavBar component, such as:

 useEffect(() => {
        setHideNavItems(!hideNavItems);
     }, [matchMedia, hideNavItems]);

That way I should get a re-render whenever matchMedia changes and as the initial state of hideNavItems is set to matchMedia that should update my state and therefore the children. This fails because it creates an infinite loop. I understand why there is an infinite loop but I also believe the solution is close to something like this.

I have also tried using useCallback on onHandleHideNavItems but that doesn't work.

I have tried using useRef to restrict the render to updates and not the initial render. This also does not work.

I'm a bit lost. Any help is much appreciated.

Upvotes: 2

Views: 198

Answers (1)

awran5
awran5

Reputation: 4536

Just use useEffect to reset the hideNavItems state back to matchMedia value whenever the screen width changes based on your useMatchMedia hook logic:

useEffect(() => {
    setHideNavItems(matchMedia);
}, [matchMedia]);

Edit bold-gagarin-d76bj

Upvotes: 1

Related Questions