I'm trying to use context for handling pieces of authentication in my app. I was running into issues because I was trying to call useContext outside of my Context.Provider, so I moved the logic to a child component of the provider.
Now I'm getting an error message TypeError: Object is not iterable (cannot read property Symbol(Symbol.iterator)) where I'm calling useContext in the child component. Is the issue really with getting the values from the context or something else?
In app.js
import AuthContextProvider from "./components/context/authContext";
import RegisterRoutes from "./components/routing/registerRoutes";
function App() {
return (
<AuthContextProvider>
<Route
exact
path="/register"
render={(props) => (
<RegisterRoutes {...props} />
)}
/>
</AuthContextProvider>
)
}
In my authContext.js
import React, { useState, useEffect, createContext } from "react";
export const AuthContext = createContext();
const AuthContextProvider = (props) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const setAuth = (boolean) => {
setIsAuthenticated(boolean);
};
//Auth API logic here//
const apiOptions = {
url: "users/is-verified",
method: "GET",
headers: {
token: localStorage.token,
},
};
async function isAuth() {
axios(apiOptions)
.then((response) => {
const resData = response.data;
resData === true ? setIsAuthenticated(true) : setIsAuthenticated(false);
})
.catch((error) => {
console.log(error.response);
});
}
useEffect(() => {
isAuth();
}, []);
return (
<AuthContext.Provider
value={[isAuthenticated, setIsAuthenticated, setAuth]}
>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContextProvider;
In my registerRoutes.js
import React, { useContext } from "react";
import { Redirect } from "react-router-dom";
import Register from "../pages/register";
import AuthContext from "../context/authContext";
function RegisterRoutes(props) {
const [isAuthenticated, setAuth] = useContext(AuthContext);
return !isAuthenticated ? (
<Register {...props} setAuth={setAuth} />
) : (
<Redirect to="/login" />
);
}
export default RegisterRoutes;
As the error says, the Context.Provider in authContext.js value is not iterable:
<AuthContext.Provider value={[isAuthenticated, setIsAuthenticated, setAuth]}>
The value passed to the provider needs to be an iterable value, in this case, a valid JSON object, instead of the array that you have provided. so, we change it to:
<AuthContext.Provider value={{isAuthenticated, setIsAuthenticated, setAuth}}>
Then you change the reference in registerRoutes.js to correctly consume the new structure:
const [isAuthenticated, setAuth] = useContext(AuthContext);
becomes
const { isAuthenticated, setAuth } = useContext(AuthContext);
Voila! Your Context.Provider value is iterable and you can consume it in your application.
I think this will help you. My solution for accessing data in the context is creating a custom hook.
//localState.js
import { createContext, useState, useContext } from 'react'
const LocalStateContext = createContext()
const LocalStateProvider = LocalStateContext.Provider
function LocalState({children}) {
const [someState, setSomeState] = useState('')
const defaultValues = {
someState, setSomeState
}
return <LocalStateProvider value={defaultValues}>
{children}
</LocalStateProvider>
}
function useLocalState() {
const all = useContext(LocalStateContext)
return all
}
export {LocalState, LocalStateContext, useLocalState}
With this code you can wrap your whole app in the LocalState component and access context values by using the new useLocalState hook. For example
import { useLocalState} from './localstate'
const SomeComponent = () => {
const { someState } = useLocalState()
return (
///...whatever you want
)
}
export default SomeComponent
I think your issue may be that you have put your default values in an array inside of the value object.
Related
I have a state that I want to make global so that I can use it across multiple different components
and I am trying to do this through using context.
So I have my initial Component which gets the data and sets the global state, the issue I am having is when I try to use this state in the other components it seems to be empty because I believe my GlobalContext varibale is not updating so will be empty when the other components try to use the state. I cannot seem to figure out what I am missing to ensure my global state and context are both updated so that I can use them across the different components that require the data as well.
Can anyone figure out where I should update my context as well as my state
Component that gets the data initially:
import React from "react";
import { useState, useEffect, useMemo, useContext } from "react";
import axios from "axios";
import { GlobalContext } from "./Store";
function Map() {
// ------- global state
const [activities, setActivities] = useContext(GlobalContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setActivitieData();
console.log("activities after useEffect", activities)
}, []);
const getActivityData = async () => {
console.log("calling")
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
console.log("Global activities state = ", activities);
};
return !isLoading ? (
<>
<MapComp
activityData={activities}
/>
</>
) : (
<div>
<p>Loading...</p>
</div>
);
}
export default Map;
GlobalStateStore component:
import React, {useState} from "react";
const initState = [];
export const GlobalContext = React.createContext();
const Store = ({children}) => {
const [activities, setActivities] = useState(initState);
return (
<GlobalContext.Provider value={[activities, setActivities]}>
{children}
</GlobalContext.Provider>
)
}
export default Store;
component I am trying to use the global state in but is empty:
import React, {useContext} from 'react';
import { GlobalContext } from "./Store";
function ActivityList() {
const [activities, setActivities] = useContext(GlobalContext);
let displayValues;
displayValues =
activities.map((activity) => {
return (
<div>
<p>{activity.name}</p>
<p>{activity.distance}m</p>
</div>
);
})
return (
<>
<p>Values</p>
{displayValues}
</>
);
}
export default ActivityList;
App.js:
function App() {
return (
<Store>
<div className="App">
<NavBar />
<AllRoutes />
</div>
</Store>
);
}
export default App;
Here's a barebones single-file version of your code that certainly works.
Since you aren't showing how you're mounting your <Map /> and <ActivityList /> components originally, there's not much more I can do to help you with that code, though I will note that it's useless to try and log activities in the same function that has just setActivities, since setState is async (and the function will have captured the earlier activities value anyway).
import React, { useContext, useState, useEffect } from "react";
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
async function getActivityData() {
console.log("calling");
await delay(1000);
return [{ name: "foo", distance: 123 }];
}
function Map() {
const [, setActivities] = useContext(GlobalContext);
useEffect(() => {
getActivityData().then(setActivities);
}, [setActivities]);
return <>map</>;
}
const initState = [];
const GlobalContext = React.createContext();
const Store = ({ children }) => {
const [activities, setActivities] = useState(initState);
return (
<GlobalContext.Provider value={[activities, setActivities]}>
{children}
</GlobalContext.Provider>
);
};
function ActivityList() {
const [activities] = useContext(GlobalContext);
return <div>{JSON.stringify(activities)}</div>;
}
export default function App() {
return (
<Store>
<Map />
<ActivityList />
</Store>
);
}
I'm trying to make an auth Context:
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
and a custom hook to get it:
import { useContext } from "react";
import AuthContext from "./AuthProvider";
const useAuth = () => {
return useContext(AuthContext);
}
export default useAuth;
And now when I try to use the setAuth function:
const Login = () =>{
const {setAuth} = useAuth();
...
setAuth({authResponce});
I get an error : Uncaught TypeError: setAuth is not a function .
I'm new to react and I don't understand the reason yet, so I'll be glad to help.
Context only works if the provider is farther up the component tree than the consumer. If there is no provider, then Login will receive the default value, and you set the default to an empty object when you wrote createContext({});. Since that object has no setAuth function, you get the error.
To fix this, make sure the auth provider is near the top of the tree, and login is inside it. Login doesn't need to be an immediate child of the provider, but it does need to be a descendant.
// Good
const App = () => {
return (
<AuthProvider>
<Login />
</AuthProvider>
)
}
// Also good
const App = () => {
return (
<AuthProvider>
<SomeOtherComponent />
</AuthProvider>
)
}
const SomeOtherComponent = () => {
return <Login />
}
// Bad
const App = () => {
return (
<div>
<AuthProvider/>
<Login/>
</div>
)
}
This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed last year.
I have 2 routes that each one is for a type of user, how do I do it differently, I already have my auth, but I don't know how to do that. If anyone knows or has seen any documentation on this just put it below, I'm finding this all day
import React, { createContext, useState, useEffect } from 'react'
import { useNavigate } from "react-router-dom";
import axios from '../services/api';
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const [user, setUser] = useState()
const [loading, setLoading] = useState(true)
useEffect(() => {
const recoveredUser = localStorage.getItem('user')
if (recoveredUser) {
const jsonUser = JSON.parse(recoveredUser)
axios.defaults.headers.common['Authorization'] = `Bearer ${jsonUser.token}`;
setUser(jsonUser)
navigate('/user')
}
setLoading(false)
}, [])
const login = (data) => {
if (data) {
localStorage.setItem('user', JSON.stringify(data))
setUser(data)
navigate('/user')
}
}
const logout = () => {
localStorage.removeItem('user')
setUser("")
navigate('/login')
}
return (
<AuthContext.Provider value={{ authenticated: !!user, user, loading, login, logout }}>
{children}
</AuthContext.Provider>
)
}
Kent C Dodds has a really good article about this and its what I follow in my apps.
https://kentcdodds.com/blog/authentication-in-react-applications
Because you have your user in your context you can just check that where your routes get rendered.
import { Routes } from 'react-router-dom'
import { useAuth } from './context/auth'
function App() {
const { user } = useAuth();
return user ? <AuthenticatedApp /> : <UnauthenticatedApp />
}
function AuthenticatedApp () {
return (
<Routes>
// all your authenticated routes
</Routes>
)
}
function UnauthenticatedApp () {
return (
<Routes>
// all your un authenticated routes
</Routes>
)
}
In my example I created a useAuth() hook which you would have to do in your file.
function useAuth() {
const context = useContext(AuthContext);
return context;
}
export { useAuth };
I have a context API:
import React, { createContext, useState } from "react";
const UserContext = createContext();
const UserContextProvider = (props) => {
const [userInfo, setUserInfo] = useState({});
return (
<UserContext.Provider value={{ userInfo, setUserInfo }}>
{props.children}
</UserContext.Provider>
)
};
export { UserContextProvider, UserContext };
and use it in App.js:
<UserContextProvider>
// Router,...
</UserContextProvider>
Well, I gonna use context API in component like a service:
import { UserContext } from "...";
function UserService() {
const { userInfo, setUserInfo } = useContext(UserContext);
const updateUserInfo = (newUserInfo) => {
setUserInfo(newUserInfo); // for example: {name:'x'}
}
return null;
}
Now I wanna use UserService inside a component without add <UserService /> ? How can I call UserService.updateUserInfo()?
Your don't need to userService.You can use UserContext directly inside App.js and access to its function but you must wrap App.js inside UserContextProvider like:
<UserContextProvider>
<App />
</UserContextProvider>
Or you can use HOC(higher order component) like:
const withUsersContext = (Comp) => {
return (props) => (
<UserContextProvider>
<Comp {...props} />
</UserContextProvider>
);
};
// then inside App.js:
...
export default withUsersContext(App)
Now inside App.js:
const { userInfo, setUserInfo } = useContext(UserContext);
Note: if you wanna use UserContext inside something like UserService you must write a custom hook for UserService(rules of react-hooks).
Custom hooks for UserService:
function useUserService() {
const { userInfo, setUserInfo } = useContext(UserContext);
const updateUserInfo = (newUserInfo) => {
setUserInfo(newUserInfo);
}
return { updateUserInfo };
}
How to use inside a component:
...
const { updateUserInfo }= useUserService();
...
I am trying to set my state in Context from within a nested child component, but it doesn't see my method, which I created in the context provider:
import React, { createContext, useState } from "react"
const Context = createContext({})
const Provider = ({ children }) => {
const [value, setValue] = useState(undefined)
return (
<Context.Provider
value={{
value,
loadValue: currentValue => {
setValue(currentValue)
},
}}
>
{children}
</Context.Provider>
)
}
export default Context
export { Provider }
In my child component I try to set it like so:
import React, { useContext } from "react"
import Context from "../context/value.context"
const MyPage = ({ data }) => {
const value = data.contentfulValue
const { loadValue } = useContext(Context)
loadValue(value)
return (
<Layout>
...
</Layout>
)
}
export default MyPage
export const valueQuery = graphql`
query valueBySlug($slug: String!) {
contentfulValue(slug: { eq: $slug }) {
...
}
}
`
The error I'm getting is TypeError: loadValue is not a function
While you not providing the entire app structure,
you may encounter such error when MyPage is not a child of Context.Provider,
therefore it suggested to add an initial value when creating the context:
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This can be helpful for testing components in isolation without wrapping them. Note: passing undefined as a Provider value does not cause consuming components to use defaultValue.
const Context = createContext({ value: undefined, loadValue: () => console.log('Default function') })
I have Changed your code a little bit
export context as named so that you can use it
import React, { createContext, useState } from "react"
export const Context = createContext({})
export default const Provider = ({ children }) => {
const [value, setValue] = useState(undefined)
return (
<Context.Provider
value={{
value,
loadValue: currentValue => {
setValue(currentValue)
},
}}
>
{children}
</Context.Provider>
)
}
Use static context like following.
import React, { useContext } from "react"
import {Context} from "../context/value.context"
const MyPage = ({ data }) => {
static contextType = Context
const value = data.contentfulValue
const { loadValue } = this.context
loadValue(value)
return (
<Layout>
...
</Layout>
)
}
export default MyPage
And Remember to import Provider at the topmost of the component tree so that any component can use it, like this
<Provider>
{/* <PageContent> */}
<Navbar />
<Forms />
{/* </PageContent> */}
</Provider>
if still doesn't work do tell me.