Attempting to create a tic-tac-toe game in NextJS and trying to create a board context for my components to read. Once I introduced a reducer into the Wrapper for a more complex state, I am now getting a null error.
Errors:
TypeError: Cannot read properties of null (reading 'useContext')
Error in then the terminal:
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
I believe I only have the react version installed in this project that next installs when using create-next-app. I'm largely stuck on where to look for what's going wrong. Can I not use the useReducer hook instead of the useState hook when providing context in NextJS?
Context Declaration:
const TTTContext = createContext();
const TTTBoard = new Board(3);
const gameStateReducer = (state, action) => {
switch (action.type) {
//...reducer logic
}
}
const TTTWrapper = ({ children }) => {
const [gameState, dispatch] = useReducer(gameStateReducer, {board: TTTBoard, playerPiece:"DEFAULT"});
const gameContextValue = () => {gameState, dispatch};
return (
<TTTContext.Provider value={gameContextValue}>
{children}
</TTTContext.Provider>
);
}
const useTTTContext = () => {
return useContext(TTTContext)
};
module.exports = { TTTWrapper, useTTTContext }
The context is applied in _app.js here:
<TTTWrapper>
<Component {...pageProps} />
</TTTWrapper>
And the game state is retrieved in the actual Grid component here:
import { useTTTContext } from "../contexts/TTTContext";
const {gameState, dispatch} = useTTTContext();
Solved: const {gameState, dispatch} = useTTTContext(); was outside of its component body.
From what I can see of the code you've provided, the issue seems to be in the last snippet:
import { useTTTContext } from "../contexts/TTTContext";
const {gameState, dispatch} = useTTTContext();
The useTTTContext doesn't appear to be used/called inside any React function component body nor any custom React hook. It should be moved into either a React function or custom hook body.
Related
I am using the useIsDirty hook in two components, CustomCodeEditor and EditorFooter, to track whether the code in the Editor has been modified. The hook returns an isDirty state and a setIsDirty function to update it. When I call setIsDirty(true) in the CustomCodeEditor component, the state is updated, but when I call setIsDirty(false) in the EditorFooter component, it doesn't seem to update the isDirty state. I believe this is because the EditorFooter component does not have access to the updated state. Anyone, please help me with this.
useIsDirty:
import { useEffect, useState } from "react"
const useIsDirty = () => {
const [isDirty, setIsDirty] = useState(false)
useEffect(() => {
const handleBeforeUnload = (event) => {
if (isDirty) {
event.preventDefault()
event.returnValue = ""
alert("You have unsaved changes, are you sure you want to leave?")
}
}
console.log("Diryt:", isDirty)
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [isDirty])
return { isDirty, setIsDirty }
}
export default useIsDirty
CustomCodeEditor
import Editor from "#monaco-editor/react"
import useIsDirty from "../../hooks/useIsDirty"
const CustomCodeEditor = () => {
const { isDirty, setIsDirty } = useIsDirty()
console.log("isDirty:", isDirty)
return (
<div className="bg-[#1e1e1e] h-full">
<Editor
onChange={(value) => {
updateCode(value || "")
setIsDirty(true) // updating state
}}
/>
</div>
)
}
export default CustomCodeEditor
EditorFooter
import Button from "../reusable/Button"
const EditorFooter = () => {
const { setIsDirty } = useIsDirty()
const handleSave = async () => {
setIsDirty(false)
}
return (
<div>
<Button
onClick={handleSave}
>
Save
</Button>
<Button
onClick={handleSave}
>
Submit
</Button>
</div>
)
}
export default EditorFooter
Hooks are not singleton instances.. when you use useIsDirty somewhere.. it always create new instance, with unrelated states to other ones. If you want to share this state you need to use Context
const IsDirtyContext = createContext(undefined);
const IsDirtyProvider = ({ children }): ReactElement => {
const [isDirty, setIsDirty] = useState(false)
return <IsDirtyContext.Provider value={{isDirty, setIsDirty}}>{children}</IsDirtyContext.Provider>;
};
and then you should wrap your commponent tree where you wanna access it with IsDirtyProvider
after that, you can even create your custom hook that will just return that context:
const useIsDirty = () => {
return useContext(IsDirtyContext)
}
Looking at your question, it looks like you are trying to use the same state in both components. However, the state doesn't work like that. A new instance is created whenever you make a call to useIsDirty from a different component.
If you want to use the state value across two components. You can do that using one of the following ways.
1 - Use a parent and child hierarchy.
Steps
Create a parent component and wrap the two components inside the parent component.
Manage the state in the parent component and pass it using props to the child component.
Create a function in child components that will execute a function from the parent component. The parent component function will hold the code to update the state based on whatever value you receive from the child component.
Now you should be able to share your state between both components.
2 - Use the context api.
If you are not familiar with what context api is, below is a brief explanation.
Context api helps you share data between components, without the need of passing them as a prop to each and every component.
You can use createContext and useContext hooks from context api.
createContext is used to create a new context provider.
useContext hook is used to manage the state globally.
You can get the value from context using this function.
Whenever the state is updated the value will be reflected globally.
Note - Any component that needs to use the value inside the useContext should be wrapped inside the useContext provider component.
Steps to create a context provider.
To create a context you just need to use the react hook createContext
Create a context using below code
const isDirtyContext = createContext();
Wrap your components in the context provider
import {IsDirtyContext} from './path/filename'
<IsDirtyContext.Provider value={[isDirty, setIsDirty]}>{children}</IsDirtyContext.Provider>
If your context is in a separate file, then you can import it into any child component using the import statement.
import {IsDirtyContext} from './path/filename'
Use the context
const [isDirty] = useContext(IsDirtyContext);
Now the isDirty state value is available globally in all components.
Hope this information helps you. Please upvote if this helps you understand and solve the problem.
I have a scenario where I am forced to call a trigger method to show a modal from two different places, one using a hotkey combination and another by clicking on a toolbar button. In order to do so I have the following code, where I call the triggerCustomLinkModal to set the state but then I am hit with the Invalid Hook call error.
import { useState, useCallback, useEffect } from "react"
import { Dialog } from "#blueprintjs/core"
const useLocalState = () => {
const [isShown, setIsShown] = useState(false)
const setState = useCallback((state) => {
setIsShown(state)
})
const getState = useCallback(() => {
return isShown
})
return {
setState,
getState
}
}
export const CustomLinkModalUI = () => {
const { getState } = useLocalState()
return (
<>
<Dialog isOpen={getState()} />
</>
)
}
export const triggerCustomLinkModal = () => {
const { setState } = useLocalState()
setState()
}
Expanding from Chris answer in the comments ( You can't use hooks outside React components. -> so you can't call useLocalState() inside triggerCustomLinkModal since triggerCustomLinkModal is not a React component ):
You don't really need the useCallback hook or even the functions itself. Aaccording to react docs :
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
This also means that using useCallback hook to set a state it doesn't really make sense (because useCallback role is just to return a memoized callback)
What you basically need is a state set up in the closest parrent component and pass the setIsShown as a prop as well as the isShown function.
Your current implementation, even if it weren't for the error, it wouldn't refer to the same state since on each useLocalState() you are initializing a fresh new state (so you are not pointing to the same state in CustomLinkModalUI and triggerCustomLinkModal)
i am trying to build a custom component video player with react and mobx, and i need to drill a refrence from a main component to a child Component but i'm getting an error message when i use the forwardRef function on a component that is an observer.
the error message is "baseComponent is not a function"
Here's the code:
// code for main component
const videoPlayer = () => {
const controlsRef = useRef<any>(null);
return (<div>
// video player screen code //
<VideoPlayerButtonCode ref={controlsRef} />
<div>)
}
// the code for the players component
interface IProps{
controlsRef: any;
}
const VideoPlayerButtonCode: React.FC<IProps> = fowardRef({props from iprops}, controlsRef ) => {
return (<div>
<Button ref={controlsRef}>Button i want to get a ref for from main</Button>
<div>)
}
export default observer(VideoPlayerButtonCode)
thats a vague abstraction of the code but the same implementation.
is there any help for mobx supports for ref or is there a way i can store the refrence in a mobx store?
What version of mobx-react are you using? It should work fine with latest 7.0.0 version, but it seems to fail if you are using mobx-react-lite#3.0.0.
I've made codesandbox with all working variants as for now: https://codesandbox.io/s/httpsstackoverflowcomquestions64227496-75xdz?file=/src/App.js
For example same version as your works fine:
const ComponentWithForwardRef = React.forwardRef((props, ref) => {
return <div ref={ref}>My Observer Component</div>;
});
const ObserverComponentWithForwardRef = observer(ComponentWithForwardRef);
There is also a forwardRef option for observer HOC, but it only currently works with mobx-react-lite, and does not work with regular mobx-react package due to this bug https://github.com/mobxjs/mobx-react/issues/868
You can use it like that:
const MyObserverComponent = observer(
(props, ref) => {
return <div ref={ref}>My Observer Component</div>;
},
{ forwardRef: true }
);
If everything fails you can just use custom prop for your ref like so:
<MyObserverComponentCustomRef innerRef={myRef} />
// ...
const MyObserverComponentCustomRef = observer((props) => {
return <div ref={props.innerRef}>My Observer Component Inner Ref</div>;
});
I want to have a global object that is available to my app where I can retrieve the value anywhere and also set a new value anywhere. Currently I have only used Context for values that are related to state i.e something needs to render again when the value changes. For example:
import React from 'react';
const TokenContext = React.createContext({
token: null,
setToken: () => {}
});
export default TokenContext;
import React, { useState } from 'react';
import './App.css';
import Title from './Title';
import TokenContext from './TokenContext';
function App() {
const [token, setToken] = useState(null);
return(
<TokenContext.Provider value={{ token, setToken }}>
<Title />
</TokenContext.Provider>
);
}
export default App;
How would I approach this if I just want to store a JS object in context (not a state) and also change the value anywhere?
The global context concept in React world was born to resolve problem with passing down props via multiple component layer. And when working with React, we want to re-render whenever "data source" changes. One way data binding in React makes this flow easier to code, debug and maintain as well.
So what is your specific purpose of store a global object and for nothing happen when that object got changes? If nothing re-render whenever it changes, so what is the main use of it?
Prevent re-render in React has multiple ways like useEffect or old shouldComponentUpdate method. I think they can help if your main idea is just prevent re-render in some very specific cases.
Use it as state management libraries like Redux.
You have a global object (store) and you query the value through context, but you also need to add forceUpdate() because mutating the object won't trigger a render as its not part of React API:
const globalObject = { counter: 0 };
const Context = React.createContext(globalObject);
const Consumer = () => {
const [, render] = useReducer(p => !p, false);
const store = useContext(Context);
const onClick = () => {
store.counter = store.counter + 1;
render();
};
return (
<>
<button onClick={onClick}>Render</button>
<div>{globalObject.counter}</div>
</>
);
};
const App = () => {
return (
<Context.Provider value={globalObject}>
<Consumer />
</Context.Provider>
);
};
TL;DR: My GOAL is to separate the API functions, and import them when I need them. And then call them under a componentDidMount scenario. Also, I've been told that async and await shall be used with, since: getCurrentPosition is an asynchronous function.
All the hints that you need to solve your problem are in the error code
Hooks or custom hooks are meant to be used within functional components
A custom hook is a hook that can be called like a function. It however must be prefixed with use to let react know that it is a custom hook
According to the above condition, your Weather component is a class component which either you need to convert to Functional component or avoid using geolocation as a custom hook
Secondly, since geoLocation is meant to be a custom hook you must call it useGetLocation
import React from 'react';
import { useGetLocation } from './getlocation';
const Weather = (props) => {
const geoLocation = useGetLocation();
useEffect(() => {
document.title = "Weather";
}, []);
return(
<>
<h1>Weather</h1>
<h2>{React.version}</h2>
</>
);
}
export default Weather;
import {useState, useEffect} from 'react';
export const useGetLocation = () => {
const [position, setPosition] = useState({});
const [error, setError] = useState(null);
const successHandler = ({coords}) => {
setPosition({
latitude: coords.latitude,
longitude: coords.longitude
});
};
const errorHandler = (error) => { setError(error.message); };
useEffect(() => {
if (!navigator.geolocation) {
setError("Geolocation might not be supported.");
return;
}
navigator.geolocation.getCurrentPosition(
successHandler,
errorHandler);
return () => {}
}, []);
return [position, error];
};
Working demo
Firstly, you have to call the hook inside of a React functional component, and not a class.
The docs:
Hooks ... let you use state and other React features without writing a class.
and
Only Call Hooks from React Functions
Secondly, change getGeolocation to useGeolocation
The docs:
A custom Hook is a JavaScript function whose name starts with ”use” ...
If a function doesn't start with "use", React won't treat it as a hook and won't allow you to call hooks inside it
You can't invoke a hook inside a class. Hooks can only be called inside stateless components. If you want to have access to that information you should either transform it in a HOC or transform the class of the componentDidMount in a function.
https://reactjs.org/docs/hooks-faq.html#what-can-i-do-with-hooks-that-i-couldnt-with-classes