How to set a value with useContext()? - reactjs

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.

Related

How to update my global context and state

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>
);
}

Getting an 'undefined' value when trying to access context from provider in TypeScript/React

I am a noob with TypeScript and React Hooks. So I'm trying to learn with a simple todo app, please bear with me:
I've been trying to create a global state using the useContext and useState hooks. I set up some default values when I used useState, however when I try to access it through the Provider, I get an undefined value.
Here is my implementation of the context and Provider component:
import React, { createContext, PropsWithChildren, useState } from "react";
export interface AppContextType {
todos: TodoType[];
addTodo: (todo: TodoType) => void;
}
export interface TodoType {
todo: string;
}
export const AppContext = createContext<AppContextType>({} as AppContextType);
export const AppContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [todos, setTodos] = useState<TodoType[]>([
{ todo: "Learn React" },
{ todo: "Create Todo app" },
{ todo: "Learn TypeScript" },
]);
const addTodo = (todo: TodoType) => {
setTodos([...todos, todo]);
}
return (
<AppContext.Provider value={{ todos, addTodo }}>
{children}
</AppContext.Provider>
);
}
And here is my main App.tsx code:
import React, { useContext } from "react";
import TodoForm from "./components/TodoForm";
import {
AppContext,
AppContextProvider,
TodoType,
} from "./context/AppContext";
function App() {
const { todos, addTodo } = useContext(AppContext);
console.log(todos); // Returns undefined.
return (
<AppContextProvider>
<ul>
{todos.map((t: TodoType) => {
return <li key={t.todo}>{t.todo}</li>
})}
</ul>
<TodoForm />
</AppContextProvider>
);
}
export default App;
Am I overlooking anything?
Any help is appreciated, thank you!
EDIT: Thanks to #Erfan's answer, the issue was fixed by removing the code accessing these values in the same place where the Provider is the root, and by putting that code into a child component.
Updated App.tsx code:
import TodoForm from "./components/TodoForm";
import TodoList from "./components/TodoList";
import { AppContextProvider } from "./context/AppContext";
function App() {
return (
<AppContextProvider>
<TodoList />
<TodoForm />
</AppContextProvider>
);
}
export default App;
And the new TodoList component:
import React, { useContext } from "react";
import {
AppContext,
TodoType,
} from "../context/AppContext";
const TodoList = () => {
const { todos, addTodo } = useContext(AppContext);
console.log(todos); // This is ok!
return (
<ul>
{todos.map((t: TodoType) => {
return <li key={t.todo}>{t.todo}</li>
})}
</ul>
)
}
export default TodoList;
You are using AppContextProvider in the same component you want to use AppContext values.
In order to use the values, you need to wrap elements at least from one level higher than the current component.
In your case, you can create a List component and use the context value inside it.
const List = ()=>{
const { todos, addTodo } = useContext(AppContext);
console.log(todos); // Returns undefined.
return (<ul>
{todos.map((t: TodoType) => {
return <li key={t.todo}>{t.todo}</li>
})}
</ul>)
}
App:
function App() {
return (
<AppContextProvider>
<List/>
<TodoForm />
</AppContextProvider>
);}

How to mock a single state variable in a context provider with jest?

