Search code examples
reactjsreact-hooksnext.jsuse-contextreact-three-fiber

React Context is not working as expected: Unable to change the value of shared variable


I made a context to share the value of the variable "clicked" throughout my nextjs pages, it seems to give no errors but as you can see the variable's value remains FALSE even after the click event. It does not change to TRUE. This is my first time working with context, what am I doing wrong? I'm using typescript PS: After the onClick event the log's number shoots up by 3 or 4, is it being executed more than once, but how?

controlsContext.tsx

import { createContext, FC, useState } from "react";

export interface MyContext {
    clicked: boolean;
    changeClicked?: () => void;
}

const defaultState = {
  clicked: false,
}

const ControlContext = createContext<MyContext>(defaultState);


export const ControlProvider: FC = ({ children }) => {
    const [clicked, setClicked] =  useState(defaultState.clicked);
    const changeClicked = () => setClicked(!clicked);
  return (
    <ControlContext.Provider
      value={{
          clicked,
          changeClicked,
      }}
    >
      {children}
    </ControlContext.Provider>
  );
};

export default ControlContext;

Model.tsx

import ControlContext from "../contexts/controlsContext";

export default function Model (props:any) { 
    const group = useRef<THREE.Mesh>(null!)
    const {clicked, changeClicked } = useContext(ControlContext);

    const handleClick = (e: MouseEvent) => {
        //e.preventDefault();
        changeClicked();
        console.log(clicked);
    }


    useEffect(() => {
        console.log(clicked);
      }, [clicked]);
    useFrame((state, delta) => (group.current.rotation.y += 0.01));
    const model = useGLTF("/scene.gltf ");
    return (
        <>
        
         <TransformControls enabled={clicked}>
         
        <mesh 
            ref={group} 
            {...props}
            scale={clicked ? 0.5 : 0.2}
            onClick={handleClick}
        >
            <primitive object={model.scene}/>
        </mesh>
        </TransformControls>
        
        </>
    )
}

_app.tsx

import {ControlProvider} from '../contexts/controlsContext';


function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ControlProvider>
      <Component {...pageProps} 
      />
    </ControlProvider>
  )
}

export default MyApp

This is the error I get


Solution

  • Issues

    1. You are not actually invoking the changeClicked callback.
    2. React state updates are asynchronously processed, so you can't log the state being updated in the same callback scope as the enqueued update, it will only ever log the state value from the current render cycle, not what it will be in a subsequent render cycle.
    3. You've listed the changeClicked callback as optional, so Typescript will warn you if you don't use a null-check before calling changeClicked.

    Solution

    const { clicked, changeClicked } = useContext(ControlContext);
    
    ...
    
    <mesh 
      ...
      onClick={(event) => {
        changeClicked && changeClicked();
      }}
    >
      ...
    </mesh>
    
    ...
    

    Or declare the changeClicked as required in call normally. You are already providing changeClicked as part of the default context value, and you don't conditionally include in in the provider, so there's no need for it to be optional.

    export interface MyContext {
      clicked: boolean,
      changeClicked: () => void
    }
    

    ...

    const { clicked, changeClicked } = useContext(ControlContext);
    
    ...
    
    <mesh 
      ...
      onClick={(event) => {
        changeClicked();
      }}
    >
      ...
    </mesh>
    
    ...
    

    Use an useEffect hook in to log any state updates.

    const { clicked, changeClicked } = useContext(ControlContext);
    
    useEffect(() => {
      console.log(clicked);
    }, [clicked]);
    

    Update

    After working with you and your sandbox it needed a few tweaks.

    1. Wrapping the index.tsx JSX code with the ControlProvider provider component so there was a valid context value being provided to the app. The UI here had to be refactored into a React component so it could itself also consume the context value.
    2. It seems there was some issue with the HTML canvas element, or the mesh element that was preventing the Modal component from maintaining a "solid" connection with the React context. It wasn't overtly clear what the issue was here, but passing the context values directly to the Modal component as props resolved the issue with the changeClicked callback becoming undefined.