reactjsreact-hooksreact-context

How to utilise useContext within a seperate function - ReactJS


I'm attempting to seperate my usage of useContext and the ensuing array manipulation into a callable function (ChangeContent) so that I can utilise it across other functions/components, instead of having to define the same code everywhere. ChangeContent will have sister functions like AddTab and DeleteTab in the new file.

I've attempted 2 different ways to call ChangeContent, as seen by the commented out code. Calling ChangeContent with () gets further, getting an Invalid hook call error. Calling ChangeContent with seems to do nothing at all.

I've stripped out the irrelevant parts of my code, leaving the core part of the issue. App has a component called SideBar, which has buttons that will be able to change the content in tabArray. When the code to change content is directly within SideBar, it works fine. (There will be more elements in the state array) In the future, I'm going to want to manipulate the content in other components, so I was hoping to extract the code into a single place that would be callable across my app.

In practice, these 3 functions aren't defined within the same file, I've just used codesandbox to replicate my error so it's all in one place so I could show my issue.

Any help will be appreciated.

import React, {createContext, useState, useContext} from "react";

export const TabContext = createContext()

export default function App() {

  const [tabArray, setTabArray] = useState([{TabText:'Home' ,Selected:true}])

  return (
    <div className="App">
      <TabContext.Provider value= {{tabArray, setTabArray}}>
      <SideBar/>
      {tabArray.map((v,i) => <div key={i}>{v.TabText} </div>)}
      </TabContext.Provider>
    </div>
  );
}

export function SideBar() {

  return (            
         <div>
                <button onClick={() => ChangeContent("All Commodity")}> All Commodity </button> 
                {/* <button onClick={() => <ChangeContent text={"All Commodity"}/>}> All Commodity </button>  */}
        </div>
  )
}

export function ChangeContent (text) {
{/* export function ChangeContent ({text}) { */}
  
    const { tabArray , setTabArray} = useContext(TabContext)
    setTabArray(oldValues => {
      return oldValues.map((v) => {
        if (v.Selected) {
        return {...v,TabText:text}
        }
        else {return {...v}}
      })
    })
  }

Solution

  • I'm not 100% sure what you're trying to achieve. I tried implementing something along those lines - I context wrapping values of tabs which are changed when clicked:

    https://codesandbox.io/p/sandbox/priceless-banach-mjwxx4

    //app.js
    import Sidebar from "./Sidebar";
    import "./styles.css";
    import { TabContextProvider } from "./TabContext";
    
    export default function App() {
      return (
        <TabContextProvider>
          <Sidebar />
        </TabContextProvider>
      );
    }
    
    //tabcontext.js
    import { createContext, useContext, useState } from "react";
    
    const TabContext = createContext();
    
    export const TabContextProvider = ({ children }) => {
      const [tabArray, setTabArray] = useState([
        { TabText: "Home", Selected: true },
      ]);
    
      const changeContent = (text) => {
        setTabArray((prev) =>
          prev.map((v) => (v.Selected ? { ...v, TabText: text } : { ...v }))
        );
      };
    
      return (
        <TabContext.Provider value={{ tabArray, setTabArray, changeContent }}>
          {children}
        </TabContext.Provider>
      );
    };
    
    export const useTabContext = () => {
      return useContext(TabContext);
    };
    
    import { useTabContext } from "./TabContext";
    
    export default function Sidebar() {
      const { changeContent, tabArray } = useTabContext();
    
      return (
        <div>
          {tabArray.map((tab) => (
            <button onClick={() => changeContent("All Commodit")}>
              {tab.TabText}
            </button>
          ))}
        </div>
      );
    }
    

    This approach is slightly different.

    Firstly, I'd take out the state and put it directly in the context TabContext.js - no point in declaring it in App.js - everytime the value will change when you interact with the context - the entire app will rerender (because all children components rerender when you update state of a component). I always export functions like TabContextProvider and useTabContext - it makes it nicer to use.

    Secondly, you can put changeContent function directly in the context. Seems like a nice place for it to live imo. You can simplify the logic inside the setState by using a ternary operator instead of if else.

    Finally, in the Sidebar.js you can just grab the changeContent function directly from the context and use it there. Clicking the button changes the state to the value of the text provided. I also noticed you weren't using the value from context - you hardcoded the value in:

    <button onClick={() => ChangeContent("All Commodity")}> All Commodity </button> 
    

    I changed that to use the value directly from the context, although I'm not entirely sure what you were aiming for:

    {tabArray.map((tab) => (
        <button onClick={() => changeContent("All Commodit")}>
            {tab.TabText}
        </button>
    ))}
    

    Hope this helps :)

    EDIT: I think I might've misunderstood your quesiton - you can only consume context in a component I believe - if you want to do something with it in a separate function, just pass the setters to it:

    function changeText(text,tabArray,setTabArray) {
      //perform operations
    }
    
    //some component 
    export default function SomeComponent() {
      const {tabArray,setTabArray} = useTabContext();
    
      return (
        <button onClick={() => changeText("newValue",tabArray,setTabArray)}>click</button>
      );
    }