We have UserContext which sets user object which we can use throughout application. Our UserContext keep executing every time and unnecessary making api call even though dependency hasn't changed.
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Whenever we navigate to different page in our react app, we are seeing "User" route being called every time even though token isn't updated. Our token changes only when user log off.
Our AppRouter looks like following;
import React from 'react';
import AppRouter from "./AppRouter";
import { Container } from 'react-bootstrap';
import Header from './components/Header';
import { ToastProvider, DefaultToastContainer } from 'react-toast-notifications';
import 'bootstrap/dist/css/bootstrap.css';
import './scss/styles.scss';
import { UserContextProvider } from './UserContextProvider';
export default function App() {
const ToastContainer = (props) => (
<DefaultToastContainer
className="toast-container"
style={{ zIndex:100,top:50 }}
{...props}
/>
);
return (
<UserContextProvider>
<ToastProvider autoDismiss={true} autoDismissTimeout={3000} components={{ ToastContainer }}>
<Container fluid>
<Header />
<AppRouter />
</Container>
</ToastProvider>
</UserContextProvider>
)
}
This is our internal app so we want user to be logged in for 30 days and they don't have to keep login every time. So when user login first time, we create a token for them and keep that token in cookies. So if user close the browser and come back again, we check token in cookies. If token exists, we make API call to fetch user information and setUser in our context. This is the part which isn't working and it keep calling our user api during navigation to each route in application.
Here is our login.js
import React, { useState, useContext } from 'react';
import { setCookies } from '../../utils/Helper';
import APIService from '../../utils/RestApiService';
import { UserContext } from '../../UserContextProvider';
import queryString from 'query-string';
import './_login.scss';
const Login = (props) => {
const [email, setEmail] = useState(null);
const [password, setPassword] = useState(null);
const [error, setError] = useState(null);
const { siteId } = props;
const { refreshToken} = useContext(UserContext);
const onKeyPress = (e) => {
if (e.which === 13) {
attemptLogin()
}
}
let params = queryString.parse(props.location.search)
let redirectTo = "/"
if (params && params.redirect)
redirectTo = params.redirect
const attemptLogin = async () => {
const payload = {
email: email,
password: password,
siteid: siteId
};
let response = await APIService.post('/login', payload);
console.log('response - ', response)
if (response.status === 200) {
const { data } = response;
setCookies('UserToken', data.token);
refreshToken(data.token)
window.location.replace(redirectTo);
}
else {
const { error } = response.data;
setError(error);
}
}
const renderErrors = () => {
return (
<div className="text-center login-error">
{error}
</div>
)
}
return (
<div className="login-parent">
<div className="container">
<div className="login-row row justify-content-center align-items-center">
<div className="login-column">
<div className="login-box">
<form className="login-form form">
<h3 className="login-form-header text-center">Login</h3>
<div className="form-group">
<label>Email:</label>
<br/>
<input
onChange={e => setEmail(e.target.value)}
placeholder="enter email address"
type="text"
onKeyPress={onKeyPress}
className="form-control"/>
</div>
<div className="form-group">
<label>Password:</label>
<br/>
<input
onChange={e => setPassword(e.target.value)}
placeholder="enter password"
type="password"
className="form-control"/>
</div>
<div className="form-group">
<button
className="btn btn-secondary btn-block"
onClick={attemptLogin}
type="button">
Login
</button>
</div>
{error ? renderErrors() : null}
</form>
</div>
</div>
</div>
</div>
</div>
)
}
export default Login;
Our userContext looks like below
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
if (!token) return;
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Our getCookies function which simply read cookies using universal-cookies package
export const getCookies = (name) => {
return cookies.get(name);
};
So I tried to replicate your issue using a CodeSandbox, and these are my findings based on your code:
Context:
Your context has a useEffect which depend on token. When you call refreshToken, you update the token which automatically triggers the useEffect and makes a call to fetchUserInfo. So you don't need to call fetchUserInfo after setToken in refreshToken. Your context would look like:
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies("UserToken"));
const [user, setUser] = useState(null);
useEffect(() => {
console.log("Inside userContext calling as token ", token);
fetchUserInfo();
}, [token]);
const fetchUserInfo = async () => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
};
const refreshToken = (newToken) => {
setToken(newToken);
};
return (
<UserContext.Provider value={{ user, token, refreshToken }}>
{props.children}
</UserContext.Provider>
);
};
export { UserContextProvider, UserContext };
Route:
Now coming to your routing, since you've not included code of AppRouter I had to make an assumption that you use react-router with Switch component. (As shown in CodeSandbox).
I see a line in your Login component which is window.location.replace(redirectTo);. When you do this, the entire page gets refreshed (reloaded?) and React triggers a re-render, which is why I suppose your context methods fire again.
Instead use the history API from react-router (Again, my assumption) like so,
let history = useHistory();
history.push(redirectTo);
Here's the sandbox if you want to play around:
Related
Hi I am trying to learn and implement react context. I have encountered a bug when tryin to update user context.
I have a login page and when a user logs in, the email address and the user name should be updated in the user context.
When I click on submit, null information is stored in the context. But, when I click on submit the second time, I can see that the context is getting updated.
authentication.component.jsx
import Navigation from "../navigation/navigation.component";
import { Button, InputGroup, Form } from "react-bootstrap";
import { UserContext, setUser, setEmail } from "../../contexts/user.context";
import { useContext, useEffect, useState } from "react";
import axios from "axios";
import React from "react";
import "./authentication.styles.scss";
const UserLogon = () => {
const { setUser, setEmail } = useContext(UserContext);
const [emailAddr, setEmailAddr] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
//console.log(emailAddr);
}, [emailAddr, password]);
const updateFormData = () => {
setEmailAddr(emailAddr);
setPassword(password);
console.log("updated");
console.log(emailAddr);
console.log(password);
};
const saveEmail = (event) => {
setEmailAddr(event.target.value);
//console.log(emailAddr);
};
const savePassword = (event) => {
setPassword(event.target.value);
//console.log(password);
};
const verifyUserHandler = (event) => {
event.preventDefault();
const baseURL = `http://localhost:4000/verify_user?email=${emailAddr}&password=${password}`;
axios
.post(baseURL)
.then((response) => {
//console.log("User verified");
if (response.data[2] === "verified") {
console.log("user verified");
var email = response.data[0];
var name = response.data[1];
console.log("email: ", email);
console.log("name: ", name);
setEmail(email);
setUser(name);
} else {
console.log("user auth error");
}
})
.catch((e) => {
console.log(e);
});
};
return (
<div className="auth-container">
<div className="auth-login">
<div className="login-info">
<Form>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Email address</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
onChange={saveEmail}
/>
<Form.Text className="text-muted">
We'll never share your email with anyone else.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicPassword">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Password"
onChange={savePassword}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicCheckbox">
<Form.Check type="checkbox" label="Check me out" />
</Form.Group>
<Button variant="primary" type="submit" onClick={verifyUserHandler}>
Submit
</Button>
</Form>
</div>
</div>
</div>
);
};
export default UserLogon;
user.context.jsx
import { createContext, useState, useEffect } from "react";
export const UserContext = createContext({
currentUser: null,
setCurrentUser: () => null,
userEmail: null,
setUserEmail: () => null,
});
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [userEmail, setUserEmail] = useState(null);
const setUser = (user) => {
console.log("USER: user context before", currentUser);
setCurrentUser(user);
console.log("USER: user context after", currentUser);
};
const setEmail = (email) => {
console.log("EMAIL: user context before", userEmail);
setUserEmail(email);
console.log("EMAIL: user context after", userEmail);
};
const value = { setUser, setEmail, currentUser, userEmail };
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
Any help is appreciated.
thank you.
The React state setter function is async which means that at the moment you console.log it's not yet updated in your state : )
Try this to validate:
const setUser = (user) => {
setCurrentUser(user)
console.log({ currentUser, user })
// To better debug wrap vars in { }
}
const setEmail = (email) => {
setUserEmail(email)
console.log({ userEmail, email })
}
More info from docs: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Try removing the intitial values from createContext. I think it might be causing your issue.
import { createContext, useState, useEffect } from "react";
export const UserContext = createContext();
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [userEmail, setUserEmail] = useState(null);
const setUser = (user) => {
setCurrentUser(user);
console.log(currentUser);
};
const setEmail = (email) => {
setUserEmail(email);
console.log(userEmail);
};
const value = { setUser, setEmail, currentUser, userEmail };
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
Add useEffect console log to context provider to check when the state is changing.
useEffect(() => {
console.log("User", currentUser);
console.log("Email", userEmail);
}, [currentUser, userEmail]);
As you can see from here it works properly
This is the first time I'm asking a question here and also a newbie to coding. I'm trying to clone google classroom.
I am trying to use firestore to make a db collection when creating the class. But when I click create it doesn't create the class and create the db in firestore. It shows that the setDoc() function is invalid. Im using firestore version 9 (modular)
Here is my Form.js file. (The firestore related code is also included here)
import { DialogActions, TextField , Button} from "#material-ui/core"
import React, {useState} from 'react'
import { useLocalContext, useAuth } from '../../../context/AuthContext'
import { v4 as uuidV4 } from 'uuid'
import { db} from '../../../firebase'
import { collection, doc, setDoc } from "firebase/firestore"
const Form = () => {
const [className, setClassName] = useState('')
const [Level, setLevel] = useState('')
const [Batch, setBatch] = useState('')
const [Institute, setInstitute] = useState('')
const {setCreateClassDialog} = useLocalContext();
const {currentUser} = useAuth();
const addClass = async (e) => {
e.preventDefault()
const id = uuidV4()
// Add a new document with a generated id
const createClasses = doc(collection(db, 'CreatedClasses'));
await setDoc(createClasses, {
owner:currentUser,
className: className,
level: Level,
batch: Batch,
institute: Institute,
id: id
}).then (() => {
setCreateClassDialog(false);
})
}
return (
<div className='form'>
<p className="class__title">Create Class</p>
<div className='form__inputs'>
<TextField
id="filled-basic"
label="Class Name (Required)"
className="form__input"
variant="filled"
value={className}
onChange={(e) => setClassName(e.target.value)}
/>
<TextField
id="filled-basic"
label="Level/Semester (Required)"
className="form__input"
variant="filled"
value={Level}
onChange={(e) => setLevel(e.target.value)}
/>
<TextField
id="filled-basic"
label="Batch (Required)"
className="form__input"
variant="filled"
value={Batch}
onChange={(e) => setBatch(e.target.value)}
/>
<TextField
id="filled-basic"
label="Institute Name"
className="form__input"
variant="filled"
value={Institute}
onChange={(e) => setInstitute(e.target.value)}
/>
</div>
<DialogActions>
<Button onClick={addClass} color='primary'>
Create
</Button>
</DialogActions>
</div>
)
}
export default Form
And also (I don't know whether this is helpful but my context file is below)
import React, { createContext, useContext, useEffect, useState } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
onAuthStateChanged,
signOut,
GoogleAuthProvider,
signInWithPopup
} from "firebase/auth";
import { auth } from "../firebase";
const AuthContext = createContext();
const AddContext = createContext()
export function useAuth() {
return useContext(AuthContext);
}
export function useLocalContext(){
return useContext(AddContext)
}
export function ContextProvider({children}){
const [createClassDialog,setCreateClassDialog] = useState(false);
const [joinClassDialog, setJoinClassDialog] = useState(false);
const value = { createClassDialog, setCreateClassDialog, joinClassDialog, setJoinClassDialog };
return <AddContext.Provider value={value}> {children} </AddContext.Provider>;
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true)
function signup(email, password) {
return createUserWithEmailAndPassword(auth,email, password);
}
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
function logout() {
return signOut(auth);
}
function resetPassword(email) {
return auth.sendPasswordResetEmail(email)
}
function googleSignIn() {
const googleAuthProvider = new GoogleAuthProvider();
return signInWithPopup(auth, googleAuthProvider);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged( auth, (user) => {
setCurrentUser(user);
setLoading(false)
});
return () => {
unsubscribe();
};
}, []);
return (
<AuthContext.Provider
value={{ currentUser, login, signup, logout, googleSignIn, resetPassword,updateEmail, updatePassword }}
>
{!loading && children}
</AuthContext.Provider>
);
}
The console error message:
Try something like this, excluding the collection function from setting the document.
// Add a new document with a generated id
await setDoc(doc(db, 'CreatedClasses'), {
owner:currentUser,
className: className,
level: Level,
batch: Batch,
institute: Institute,
id: classId
}).then (() => {
setCreateClassDialog(false);
})
I want to understand why don't work properly the useEffect hook without the AbortionController.abort function.
I have a nested route in the app.js like:
<Route path='/profile' element={<PrivateRoute />}>
<Route path='/profile' element={<Profile />} />
</Route>
than the two component:
PrivateRoute:
import { Navigate, Outlet } from 'react-router-dom';
import { useAuthStatus } from '../hooks/useAuthStatus';
export default function PrivateRoute() {
const { loggedIn, checkingStatus } = useAuthStatus();
if (checkingStatus) return <h1>Loading...</h1>;
return loggedIn ? <Outlet /> : <Navigate to='/sign-in' />;
}
Profile:
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getAuth, updateProfile } from 'firebase/auth';
import { updateDoc, doc } from 'firebase/firestore';
import { db } from '../firebase.config';
import { toast } from 'react-toastify';
export default function Profile() {
const auth = getAuth();
const [changeDetails, setChangeDetails] = useState(false);
const [formData, setFormData] = useState({
name: auth.currentUser.displayName,
email: auth.currentUser.email,
});
const { name, email } = formData;
const navigate = useNavigate();
const onLogout = () => {
auth.signOut();
navigate('/');
};
const onSubmit = async (e) => {
try {
if (auth.currentUser.displayName !== name) {
//update display name if fb
await updateProfile(auth.currentUser, {
displayName: name,
});
//update in firestore
const userRef = doc(db, 'users', auth.currentUser.uid);
await updateDoc(userRef, {
name,
});
}
} catch (error) {
toast.error('Could not update profile details');
}
};
const onChange = (e) => {
setFormData((prev) => ({
...prev,
[e.target.id]: e.target.value,
}));
};
return (
<div className='profile'>
<header className='profileHeader'>
<p className='pageHeader'>My Profile</p>
<button type='button' className='logOut' onClick={onLogout}>
Logout
</button>
</header>
<main>
<div className='profileDetailsHeader'>
<p className='profileDetailsText'>Personal Details</p>
<p
className='changePersonalDetails'
onClick={() => {
setChangeDetails((prev) => !prev);
changeDetails && onSubmit();
}}
>
{changeDetails ? 'done' : 'change'}
</p>
</div>
<div className='profileCard'>
<form>
<input
type='text'
id='name'
className={!changeDetails ? 'profileName' : 'profileNameActive'}
disabled={!changeDetails}
value={name}
onChange={onChange}
/>
<input
type='text'
id='email'
className={!changeDetails ? 'profileEmail' : 'profileEmailActive'}
disabled={!changeDetails}
value={email}
onChange={onChange}
/>
</form>
</div>
</main>
</div>
);
}
and the custom Hook:
import { useEffect, useState } from 'react';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
export function useAuthStatus() {
const [loggedIn, setLoggedIn] = useState(false);
const [checkingStatus, setCheckingStatus] = useState(true);
useEffect(() => {
const abortCont = new AbortController();
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
setCheckingStatus(false);
});
// return () => abortCont.abort();
}, [setLoggedIn, setCheckingStatus]);
return { loggedIn, checkingStatus };
}
Can you explain me why do I get the error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I found the solution with the AbortController, but still don't understand what is the problem.
The error appears randomly, sometimes when I log in, sometimes when I'm not logged in, and try to go on the profile page. The app works fine, just want to understand what happens under the hood.
If I understand, if I'm not logged in, then it will rendered the 'Sign-in' page, if I'm logged in, then the 'Profile' page will be rendered, also there is a loading page but it's not the case. So, it's simple, if I'm logged in render this page, if not, render the other page. So where is the Problem? Why do I need the AbortController function?
onAuthStateChanges will listen forever in your useEffect hook. You need to unsubscribe every time the hook is run otherwise you will have these memory leaks. In your case the change of the users auth state will try to called setLoggedIn even when the component has been unmounted.
Looking at the documentation for onAuthStateChanged (https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#onauthstatechanged) it returns a firebase.Unsubscribe.
You'll have to do something like this:
useEffect(() => {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
setCheckingStatus(false);
});
return () => unsubscribe();
})
The callback you can optionally return in a useEffect hook is used for cleanup on subsequent calls.
I'm studying Nextjs, I come from React and a lot seems to be the same but I was lost with the authentication and private routes.
I looked for a lot of codes on the internet, but either they broke, they didn't make sense to me, or they didn't explain my doubts correctly. My scenario is basically:
I have an application
This application has public and private routes
Users need to login to access private routes
Private routes have the same Navbar
My questions are:
How to create private routes using ReactContext.
How to share the same NavBar between pages (Without having to place the NavBar component on each screen)
How to correctly authenticate a user with my own code using preferably ReactContext
How to reset the routes after authentication (The user is unable to return to the login screen if he clicks the back button on the browser)
How to correctly save the JWT token so that it saves the user's session for longer so that he does not need to log in again
My code is working so far, but I'm sure it is horrible and completely flawed.
I have the following files:
_app.js (Root of the Nextjs project)
index.js (Login page)
privateRoute.js (File that verifies if the user is logged in and allows or not his access)
userContext.js (File that saves user information to be accessed by other components and pages)
NavContext.js (File that checks whether someone is logged in to render the NavBar or not)
_app.js
function MyApp({ Component, pageProps }) {
return (
<UserContextFunc>
<NavContext>
<Component {...pageProps} />
</NavContext>
</UserContextFunc>
);
}
export default MyApp;
index.js
export default function App() {
const [loading, setLoading] = React.useState(false);
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const { setState } = React.useContext(UserContext);
const router = useRouter();
const login = () => {
setLoading(true);
fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
setLoading(false);
if (res.status === 200) {
res.json().then(({ token, roles }) => {
setState({ roles, token });
window.localStorage.setItem(
"rou",
btoa(unescape(encodeURIComponent(JSON.stringify(roles))))
);
window.localStorage.setItem("token", token);
router.replace("/app/adm/home");
});
}
})
.catch((err) => {
});
};
return (
<>
<ReactNotification />
<label htmlFor="exampleInputEmail1" className="form-label">
Email
</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="form-control"
/>
<label htmlFor="exampleInputPassword1" className="form-label">
Senha
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="form-control"
/>
<button
onClick={() => login()}
type="button"
className="btn btn-primary btn-block"
>
{loading ? (
<div
className="spinner-border text-light spinner-border-sm"
role="status"
>
<span className="visually-hidden"></span>
</div>
) : (
"Login"
)}
</button>
</>
);
}
privateRoute.js
const indexPage = "/";
const withAuth = (Component) => {
const Auth = (props) => {
const { setState } = React.useContext(UserContext);
const router = useRouter();
React.useEffect(() => {
const token = window.localStorage.getItem("token");
var roles = window.localStorage.getItem("rou");
if (roles) {
roles = decodeURIComponent(escape(atob(roles)));
}
if (!token || !roles || token == "undefined") {
window.localStorage.removeItem("token");
window.localStorage.removeItem("rou");
return router.replace(indexPage);
} else {
setState({ roles, token });
}
}, []);
return <Component {...props} />;
};
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default withAuth;
userContext.js
export const UserContext = React.createContext();
export const UserContextFunc = ({ children }) => {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'SET':
return {
...prevState,
...action.newState,
};
}
},
{
roles: []
}
);
const setState = newState => {
dispatch({ type: 'SET', newState });
}
const getState = async () => {
return state
}
return (
<UserContext.Provider value={{ getState, setState, state }}>
{children}
</UserContext.Provider>
);
}
NavContext.js
function NavContext(props) {
const { state } = React.useContext(UserContext);
return (
<>
{state.roles && state.token && <NavBar />}
{props.children}
</>
);
}
export default NavContext;
In private files I export them this way
import withPrivateRoute from "../../../utils/privateRoute";
...
export default withPrivateRoute(Dashboard);
I hope I managed to explain it well, I know it's a lot, but I didn't find any content explaining how to create a private route in Nextjs or how to authenticate correctly without using the authentication templates found in the Next documentation.
This code works, but as I said it seems completely wrong. I accept tips too.
I am getting "Cannot read property "map" of undefined (it is referring to tradeData) from the TradeDataList.js file. I am calling an API that is called through search term that is passed to "unirest" get function. I have setup context API passing some centralized values to various other files. I have it set up as an array in the useState. I am loosing my head with this if anyone can help please. The files I have are the following:
context.js
import React, { useState, useContext, useEffect } from 'react'
import { useCallback } from 'react'
var unirest = require('unirest');
const url = 'https://api.someurl.com/oauth/client_credential/accesstoken' ;
const Browse_URL = 'https://api.someurl.com/tradesregister/v1/browse' ;
const MY_API_KEY = 'EpVhCPGX4lzMwzdXVKG7yYFubGtwmlYU4343434';
const Authorization = 'Basic RXBWaENQR1g0bHpNd3pkWFZLRzd5WUZ1Ykd0d21sWVU6M2NvcEVuQTVEZlJXZ3BSYw==' ;
const AppContext = React.createContext()
const AppProvider = ({ children }) => {
const [token, setToken] = useState('') ;
const [loading, setLoading] = useState(true) ;
const [tradeData, setTradeData] = useState([]) ;
const [searchTerm, setSearchTerm] = useState('')
const getToken = () => {
unirest.get(url)
.header({'Accept': 'application/json', 'authorization': Authorization})
.query({"grant_type": "client_credentials"})
.end(function (response) {
const token = response.body["access_token"] ;
//console.log(token) ;
setToken(token)
})
}
useEffect (() => {
// call the getToken function
getToken() ;
}, [])
//Get the new token to access trade data
const newAuth = 'Bearer ' + token
return (
<AppContext.Provider
value={{ token,
MY_API_KEY,
newAuth,
Browse_URL,
loading,
setTradeData,
tradeData,
setLoading,
searchTerm,
setSearchTerm
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
SearchForm.js (call starts from here )
import React, { useEffect, useState } from 'react'
import { useGlobalContext } from '../context'
import TradeDataList from './TradeDataList'
//import Loading from './Loading'
var unirest = require('unirest');
const SearchForm = () => {
const {searchTerm, setSearchTerm,loading, setLoading, tradeData, setTradeData, MY_API_KEY, newAuth, Browse_URL } = useGlobalContext();
//const [tradeData, setTradeData] = useState([]) ;
const getTrade = () => {
setLoading(true)
unirest.get(Browse_URL)
.header({'Accept': 'application/json', 'authorization': newAuth, "apikey": MY_API_KEY })
.query({"searchText": searchTerm })
.end(function (response) {
const tradeData = response.body ;
//console.log(tradeData) ;
setTradeData(tradeData)
setLoading(false);
})
}
useEffect (() => {
// call the getTrade function
getTrade() ;
}, [])
const handleSubmit = (e) => {
e.preventDefault() ;
getTrade() ;
}
console.log('here is the data' ,tradeData)
return (
<section className="section-search">
<form className="search-form" onSubmit={handleSubmit}>
<div className="form-control">
<label htmlFor='searchTerm'> search trade licence info</label>
<input
type='text'
id='searchTerm'
//ref={searchValue} //reference
value={searchTerm}
//onChange={searchTrade}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
<div className="cocktails-center">
<TradeDataList tradeData={tradeData} />
</div>
</section>
)
}
export default SearchForm
TradeList.js
import React from 'react'
import TradeData from './TradeData'
//import Loading from './Loading'
//import { useGlobalContext } from '../context'
/**
* This page displays TradeData and loading
*/
const TradeDataList = ({ tradeData }) => {
//display loading while cocktails are being loaded
return (
<section className="section">
<h2 className="section-title">
Trade Data
</h2>
{/* <h2>cocktail list</h2> */}
<div className="cocktails-center">
{tradeData.map((trade) => {
// this is to be handled by Cocktail component
return <TradeData key={trade.licenceID} {...trade} />
})}
</div>
</section>
)
}
export default TradeDataList
TradeData.js
import React from 'react'
import { Link } from 'react-router-dom'
//image,name,id,info,glass
const TradeData = ({
licenceID,
licensee,
licenceName,
licenceNumber,
licenceType,
status,
suburb,
postcode,
businessNames,
categories,
classes
}) => {
return (
<article className="cocktail">
<div className="img-container">
<h1>This is header </h1>
</div>
<div className="cocktail-footer">
<h3>{licenceID}</h3>
<p>{licensee}</p>
<h4>{licenceName}</h4>
<p>{licenceType}</p>
{/* <Link to={`/cocktail/${id}`} className="btn btn-primary
btn-details">details</Link> */}
</div>
</article>
)
}
export default TradeData
Inside your TradeList.js you simply need to add a check condition:
<div className="cocktails-center">
{tradeData?tradeData.map((trade) => {
// this is to be handled by Cocktail component
return <TradeData key={trade.licenceID} {...trade} />
}):null}
</div>
Another alternative solution
{ tradeData && tradeData.map((trade) => {
// this is to be handled by Cocktail component
return <TradeData key={trade.licenceID} {...trade} />
})}
Note: The .map function is only available on array.
If data isn't in the format you are expecting it to be (it is {} but you are expecting []).