Search code examples
reactjsnavbarreact-contextreact-state-managementuse-reducer

How manage global state using context API in React js


I am having issues managing the state of my navbar using useContext. Atm my app renders the menu items as soon as the menu toggle. I want this event to happen only onClick, also the button does not log the console.log message, it works only when I click directly on the link item ex:home.

So I have 2 questions.

How do I manage my navbar state to show how to hide the menu items without having to create a new component for it?

How do I fix my click event for it be triggered on either the menu button itself or/and menu items?

Below you will code snippets for App.js, Layout.js, ThemeContext.js, useTheme.js, useToggle.js, ToggleContext.js and the Navbar where the toggle context is used.

I would really appreciate some help here guys, I am junior and really kind of stuck here.

Thanks in advance to you all.

App.js

//import { data } from '../../SkillData';
import Header from './Header';
import Navbar from './Navbar';
import Skills from './Skills';
import Layout from './Layout';

function App () {

    return (
        <Layout startingTheme="light" startingToggle={"show"}>
        <div>
        <Navbar />
        <Header />
        <Skills />
        </div>
        </Layout>
    );
}

export default App;

Layout.js

import React, { useContext } from "react";
import { ThemeContext, ThemeProvider } from "../contexts/ThemeContext";
import { ToggleContext, ToggleProvider } from "../contexts/ToggleContext";
function Layout ({startingTheme, startingToggle, children}) { 
    return (
        <>
        <ThemeProvider startingTheme={startingTheme} >
            <ToggleProvider startingToggle={startingToggle}>
                <LayoutNoToggleProvider>
                </LayoutNoToggleProvider> 
            </ToggleProvider>
            <LayoutNoThemeProvider >{children}</LayoutNoThemeProvider>
        </ThemeProvider>        
        </>
        
    );
}

function LayoutNoToggleProvider ({children}) {
    const  toggle = useContext(ToggleContext);

    return (
        <div className={            
        toggle === false ? "navbar navbar-collapsed" : "navbar navbar-collapse show"
        }> 
        {children}     
        </div>
    )
}

function LayoutNoThemeProvider ({ children }) {
    const {theme} = useContext(ThemeContext);

    return (
        
        <div className={
            theme === "light" ? 
            "container-fluid bg-white" :
            "container-fluid bg-dark"  
        }>
        {children}
        </div>
    
    );
}
export default Layout;

ThemeContext

import React, { createContext} from "react";
import useTheme from "../hooks/useTheme";

export const ThemeContext = createContext(); 

function ThemeProvider ({children, startingTheme}) {
    const { theme, setTheme } = useTheme(startingTheme);

    return (
        <ThemeContext.Provider value={
            {theme, setTheme}
        }>    
        {children}    
        </ThemeContext.Provider>
    );

}

export { ThemeProvider };

useTheme.js

import { useState } from "react";

function useTheme (startingTheme ="light") {

    const [theme, setTheme] = useState(startingTheme);

    function validateTheme (themeValue) {
        if (themeValue === "dark") {
            setTheme("dark");
        } else {
            setTheme("light");
        }
    }    

    return {
        theme,
        setTheme: validateTheme,
    }
}

export default useTheme;

ToggleContext.js

import React, { createContext } from "react";
import useToggle from "../hooks/useToggle";

export const ToggleContext = createContext();

