Home > Back-end >  How manage global state using context API in React js
How manage global state using context API in React js

Time:11-07

I am having issues managing the state of my navbar using useContext. Atm my app render the menu items as soon as the menu toggle. I want this event to happen only onClick, also the button do 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 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 appreaciate some help here guys, i am junior and really kind of stuck here. Thanks in advance to you all.

Leo

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;

CodePudding user response:

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

  • Related