I thought that if I want to pass a state in another component, it is possible only through props or with contextAPI (provider etc).
For example:
import { createContext, useState, useContext } from 'react';
const FavContext = createContext();
//now we have a context object
function FavProvider(props) {
//create the provider and its functionality
const shape = {
type: '', //films, planets, people
id: 0, //id of film, or planet, or person
data: {}, //the actual data object
};
const [fav, setFav] = useState(shape);
function updateFav(type, id, data) {
setFav({
type,
id,
data,
});
}
return <FavContext.Provider value={[fav, updateFav]} {...props} />;
}
function useFav() {
//for pages that want to access the context object's value
//custom hook use...
const context = useContext(FavContext);
if (!context) throw new Error('Not inside the Provider');
return context; // [fav, updateFav]
}
export { useFav, FavProvider };
I wrap all the other components with the FavProvider, then I use the useFav wherever I want.
Here is a code without the contextAPI, and it works. Why is that? Why isn't necessary the contextAPI in that case?
import { useEffect, useState } from 'react';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
export default function useAuthStatus() {
const [loggedIn, setLoggedIn] = useState(false);
const [checkingStatus, setCheckingStatus] = useState(true);
useEffect(() => {
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
setLoggedIn(true);
}
setCheckingStatus(false);
});
});
return { loggedIn, checkingStatus };
}
and when I want to use the loggedIn and chekingStatus, I just bring it in like this:
import { Navigate, Outlet } from 'react-router-dom';
import useAuthStatus from '../hooks/useAuthStatus';
import Spinner from './Spinner';
export default function PrivateRoutes() {
const { loggedIn, checkingStatus } = useAuthStatus();
if (checkingStatus) {
return <Spinner />;
}
return loggedIn ? <Outlet /> : <Navigate to='/sign-in' />;
}
Why did you use an array to pass the state through the value in the context API?
Related
I have the following app entry component:
React.useEffect(() => {
const fetchData = async () => {
try {
const libraries: unknown[] = await sendRequest('/libraries');
const softwareComponents: unknown[] = await sendRequest('/softwareComponents');
localStorage.setItem('libraries', JSON.stringify(arraySetup(libraries, 'libraries')));
localStorage.setItem('softwareComponents', JSON.stringify(arraySetup(softwareComponents, 'software-components')));
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
I am fetching Arrays from two endpoints and then set the result in the Local Storage, so I can read from it in other components.
A child component is using the data like this:
const [data, setData] = React.useState<Array<any>>([]);
React.useEffect(() => {
const libraries = getLocalStorageItem('libraries');
const softwareComponents = getLocalStorageItem('softwareComponents');
const condition = libraries && softwareComponents;
if (condition) {
setData([...libraries, ...softwareComponents]);
}
}, []);
const getDataLength = (category: string) => {
return (data || []).filter((item: any) => item.category === category).length;
};
return (
<React.Fragment>
<OwcGrid item xs={12} s={4}>
<LibrariesCard numberOfElements={getDataLength('libraries')} /> // rendering here the length of the localStorage item.
</OwcGrid>
Goal/Challenge:
I want to use React.Context to remove local storage implementation but I am not sure how to keep it as simple as possible.
I only saw guides which implemented dispatch actions and so on but this seems already too complex because I only fetch the data and don't change it as I only render it.
Are there any tipps or guides how to start with this?
Possible implementation with context:
//context.tsx
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
export interface LibsAndComponentsInterface {
data: unknown[];
}
const LibsAndComponentsContext = createContext<
LibsAndComponentsInterface | undefined
>(undefined);
// Wrap your App component with this
export function LibsAndComponentsProvider({
children,
}: {
children: ReactNode;
}) {
const [libs, setLibs] = useState<unknown[]>([]);
const [components, setComponents] = useState<unknown[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const libraries: unknown[] = await sendRequest('/libraries');
const softwareComponents: unknown[] = await sendRequest(
'/softwareComponents'
);
setLibs(libraries);
setComponents(softwareComponents);
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
const ctxValue = useMemo(
() => ({
data: [...libs, ...components],
}),
[libs, components]
);
return (
<LibsAndComponentsContext.Provider value={ctxValue}>
{children}
</LibsAndComponentsContext.Provider>
);
}
export function useLibsAndComponents() {
const ctx = useContext(LibsAndComponentsContext);
if (ctx == null) {
throw new Error(
'useLibsAndComponents must be inside LibsAndComponentsProvider'
);
}
return ctx;
}
// later in your components
const { data } = useLibsAndComponents()
Here is the complete setup for React Context. Please use typescript if needed.
MyContextProvider.js
const { createContext, useState } = require("react");
//Create a context
export const Mycontext = createContext();
//Created a component that helps to provide the context.
const MyContextProvider = ({ children }) => {
//Declare all the states that you need
const [libraries, setLibraries] = useState([]);
const [softwareComponents, setSoftwareComponents] = useState([]);
return (
<Mycontext.Provider
//provide all the state, function as value that you need in any child component
value={{
libraries,
setLibraries,
softwareComponents,
setSoftwareComponents
}}
>
{children}
</Mycontext.Provider>
);
};
export default MyContextProvider;
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import MyContextProvider from "./MyContextProvider";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
//Wrap App component By the MycontextProvider component
root.render(
<StrictMode>
<MyContextProvider>
<App />
</MyContextProvider>
</StrictMode>
);
App.js
import { useContext } from "react";
import ChildComponent from "./Child";
import { Mycontext } from "./MyContextProvider";
import "./styles.css";
export default function App() {
//This is the way of getting value from context here useContext is the builtin hook and Mycontext is the context name
const { setLibraries, setSoftwareComponents } = useContext(Mycontext);
//After getting data from API use setLibraries and setSoftwareComponents to store data in the state(context) instead of local storage.
return (
<div>
<ChildComponent />
</div>
);
}
Child.js
import { useContext } from "react";
import { Mycontext } from "./MyContextProvider";
const ChildComponent = () => {
const { libraries, softwareComponents } = useContext(Mycontext);
//Here is your state you can use it as your need.
console.log(libraries, softwareComponents);
return <h1>Child Component</h1>;
};
export default ChildComponent;
I have a React context which I am using to manage the authentication within my application. I have done this previously and all seemed OK, but in this application the value of the isAuthenticated property is not being updated. I've tried to replicate using CodeSanbox but I get the expected result.
Essentially, I want the context to hold a value of isAuthenticating: true until the authentication flow has finished, once this has finished I will determine if the user is authenticated by checking isAuthenticated === true && authenticatedUser !== undefined however, the state does not seem to be getting updated.
As a bit of additional context to this, I am using turborepo and next.js.
AuthenticationContext:
import { SilentRequest } from '#azure/msal-browser';
import { useMsal } from '#azure/msal-react';
import { User } from 'models';
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { msal, sendRequest } from 'utils';
interface AuthenticationContextType {
authenticatedUser?: User;
isAuthenticating: boolean;
}
const AuthenticationContext = createContext<AuthenticationContextType>({
authenticatedUser: undefined,
isAuthenticating: true
});
export const AuthenticationProvider = (props: { children: React.ReactNode }) => {
const { accounts, instance } = useMsal();
const [user, setUser] = useState<User>();
const [isAuthenticating, setIsAuthenticating] = useState<boolean>(true);
const [currentAccessToken, setCurrentAccessToken] = useState<string>();
const getUserFromToken = useCallback(async () => {
if (user) {
setIsAuthenticating(false);
return;
}
const userRequest = await sendRequest('me');
if (! userRequest.error && userRequest.data) {
setUser(userRequest.data as User);
}
}, [user]);
const getAccessToken = useCallback(async () => {
if (! currentAccessToken) {
const request: SilentRequest = {
...msal.getRedirectRequest(),
account: accounts[0]
}
const response = await instance.acquireTokenSilent(request);
setCurrentAccessToken(response.accessToken);
}
return getUserFromToken();
}, [accounts, currentAccessToken, getUserFromToken, instance]);
useEffect(() => {
async function initialiseAuthentication() {
await getAccessToken();
setIsAuthenticating(false);
}
initialiseAuthentication();
}, [getAccessToken]);
return (
<AuthenticationContext.Provider value={{ authenticatedUser: user, isAuthenticating }}>
{ props.children }
</AuthenticationContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthenticationContext);
if (context === undefined) {
throw new Error("useAuth was used outside of it's provider.")
}
return context;
}
AuthenticationLayout:
import { useEffect, useState } from 'react';
import { AuthenticationProvider, useAuth } from '../hooks/authentication';
import MsalLayout from './msal-layout';
const AuthenticationLayout = (props: { children: React.ReactNode }) => {
const { isAuthenticating, authenticatedUser } = useAuth();
const wasAuthenticationSuccessful = () => {
return ! isAuthenticating && authenticatedUser !== undefined;
}
const renderContent = () => {
if (! wasAuthenticationSuccessful()) {
return (
<p>You are not authorized to view this application.</p>
)
}
return props.children;
}
if (isAuthenticating) {
return (
<p>Authenticating...</p>
)
}
return (
<MsalLayout>
{ renderContent() }
</MsalLayout>
)
}
export default AuthenticationLayout;
MsalLayout:
import { InteractionType } from '#azure/msal-browser';
import {
AuthenticatedTemplate,
MsalAuthenticationTemplate,
MsalProvider,
} from "#azure/msal-react";
import { msalInstance, msal } from 'utils';
import { AuthenticationProvider } from '../hooks/authentication';
msal.initialize();
const MsalLayout = (props: { children: React.ReactNode }) => {
return (
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect} authenticationRequest={msal.getRedirectRequest()}>
<AuthenticatedTemplate>
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
</AuthenticatedTemplate>
</MsalAuthenticationTemplate>
</MsalProvider>
)
}
export default MsalLayout;
Theoretically, once the authentication is finished I would expect the props.children to display.
I think that the problem is AuthenticationLayout is above the provider. You have consumed the provider in MsalLayout. Then AuthenticationLayout uses MsalLayout so the AuthenticationLayout component is above the provider in the component tree. Any component that consumes the context, needs to be a child of the provider for that context.
Therefore the context is stuck on the static default values.
Your capture of this scenario in useAuth where you throw an error is not warning you of this as when its outside the context -- context is not undefined, it is instead the default values which you pass to createContext. So your if guard isn't right.
There are some workarounds to checking if its available -- for example you could use undefined in the default context for isAuthenticating and authenticatedUser and then check that. Or you can change them to getters and set the default context version of this function such that it throws an error.
Working on a nextjs app using typescript w/ a firestore backend.
I currently am using my own useContext hook to make globally available a firestore user object, an array of 'business entity IDs' (strings) that the user is authorized to view, as well as an array of objects containing each biz's document data.
Because a user can be a member of multiple business entities, I'd like to be able to have them toggle between a 'current business' object whose document ID would be used to pass into further queries as well as document add / update functions.
I'm just not sure how to go about trigger the setting and switching of this 'current biz' id.
I'm assuming I'd want to somehow set the value in another usecontext hook so that the value is globally available to use around the app?
Below is my current custom context hooks doc
please let me know if you'd need to see any more data
import { auth, firestore } from "../lib/firebase";
import { useContext, useEffect, useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import {
doc,
onSnapshot,
collection,
query,
where,
getDocs,
} from "firebase/firestore";
import { BizContext } from "./context";
//types
// Custom hook to read auth record and user profile doc
export function useUserData() {
const [user, loading, error] = useAuthState(auth);
const [username, setUsername] = useState(null);
useEffect(() => {
// turn off realtime subscription
let unsubscribe;
//this actively listens to a the users collection
if (user) {
const userDocRef = doc(firestore, "users", user.uid);
unsubscribe = onSnapshot(userDocRef, (doc) => {
setUsername(doc.data()?.username);
});
} else {
setUsername(null);
}
return unsubscribe;
}, [user]);
return { user, username };
}
export function useBelongsTo() {
const [user] = useAuthState(auth);
const [belongsTo, setBelongsTo] = useState<[]>([]);
const [bizInfo, setBizInfo] = useState<[]>([]);
useEffect(() => {
let unsubscribe;
if (user) {
const bizColRef = collection(firestore, "businesses");
const q = query(bizColRef, where("admins", "array-contains", user.uid));
unsubscribe = onSnapshot(q, (querySnapshot) => {
let bizId: [] = [];
let bizInfo: [] = [];
querySnapshot.forEach((doc) => {
bizId.push(doc.id);
bizInfo.push(doc.data());
});
setBelongsTo(bizId);
setBizInfo(bizInfo);
});
} else {
setBelongsTo([]);
console.log("no businesses, no user");
}
return unsubscribe;
}, [user]);
return { belongsTo, bizInfo };
}
_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { ChakraProvider } from "#chakra-ui/react";
import customTheme from "../extendTheme";
import { Toaster } from "react-hot-toast";
import { BizContext, UserContext } from "../lib/context";
import { useBelongsTo, useUserData } from "../lib/hooks";
import Layout from "../components/layout/Layout";
function MyApp({ Component, pageProps }: AppProps) {
const userData = useUserData();
const bizData = useBelongsTo();
return (
<ChakraProvider theme={customTheme}>
<UserContext.Provider value={userData}>
<BizContext.Provider value={bizData}>
<Layout>
<Component {...pageProps} />
<Toaster />
</Layout>
</BizContext.Provider>
</UserContext.Provider>
</ChakraProvider>
);
}
export default MyApp;
context.tsx
import React from "react";
export const UserContext = React.createContext({});
export const BizContext = React.createContext({});
If you want to make belongsTo and bizInfo globally for later usage, I recommend you to use react Context instead of using hooks. Move the logic to the context like the sample below.
import { useContext, createContext, useEffect, useState } from "react";
export const AuthContext = createContext();
export function AuthContextProvider({ children }) {
const [user] = useAuthState(auth);
const [belongsTo, setBelongsTo] = useState<[]>([]);
const [bizInfo, setBizInfo] = useState<[]>([]);
const [currentBizId, setCurrentBizId] = useState();
useEffect(() => {
let unsubscribe;
if (user) {
const bizColRef = collection(firestore, "businesses");
const q = query(bizColRef, where("admins", "array-contains", user.uid));
unsubscribe = onSnapshot(q, (querySnapshot) => {
let bizId: [] = [];
let bizInfo: [] = [];
querySnapshot.forEach((doc) => {
bizId.push(doc.id);
bizInfo.push(doc.data());
});
setBelongsTo(bizId);
setBizInfo(bizInfo);
});
} else {
setBelongsTo([]);
console.log("no businesses, no user");
}
return unsubscribe;
}, [user]);
function switchBusinessEntity(id){
let currentBusinessEntity=belongsTo.find(biz=>biz.id === id);
setCurrentBizId(currentBusinessEntity);
}
return <AuthContext.Provider
value={{
belongsTo,
bizInfo,
currentBizId,
switchBusinessEntity
}}>
{children}
</AuthContext.Provider>;
}
export const useAuthContext = () => useContext(AuthContext);
And in the _app.js, wrapper the App with AuthContextProvider
import { AuthContextProvider } from "#/contexts/authContext";
function MyApp({ Component, pageProps }) {
return <AuthContextProvider>
<Component {...pageProps} />
</AuthContextProvider>;
}
export default MyApp;
And in the component, you can access the global states like this:
function YourComponent(){
const {
belongsTo,
bizInfo,
currentBizId,
switchBusinessEntity
} = useAuthContext()
return <select onChange={e => switchBusinessEntity(e.target.value)>
{belongsTo.map((bizId, index) => <option key={index} value={bizId.value}>{bizId}</option>)}
</select>
}
I'm attempting to create an Auth context file which, upon app load, checks if a user is signed in.
To do this, I'm using a 'helper' function which allows me to import the initialisation of the context and just build upon that with additional functions which authorise a user.
However, upon every app load, the Context is returning as 'undefined', and it says 'evaluating _useContext.trySignIn'.
For reference, here is my Context file:
import createDataContext from './createDataContext';
import { AsyncStorage } from 'react-native';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API_key');
const authReducer = (state, reducer) => {
switch (action.type) {
default:
return state;
}
};
const trySignIn = dispatch => async () => {
const isLoggedIn = await m.user.isLoggedIn();
if (isLoggedIn === true) {
navigate('Dashboard');
} else {
navigate('loginFlow');
}
};
export const { Provider, Context } = createDataContext (
authReducer,
{ trySignIn },
{ isLoggedIn: null }
);
Here is my 'createDataContext' file:
import React, { useReducer } from 'react';
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key].dispatch;
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
)
};
return { Context, Provider }
};
Here is my navigation file:
import { NavigationActions } from 'react-navigation';
let navigator;
export const setNavigator = (nav) => {
navigation = nav;
};
export const navigate = (routeName, params) => {
navigator.dispatch(
NavigationActions.navigate({
routeName, params
})
);
};
And finally, here is my component attempting to use my context:
import React, { useEffect, useContext } from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { Context } from '../context/AuthContext';
const LoadingScreen = () => {
const { trySignIn } = useContext(Context);
useEffect(() => {
trySignIn();
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
Can anyone see why my context would be returning as 'undefined' in my component?
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.