I am using jest and react-testing-library to write tests for my react application. In the application, I have a context provider that contains the state across most of the application. In my tests, I need to mock one state variable in context provider and leave one alone, but I'm not sure how to do this.
AppContext.tsx
const empty = (): void => {};
export const emptyApi: ApiList = {
setStep: empty,
callUsers: empty,
}
export const defaultState: StateList = {
userList = [],
step = 0,
}
const AppContext = createContext<ContextProps>({ api: emptyApi, state: defaultState });
export const AppContextProvider: React.FC<Props> = props => {
const [stepNum, setStepNum] = React.useState(0);
const [users, setUsers] = useState<User[]>([]);
const api: ApiList = {
setStep: setStepNum,
callUsers: callUsers,
}
const state: Statelist = {
userList: users,
step: stepNum,
}
const callUsers = () => {
const usersResponse = ... // call users api - invoked somewhere else in application
setUsers(userResponse);
}
return <AppContext.Provider value={{ api, state}}>{children}</AppContext.Provider>;
}
export default AppContext
In _app.tsx
import { AppContextProvider } from '../src/context/AppContext';
import { AppProps } from 'next/app';
import { NextPage } from 'next';
const app: NextPage<AppProps> = props => {
const { Component, pageProps } = props;
return (
<React.Fragment>
<AppContextProvider>
<Component {...pageProps}
</AppContextProvider>
</React.Fragment>
)
}
export default app
the component that uses AppContext
progress.tsx
import AppContext from './context/AppContext';
const progress: React.FC<Props> = props => {
const { state: appState, api: appApi } = useContext(AppContext);
const { userList, step } = appState;
const { setStep } = appApi;
return (
<div>
<Name />
{step > 0 && <Date /> }
{step > 1 && <UserStep list={userList} /> }
{step > 2 && <Address />
</div>
)
}
export default progress
users is data that comes in from an API which I would like to mock. This data is shown in progress.tsx.
stepNum controls the display of subcomponents in progress.tsx. It is incremented after a step is completed, once incremented, the next step will show.
In my test, I have tried the following for rendering -
progress.test.tsx
import progress from './progress'
import AppContext, { emptyApi, defaultState } from './context/AppContext'
import { render } from "#testing-library/react"
describe('progress', () => ({
const api = emptyApi;
const state = defaultState;
it('should go through the steps', () => ({
state.usersList = {...}
render(
<AppContext.Provider value={{api, state}}>
<progress />
</AppContext.Provider>
)
// interact with screen...
// expect(...)
})
})
However, when I set up the context provider like that in the test, I can set the userList to whatever I want, but it'll also override the setStep state hook so in the component, it won't update the state.
Is it possible to mock only the users variable inside of AppContext with jest, but leave users hook alone?

How to use context with hooks for authentication?

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.

React hooks: useState/context; Cannot read property 'avatar' of undefined/How to update a nested object

I have passed down a state variable and function from a context file:
UserContext:
import React, { useState } from 'react';
const UserContext = React.createContext();
function UserProvider({ children }) {
var [userImages, setUserImages] = useState({
avatar: '/static/uploads/profile-avatars/placeholder.jpg'
});
return (
<UserContext.Provider
value={{
userImages,
setUserImages
}}
>
{children}
</UserContext.Provider>
);
}
export default UserContext;
export { UserProvider };
At this point UserImages is just an object with one prop i.e. avatar
This is my App which is being wrapped by the Provider (please disregard the redux implementation, I just wanted to try Context)
import React from 'react';
import { Provider } from 'react-redux';
import { UserProvider } from './UserContext';
import App from 'next/app';
import withRedux from 'next-redux-wrapper';
import { PersistGate } from 'redux-persist/integration/react';
import reduxStore from '../store/index';
import withReactRouter from '../with-react-router/with-react-router';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const pageProps = Component.getInitialProps
? await Component.getInitialProps(ctx)
: {};
return { pageProps };
}
render() {
const { Component, pageProps, store } = this.props;
return (
<UserProvider>
<Provider store={store}>
<PersistGate persistor={store.__PERSISTOR} loading={null}>
<Component {...pageProps} />
</PersistGate>
</Provider>
</UserProvider>
);
}
}
I am trying to update some context using a setState function following this post
However I still get TypeError: Cannot read property 'avatar' of undefined
This is the shape of the state object:
userData:
setUserImages: ƒ ()
userImages:
avatar: "/static/uploads/profile-avatars/placeholder.jpg"
or
userData : {
setUserImages : SetUserImages function,
userImages : {
avatar : "/static/uploads/profile-avatars/placeholder.jpg"
}
}
My component:
function ImageUploader({ userData }) {
var { avatar } = userData.userImages;
var setUserAvatar = userData.setUserImages;
function avatarUpdater(avatarPath) {
setUserAvatar({ userData: { ...userData.userImages.avatar, avatarPath } });
}
}
Does anyone have an idea why this is happening?
UserProvider is the root of your app, so you can directly get it {userImages, setUserImages} in ImageUploader
function ImageUploader() {
const {userImages, setUserImages} = useContext(UserContext)
const { avatar } = userImages;
function avatarUpdater(avatarPath) {
setUserImages({ avatar: avatarPath });
}
}
Typically its good practice to not expose the setState from your context. You wanna wrap it in an explicit method to update state, then add that method to your Provider. Something like:
const userContext = {
avatar: userImages,
updateAvatarUrl: (url) => {
setUserImages(prevState => ({...prevState, avatar: url}))
}
}
return <UserContext.Provider value={userContext}>{children}</UserContext.Provider>
Try adding a hook for your UserContext which you can consume inside your component.
In UserContext add
export const useUserContext = () => useContext(UserContext)
Then inside your component:
import { useUserContext } from '<UserContext import>'
...
function avatarUpdater(avatarPath) {
userCtx.updateAvatarUrl(avatarPath)
}
Cleanest structure for Context in my opinion. Allows for more precise control over context state.

Resources