function ToggleProvider({ children, startingToggle }) {
  const { toggle, setToggle } = useToggle(startingToggle);

  return (
    <ToggleContext.Provider value={{ toggle, setToggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

export { ToggleProvider };

useToggle.js

import { useState } from "react";

function useToggle (startingToggle = false) {
    const [toggle, setToggle] = useState(startingToggle);

    function validateShowSidebar (showSidebarValue) {
        if (showSidebarValue === "show")  {
            setToggle("show");
        } else {
            setToggle("");
        }
    }
    return {
        toggle,
        setToggle: validateShowSidebar,
    }
}

export default useToggle;

Navbar.js

import Image from "next/image";
import styles from "../../styles/Home.module.scss"
import Logo  from "../../public/Knowledge Memo.svg"
import { useContext } from "react";
import { ThemeContext } from "../contexts/ThemeContext";
import { ToggleContext } from "../contexts/ToggleContext";
import Link from 'next/link';
import { useState } from "react";


const navbarData  = [
    {   id: "1",
        title: "home",
        ref: "#home"
    },
    {   id:"2",
        title: "Skills",
        ref: "#skills"
    },
    {   id:"3",
        title: "The List",
        ref: "#theList"
    },
    {   id: "4",
        title: "Team",
        ref: "#team"
    },
    {   id: "5",
        title: "Contact",
        ref: "#contact"
    },
];

function Navbar() {

    const theme = useContext(ThemeContext);
    const toggle  = useContext(ToggleContext);   


    return (
        <>  
            
            <nav className={
                theme === "light" ? 
                "navbar navbar-expand-lg navbar-dark fixed-top": 
                "navbar navbar-expand-lg navbar-dark bg-dark fixed-top id= mainNav"}>
                <div className="container d-flex flex justify-content-between">
                    <a className="navbar-brand h-50" href="#page-top">
                    <div className="navbar-brand"> 
                    <Image 
                    src={Logo} 
                    alt="..."                  
                    fill="#fff"
                    objectFit="contain"
                    className="h-50"                    
                    />
                    </div>
                    </a>                    
                    <button
                    onClick={ () => toggle === !toggle, console.log("clicked")}
                    className="navbar-toggler collapsed" 
                    type="button" 
                    data-bs-toggle="collapsed" 
                    data-bs-target="#navbarResponsive" 
                    aria-controls="navbarResponsive" 
                    aria-expanded="false" 
                    aria-label="Toggle navigation"
                    >
                        Menu
                    <i className="fa fa-bars ms-1 navbar-toggler" aria-hidden="true"></i>
                    </button>
                    {toggle ?
                    <div className="collapsed navbar-collapse mt-2 id=navbarResponsive">
                        <ul className="navbar-nav text-uppercase ms-auto py-4 py-lg-0">
                            {navbarData.map((link,idx) => {

                                return (
                                    <li key={link.id}>
                                        <Link  href={`/${link.ref}`} className="nav-item" data-index={idx} passHref>
                                        <a className="nav-link">
                                        {link.title}
                                        </a>
                                        </Link>
                                    </li>

                                );
                            })}
                        </ul>
                    </div>
:                     <div className="collapse navbar-collapse show mt-2 id=navbarResponsive">
<ul className="navbar-nav show text-uppercase ms-auto py-4 py-lg-0">
    {navbarData.map((link,idx) => {

        return (
            <li key={link.id}>
                <Link  href={`/${link.ref}`} className="nav-item" data-index={idx} passHref>
                <a className="nav-link">
                {link.title}
                </a>
                </Link>
            </li>

        );
    })}
</ul>
</div>}
                </div>
            </nav>
        </>
    );
}

export default Navbar;

Solution

  • You can try out this implemetation with reducers to handle for you the state change with localstorage. It is not an exact implemetation of your's but you can see the flow

    In the AppContext.jsx

    The AppContext holds the global state of the application so that it's easier working with a single context provider and dispatching actons to specific reducers to handle state change without providing many providers. The combinedReducers handle reducer methods to a given state component

    import { useReducer, createContext, useEffect } from "react";
    import userReducer from "./reducers/userReducer";
    import themeReducer from "./reducers/themeReducer";
    export const APP_NAME = "test_app";
    
    //Check the localstorage or set a default state
    const initialState = JSON.parse(localStorage.getItem(APP_NAME))
      ? JSON.parse(localStorage.getItem(APP_NAME))
      : {
          user: {
            username: "",
            email: "",
            isAdmin: false,
          },
          theme: { dark: false },
        };
    //Create your global context
    const AppContext = createContext(initialState);
    
    //Create combined reducers
    const combinedReducers = ({ user, theme }, action) => ({
      user: userReducer(user, action),
      theme: themeReducer(theme, action),
    });
    const AppState = ({ children }) => {
      //Making it to provider state
      const [state, dispatch] = useReducer(combinedReducers, initialState);
      useEffect(() => {
        localStorage.setItem(APP_NAME, JSON.stringify(state));
      }, [state]);
      return (
        <AppContext.Provider value={{ state, dispatch }}>
          {children}
        </AppContext.Provider>
      );
    };
    
    export default AppState;
    
    export { AppContext, AppState };
    

    The above implementation works like redux but you destructure the given state to a specific reducer to handle the state change

    In this I have used localstorage to keep a persistent state because with context API on page reload the state goes. Use the useEffect hook from react and add the state in the dependency array to ensure your state is in sync

    In the UserReducer.jsx

    const userReducer = (state, action) => {
      const { type, payload } = action;
      switch (type) {
        case "LOGIN":
          return { ...state, ...payload };
        case "LOGOUT":
          return {};
        default:
          return state;
      }
    };
    
    export default userReducer;
    

    In the ThemeReducer.jsx

    const themeReducer = (state, action) => {
      const { type, payload } = action;
      switch (type) {
        case "DARK":
          return { ...payload };
        default:
          return state;
      }
    };
    
    export default themeReducer;
    

    Wrapping the whole app with a single provider in the index.jsx

    import reactDom from "react-dom"
    import React from "react"
    import App from "./App"
    import "./index.css"
    import AppState from "./state/AppState"
    
    reactDom.render(
        <React.StrictMode>
            <AppState >
                <App />
            </AppState>
        </React.StrictMode>,
        document.getElementById("root")
    )
    

    Accessing the context from App.jsx

    import { useContext } from "react";
    import { AppContext } from "./state/AppState";
    const App = () => {
      const { state, dispatch } = useContext(AppContext);
      const handleLogin = () => {
        dispatch({
          type: "LOGIN",
          payload: {
            username: "Mike",
            email: "[email protected]",
            isAdmin: false,
          },
        });
      };
    
      const handleLogout = () => {
        dispatch({
          type: "LOGOUT",
          payload: {},
        });
      };
    
      return (
        <div className="main-container">
          <div className="container">
            <p>Username: {state.user.username ? state.user.username : "Unknown"}</p>
            <p>Email: {state.user.email ? state.user.email : "Unknown"}</p>
          </div>
          <button onClick={handleLogin}>Login</button>
          <button onClick={handleLogout} style={{ background: "red" }}>
            Login
          </button>
        </div>
      );
    };
    
    export default App;
    

    Here is my code LINK if you want to see the structure Github