0

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;

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks@FelixOrinda. This implementation will surely work but i am learning useContext and how to manage multiple context across the app with it. Use effect and CRUD are the next step after that. For the purpose of what i am learning atm i would really appreciate if solutions can remain within context. Thanks again
I have found context API handy and use it for all my complex state. Handling multiple states is easier with this approach
0

Short answer, use react-context-slices. It's a library that allows you to define slices of Context and fetch them through a unique hook, useSlice. You must pass the name of the slice you want to fetch and this hook will return to you the value or state of the slice and a setter/dispatch function, depending on if you defined a reducer or not for the slice.

Here is an example on how you do it with this library:

// slices.js
import getHookAndProviderFromSlices from "react-context-slices"
export const {useSlice, Provider} = getHookAndProviderFromSlices({
  count: {initialArg: 0},
  // rest of slices of Context you want to define
})
// app.js
import {useSlice} from "./slices"
const App = () => {
  const [count, useCount] = useSlice("count")
  return <>
    <button onClick={() => setCount(c => c + 1)}>+</button>{count}
  </>
}

Hope this helps anyone planning to use Context API for state management in a react app.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.