I recently started working with React, and I'm trying to understand why my context.js is giving me so much trouble. Admittedly I'm not great with JavaScript to start, so I'd truly appreciate any insight.
Thank you, code and the error that it generates:
import React, { useState, useContext } from 'react';
const AppContext = React.createContext(undefined, undefined);
const AppProvider = ({ children }) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const openSidebar = () => {
setIsSidebarOpen(true);
};
const closeSidebar = () => {
setIsSidebarOpen(false);
};
const toggle = () => {
if (isSidebarOpen) {
closeSidebar();
} else {
openSidebar();
}
};
return (
<AppContext.Provider
value={{
isSidebarOpen,
openSidebar,
closeSidebar,
toggle
}}
>
{children}
</AppContext.Provider>
);
};
export const useGlobalContext = () => {
return useContext(AppContext);
};
export { AppContext, AppProvider };
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
Thank you again for taking the time to look!
EDIT: Sidebar App Added for context (double entendre!)
import React from 'react';
import logo from './logo.svg'
import {links} from './data'
import {FaTimes} from 'react-icons/fa'
import { useGlobalContext } from "./context";
const Sidebar = () => {
const { toggle, isSidebarOpen } = useGlobalContext();
return (
<aside className={`${isSidebarOpen ? 'sidebar show-sidebar' : 'sidebar'}`}>
<div className='sidebar-header'>
<img src={logo} className='logo' alt='NavTask Management'/>
<button className='close-btn' onClick={toggle}>
<FaTimes />
</button>
</div>
<ul className='links'>
{links.map((link) => {
const { id, url, text, icon } = link;
return (
<li key={id}>
<a href={url}>
{icon}
{text}
</a>
</li>
);
})}
</ul>
</aside>
);
};
export default Sidebar;
Related
I created a custom hook, Custom.js:
import React, {useState, useEffect} from 'react';
import Clarifai from 'clarifai';
const app = new Clarifai.App({
apiKey: 'XXXXXXXXXXXXXX'
})
const Custom = () => {
const [input, setInput] = useState('');
const [imgUrl, setImgUrl] = useState('');
function onInputChange (text) {
setInput(text);
}
useEffect(()=>{
setImgUrl(input)
}, [input])
function onSubmit () {
console.log('submitted');
console.log(imgUrl)
app.models.predict(Clarifai.COLOR_MODEL, "https://www.takemefishing.org/getmedia/bde1c54e-3a5f-4aa3-af1f-f2b99cd6f38d/best-fishing-times-facebook.jpg?width=1200&height=630&ext=.jpg").then(
function(response) {
console.log(response);
},
function(err) {
// there was an error
}
);
}
return {input, imgUrl, onInputChange, onSubmit}
}
export default Custom;
I imported this custom hook into 2 of my other components, FaceRecognition.js and InputForm.js.
FaceRecognition.js:
import React from 'react';
import Custom from '../Custom';
const FaceRecognition = () => {
const { imgUrl } = Custom();
function yes (){
return console.log(imgUrl)
}
yes()
return (
<div>
<h1 className='white'>The url is {ImgUrl} </h1>
<img width={'50%'} alt=''src={imgUrl}/>
</div>
);
}
export default FaceRecognition;
ImportForm.js:
import React, {useState} from 'react';
import './InputForm.css'
import Custom from '../Custom';
const InputForm = () => {
const { onInputChange, onSubmit } = Custom();
return (
<>
<p className='txt f3'>Enter image link address</p>
<div className='center flex w-70'>
<input type='text' className='w-80 pa1' onChange={(e)=>onInputChange(e.target.value)}/>
<button className='w-20 pa1 pointer' onClick={onSubmit}>Detect</button>
</div>
</>
);
}
export default InputForm;
The functions onSubmit and onImputChange work as expected for InputForm.js and the value of imgUrl logs on the console when function onSubmit runs, as expected. But the imgUrl state which is a string fails to show up between the h1 tags <h1 className='white'>The url is {imgUrl} boy</h1> from my FaceRecognition.js snippet above, and it also doesn't work as the src of the image <img width={'50%'} alt=''src={imgUrl}/> below the h1 tag. This is my problem.
Issue
React hooks don't magically share state. You've two separate instances of this Custom function, each with their own useState hook. I say "function" because you've also mis-named your hook. All React hooks should be named with a "use-" prefix so React can identify it and apply the Rules of Hooks against it.
Solution
If you want separate instances of your useCustom hook to share state then the state needs to be lifted to a common component to be shared. For this you should use a React Context.
Example:
import React, { createContext, useContext, useState, useEffect } from 'react';
import Clarifai from 'clarifai';
const app = new Clarifai.App({
apiKey: 'XXXXXXXXX'
});
const CustomContext = createContext({
input: '',
imgUrl: '',
onInputChange: () => {},
onSubmit: () => {}
});
const useCustom = () => useContext(CustomContext);
const CustomProvider = ({ children }) => {
const [input, setInput] = useState('');
const [imgUrl, setImgUrl] = useState('');
function onInputChange (text) {
setInput(text);
}
useEffect(()=>{
setImgUrl(input);
}, [input]);
function onSubmit () {
console.log('submitted');
console.log(imgUrl);
app.models.predict(
Clarifai.COLOR_MODEL,
"https://www.takemefishing.org/getmedia/bde1c54e-3a5f-4aa3-af1f-f2b99cd6f38d/best-fishing-times-facebook.jpg?width=1200&height=630&ext=.jpg"
).then(
function(response) {
console.log(response);
},
function(err) {
// there was an error
}
);
}
return (
<CustomContext.Provider value={{ input, imgUrl, onInputChange, onSubmit }}>
{children}
</CustomContext.Provider>
);
}
export {
CustomContext,
useCustom
};
export default CustomProvider;
Usage:
Wrap your app with your CustomProvider component.
import CustomProvider from '../path/to/CustomProvider';
...
return (
<CustomProvider>
<App />
</CustomProvider>
);
Import and use the useCustom hook in consumers.
import React from 'react';
import { useCustom } from '../path/to/CustomProvider';
const FaceRecognition = () => {
const { imgUrl } = useCustom();
useEffect(() => {
console.log(imgUrl);
});
return (
<div>
<h1 className='white'>The url is {ImgUrl}</h1>
<img width={'50%'} alt='' src={imgUrl}/>
</div>
);
}
export default FaceRecognition;
...
import React, {useState} from 'react';
import './InputForm.css'
import { useCustom } from '../path/to/CustomProvider';
const InputForm = () => {
const { onInputChange, onSubmit } = useCustom();
return (
<>
<p className='txt f3'>Enter image link address</p>
<div className='center flex w-70'>
<input
type='text'
className='w-80 pa1'
onChange={(e) => onInputChange(e.target.value)}
/>
<button
className='w-20 pa1 pointer'
onClick={onSubmit}
>
Detect
</button>
</div>
</>
);
}
export default InputForm;
try to put your return statement inside the .then of the predict
I have a hook, and 2 components. Component App.js has a function that changes the state in hook, but the value is not updated in Component New.js, why? I think I've missed something but can't figure it out.
App.js
export const useToggle = () => {
const [onOff, setOnOff] = useState(false);
return [onOff, () => setOnOff((prev) => !prev)];
};
export default function App() {
const [onOff, setOnOff] = useToggle();
return (
<div className="App">
<h1>{onOff.toString()}</h1>
<button onClick={setOnOff}>toggle</button>
</div>
);
}
New.js
import { useToggle } from "./App.js";
export default function New() {
const [onOff] = useToggle();
return (
<div className="App">
<hr />
<h1>NEW:</h1>
<pre>{onOff.toString()}</pre>
</div>
);
}
https://codesandbox.io/s/musing-fire-rjude?file=/src/App.js
Each useToggle hook is its own entity with its own state. The useToggle that you are toggling in App isn't the same useToggle that is rendered/used in New.
This means they are toggled independently of any other hooks and state. They don't share "state".
If you are wanting to create a useToggle hook that does have shared state then I would suggest implementing it via a React context and the useContext hook so each useToggle hook can toggle the same shared state held in the context.
Update
Global useToggle hook.
togglecontext.js
import { createContext, useContext, useState } from 'react';
export const ToggleContext = createContext([false, () => {}]);
const ToggleProvider = ({ children }) => {
const [onOff, setOnOff] = useState(false);
const toggle = () => setOnOff(t => !t);
return (
<ToggleContext.Provider value={[onOff, toggle]}>
{children}
</ToggleContext.Provider>
);
}
export const useToggle = () => useContext(ToggleContext);
export default ToggleProvider;
index - provide the context
...
import ToggleProvider from "./toggle.context";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<ToggleProvider>
<App />
<New />
</ToggleProvider>
</StrictMode>,
rootElement
);
App
import "./styles.css";
import { useToggle } from "./toggle.context";
export default function App() {
const [onOff, setOnOff] = useToggle();
return (
<div className="App">
<h1>{onOff.toString()}</h1>
<button onClick={setOnOff}>toggle</button>
</div>
);
}
New
import { useToggle } from "./toggle.context";
export default function New() {
const [onOff] = useToggle();
return (
<div className="App">
<hr />
<h1>NEW:</h1>
<pre>{onOff.toString()}</pre>
</div>
);
}
Note that the only thing that changed in the App and New components was the import, where the useToggle hook is defined.
I started to learn React, I'm trying to retrieve data from api, the data is an object with the fields of base, date & rates, without any problem I can print and logout base & date but rates which is an object not.
console.log gives undefined, when trying to iterate is obviously that the object does not exist but in DevTools i can see normal data
Thank you for your help and greetings
Context:
export const ExchangeProvider = props => {
const [lastestExchanges, setLastestExchanges] = useState({})
const fetchLastestExchange = async () => {
try {
await fetch(`https://api.exchangeratesapi.io/latest`).then(data => data.json()).then(data => setLastestExchanges(data))
} catch (err) {
console.log(err)
}
}
useEffect(() => {
fetchLastestExchange()
}, [])
return (
<ExchangeContext.Provider value={[lastestExchanges, setLastestExchanges]}>
{props.children}
</ExchangeContext.Provider>
)
}
Usage:
import React, {useState, useContext} from "react";
import {ExchangeContext} from "../ExchangeContext";
function HomeView() {
const [lastestExchange, setLastestExchange] = useContext(ExchangeContext)
console.log(lastestExchange)
return (
<div className="container">
<p>{lastestExchange.base}</p>
<p>{lastestExchange.date}</p>
{/*<p>{lastestExchange.rates['PLN']}</p>*/}
<ul>
{/*{Object.keys(lastestExchange.rates).map(key => <li>{lastestExchange.rates[key]}</li>)}*/}
</ul>
</div>
)
}
export default HomeView
Provider usage:
import React from 'react';
import HomeView from "./Views/HomeView";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
import {ExchangeProvider} from "./ExchangeContext";
function App() {
return (
<ExchangeProvider>
<div className="App container w-full flex h-full">
<Router>
<Switch>
<Route path="/">
<HomeView/>
</Route>
</Switch>
</Router>
</div>
</ExchangeProvider>
);
}
export default App;
You can use react context simpler like this :
// src/ThemeContext.js
import React from 'react';
const ThemeContext = React.createContext(null);
export default ThemeContext;
// src/ComponentA.js
import React from 'react';
import ThemeContext from './ThemeContext';
const A = () => (
<ThemeContext.Provider value="green">
<D />
</ThemeContext.Provider>
);
// src/ComponentD.js
import React from 'react';
import ThemeContext from './ThemeContext';
const D = () => (
<ThemeContext.Consumer>
{value => (
<p style={{ color: value }}>
Hello World
</p>
)}
</ThemeContext.Consumer>
);
I would like to use useEffect()to instead of componentWillMount(), but I found the hook can not use in class components, so I change the code as Function component, but it will get more error for the whole component, all code with this.xxx are getting an error, how could I edit below code to make it work? it was not easy for the react beginner. Please help me.
Below code is working fine with componentWillMount().
import React, { Component, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import logo from '../../assets/images/logo.png';
import './index.less';
import { Menu } from 'antd';
import menuList from '../../config/menuConfig';
const { SubMenu } = Menu;
class LeftNav extends Component {
getMenuNodes = menuList => {
const path = this.props.location.pathname;
return menuList.reduce((pre, item) => {
if (!item.children) {
pre.push(
<Menu.Item key={item.key} icon={item.icon}>
<Link to={item.key}>{item.title}</Link>
</Menu.Item>,
);
} else {
const cItem = item.children.find(cItem => cItem.key === path);
if (cItem) {
this.openKey = item.key;
}
pre.push(
<SubMenu key={item.key} icon={item.icon} title={item.title}>
{this.getMenuNodes(item.children)}
</SubMenu>,
);
}
return pre;
}, []);
};
componentWillMount() {
this.MenuNodes = this.getMenuNodes(menuList);
}
render () {
// useEffect(() => {
//this.MenuNodes = this.getMenuNodes(menuList);
// }, []);
const path = this.props.location.pathname;
console.log(path);
const openKey = this.openKey;
return (
<div className="left-nav">
<Link to="./" className="left-nav-header">
<img src={logo} alt="" />
<h1>Backend System</h1>
</Link>
<Menu
selectedKeys={[path]}
defaultOpenKeys={[openKey]}
mode="inline"
theme="dark"
>
{this.MenuNodes}
</Menu>
</div>
);
}
}
export default withRouter(LeftNav);
componentDidMount() is the lifecycle method you want to use, it can be compared to useEffect you use in functional components. componentWillMount() is deprecated and you should not use it.
I took a stab at refactoring your class component into a functional component using hooks. In the end I found that instead of the useEffect hook, it was better to use the useMemo hook. useMemo is used to memoize expensive calculations, in this case, the calculation of menuNodes and openKey. useMemo will run anytime it's dependencies change, in this case, [menuList, path].
You will also notice that instead of the withRouter Higher-Order-Component, I replaced it with a call to the useLocation hook from 'react-router-dom'.
Let me know if this works for you, and if you have any questions.
import React, { useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import logo from '../../assets/images/logo.png';
import './index.less';
import { Menu } from 'antd';
import menuList from '../../config/menuConfig';
const LeftNav = (props) => {
const location = useLocation();
const path = location.pathname;
const { MenuNodes, openKey } = useMemo(() => {
let openKey;
const getMenuNodes = (menuList) => {
return menuList.reduce((pre, item) => {
if (!item.children) {
pre.push(
<Menu.Item key={item.key} icon={item.icon}>
<Link to={item.key}>{item.title}</Link>
</Menu.Item>
);
} else {
const cItem = item.children.find((cItem) => cItem.key === path);
if (cItem) {
openKey = item.key;
}
pre.push(
<Menu.SubMenu key={item.key} icon={item.icon} title={item.title}>
{getMenuNodes(item.children)}
</Menu.SubMenu>
);
}
return pre;
}, []);
};
const nodes = getMenuNodes(menuList);
return { MenuNodes: nodes, openKey: openKey };
}, [path]);
return (
<div className="left-nav">
<Link to="./" className="left-nav-header">
<img src={logo} alt="" />
<h1>Backend System</h1>
</Link>
<Menu selectedKeys={[path]} defaultOpenKeys={[openKey]} mode="inline" theme="dark">
{MenuNodes}
</Menu>
</div>
);
};
export default LeftNav;
edit
Fixed recursive call in getMenuNodes
I need help with this issue, my app component as in the image below. I want to store track object inselectedTrack in the state using useState when I click on the view details button. Then use it to display track details in instead of making another fetch from API to get tack details, but when I use useContext inside give me this error TypeError: Cannot read property 'selectedTrack' of undefined.
React Components
import React from 'react';
import Header from './Header';
import Search from '../tracks/Search';
import Tracks from '../tracks/Tracks';
import Footer from './Footer';
import TrackContextProvider from '../../contexts/TrackContext';
const Main = () => {
return (
<div>
<TrackContextProvider>
<Header />
<Search />
<Tracks />
<Footer />
</TrackContextProvider>
</div>
);
};
export default Main;
TrackContext.js
import React, { createContext, useState, useEffect } from 'react';
export const TrackContext = createContext();
const TrackContextProvider = props => {
const [tracks, setTracks] = useState([]);
const [selectedTrack, setSelectedTrack] = useState([{}]);
const API_KEY = process.env.REACT_APP_MUSICXMATCH_KEY;
useEffect(() => {
fetch(
`https://cors-anywhere.herokuapp.com/https://api.musixmatch.com/ws/1.1/chart.tracks.get?chart_name=top&page=1&page_size=10&country=fr&f_has_lyrics=1&apikey=${API_KEY}`
)
.then(response => response.json())
.then(data => setTracks(data.message.body.track_list))
.catch(err => console.log(err));
// to disable the warning rule of missing dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// state for heading
const [heading, setHeading] = useState(['Top 10 Tracks']);
return (
<TrackContext.Provider value={{ tracks, heading, selectedTrack, setSelectedTrack }}>
{props.children}
</TrackContext.Provider>
);
};
export default TrackContextProvider;
import React, { Fragment, useContext } from 'react';
import { Link } from 'react-router-dom';
import { TrackContext } from '../../contexts/TrackContext';
const TrackDetails = () => {
const { selectedTrack } = useContext(TrackContext);
console.log(selectedTrack);
return (
<Fragment>
<Link to="/">
<button>Go Back</button>
</Link>
<div>
{selectedTrack === undefined ? (
<p>loading ...</p>
) : (
<h3>
{selectedTrack.track.track_name} by {selectedTrack.track.artist_name}
</h3>
)}
<p>lyrics.............</p>
<div>Album Id: </div>)
</div>
</Fragment>
);
};
export default TrackDetails;
import React, { useState, useContext, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { TrackContext } from '../../contexts/TrackContext';
const Track = ({ trackInfo }) => {
const { selectedTrack, setSelectedTrack } = useContext(TrackContext);
const handleClick = e => {
setSelectedTrack(trackInfo);
};
console.log(selectedTrack);
return (
<li>
<div>{trackInfo.track.artist_name}</div>
<div>Track: {trackInfo.track.track_name}</div>
<div>Album:{trackInfo.track.album_name}</div>
<div>Rating:{trackInfo.track.track_rating}</div>
<Link to={{ pathname: `/trackdetails/${trackInfo.track.track_id}`, param1: selectedTrack }}>
<button onClick={handleClick}>> View Lyric</button>
</Link>
</li>
);
};
export default Track;
UPDATE: adding Tracks component
import React, { useContext, Fragment } from 'react';
import Track from './Track';
import { TrackContext } from '../../contexts/TrackContext';
const Tracks = () => {
const { heading, tracks } = useContext(TrackContext);
const tracksList = tracks.map(trackInfo => {
return <Track trackInfo={trackInfo} key={trackInfo.track.track_id} />;
});
return (
<Fragment>
<p>{heading}</p>
{tracks.length ? <ul>{tracksList}</ul> : <p>loading...</p>}
</Fragment>
);
};
export default Tracks;
I think the issue here is that since the selectedTrack is loaded asynchronously, when it is accessed from the context, it is undefined (you can get around the TrackContext being undefined by passing in a default value in the createContext call). Since the selectedTrack variable is populated anychronously, you should store it in a Ref with useRef hook, and return that ref as part of the context value. That way you would get the latest value of selectedTrack from any consumer of that context.
const selectedTracks = useRef([]);
useEffect(() => {
fetch(
`https://cors-anywhere.herokuapp.com/https://api.musixmatch.com/ws/1.1/chart.tracks.get?chart_name=top&page=1&page_size=10&country=fr&f_has_lyrics=1&apikey=${API_KEY}`
)
.then(response => response.json())
.then(data => {
selectedTrack.current = data.message.body.track_list;
})
.catch(err => console.log(err));
// to disable the warning rule of missing dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);