Using React Context in a custom hook always returns undefined - reactjs

I'm trying to create a custom hook which will eventually be packaged up on NPM and used internally on projects in the company I work for. The basic idea is that we want the package to expose a provider, which when mounted will make a request to the server that returns an array of permission strings that are then provided to the children components through context. We also want a function can which can be called within the provider which will take a string argument and return a boolean based on whether or not that string is present in the permissions array provided by context.
I was following along with this article but any time I call can from inside the provider, the context always comes back as undefined. Below is an extremely simplified version without functionality that I've been playing with to try to figure out what's going on:
useCan/src/index.js:
import React, { createContext, useContext, useEffect } from 'react';
type CanProviderProps = {children: React.ReactNode}
type Permissions = string[]
// Dummy data for fake API call
const mockPermissions: string[] = ["create", "click", "delete"]
const CanContext = createContext<Permissions | undefined>(undefined)
export const CanProvider = ({children}: CanProviderProps) => {
let permissions: Permissions | undefined
useEffect(() => {
permissions = mockPermissions
// This log displays the expected values
console.log("Mounted. Permissions: ", permissions)
}, [])
return <CanContext.Provider value={permissions}>{children}</CanContext.Provider>
}
export const can = (slug: string): boolean => {
const context = useContext(CanContext)
// This log always shows context as undefined
console.log(context)
// No functionality built to this yet. Just logging to see what's going on.
return true
}
And then the simple React app where I'm testing it out:
useCan/example/src/App.tsx:
import React from 'react'
import { CanProvider, can } from 'use-can'
const App = () => {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
{/* Again, this log always shows undefined */}
{can("post")}
</div>
</CanProvider>
)
}
export default App
Where am I going wrong here? This is my first time really using React context so I'm not sure where to pinpoint where the problem is. Any help would be appreciated. Thanks.

There are two problems with your implementation:
In your CanProvider you're reassigning the value in permissions with =. This will not trigger an update in the Provider component. I suggest using useState instead of let and =.
const [permissions, setPermissions] = React.useState<Permissions | undefined>();
useEffect(() => {
setPermissions(mockPermissions)
}, []);
This will make the Provider properly update when permissions change.
You are calling a hook from a regular function (the can function calls useContext). This violates one of the main rules of Hooks. You can learn more about it here: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions
I suggest creating a custom hook function that gives you the can function you need.
Something like this, for example
const useCan = () => {
const context = useContext(CanContext)
return () => {
console.log(context)
return true
}
}
Then you should use your brand new hook in the root level (as per the rules of hooks) of some component that's inside your provider. For example, extracting a component for the content like so:
const Content = (): React.ReactElement => {
const can = useCan();
if(can("post")) {
return <>Yes, you can</>
}
return null;
}
export default function App() {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
<Content />
</div>
</CanProvider>
)
}

You should use state to manage permissions.
Look at the example below:
export const Provider: FC = ({ children }) => {
const [permissions, setPermissions] = useState<string[]>([]);
useEffect(() => {
// You can fetch remotely
// or do your async stuff here
retrivePermissions()
.then(setPermissions)
.catch(console.error);
}, []);
return (
<CanContext.Provider value={permissions}>{children}</CanContext.Provider>
);
};
export const useCan = () => {
const permissions = useContext(CanContext);
const can = useCallback(
(slug: string) => {
return permissions.some((p) => p === slug);
},
[permissions]
);
return { can };
};
Using useState you force the component to update the values.
You may want to read more here

Related

Writing a TypeScript Interface for React Context, how to describe the interface/ different types to be passed as values?

I am trying to write a React Context for my application
This is a follow up from my previous question:
How to pass value to Context Provider in TypeScript?
I would please like some help describing different value types to be passed through the provider.
Overview:
I am currently trying to construct a context provider that can be used across the scope of my application in TypeScript.
It contains some useState hooks and and an asynchronous function to be passed through as values the provider to all the child components.
ResultConextProvider.tsx
export const ResultContextProvider = () => {
const [isLoading, setIsLoading] = useState<boolean>(false)
const [greenStatus, setGreenStatus] =
useState(new Set<MyEnum>());
const [redStatus, setRedStatus] =
useState(new Set<MyEnum>());
const [searchTerm, setSearchTerm] = useState<string>('')
// objects to be passed values
const greenValue = {greenStatus, setGreenStatus};
const redValue = {redStatus, setRedStatus};
const searchValue = {searchTerm, setSearchTerm};
// api function coming from tested API spec (external)
const getResults = async () => {
setIsLoading(true)
myAPI.myGet(greenStatus, redStatus).then((result) => {
setResults(result.data);
})
setIsLoading(false)
}
return (
<ResultContext.Provider value={{getResults, greenValue, redValue, searchValue}}>
{children}
</ResultContext.Provider>
}
export const useResultContext = () => useContext(ResultContext);
As you can see above, I would like to pass the getResults function, my greenValues, redValus and searchValues to all my child components, the Provider implentation will look something like this:
index.tsx
import { ResultContextProvider } from "./contexts/ResultContextProvider";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<ResultContextProvider
<Router>
<App />
</Router>
</ResultContextProvider>
);
When it comes to writing the interface, I am struggling to understand what needs to be represented prior to my component.
So far my interface looks something like this:
ResultConextProvider.tsx
interface ContextParametersType {
greenValue: { greenStatus: Set<MyEnum>, setGreenStatus:
Dispatch<SetStateAction<Set<MyEnum>>> };
redValue: { redStatus: Set<MyEnum>, setRedStatus:
Dispatch<SetStateAction<Set<MyEnum>>> };
///INCORRECT //
// searchValue: {searchTerm: <string>(''), setSearchTerm:
Dispatch<SetStateAction<string> };
///UNSURE
// getResults : {
// What to write as interface for getResults
// }
}
I have worked out what needs to be declared for my greenValue and redValue, based on the answer to my previous question however struggling to fill out the rest of the interface specification
The Search Term:
///INCORRECT //
// searchValue: {searchTerm: (''), setSearchTerm:
Dispatch<SetStateAction };
I have tried to follow the pattern of the Enum state, particularly in declaring the state as type string and rerferecning the Dispatch<SetStateAction as the same type to change the state however this produces error
getResults
This is an asynchronous function with an Axios function inside, to be truthful i do not know where to begin within this one.
My confusion on these two steps persists when creating the ResultContext
const ResultContext = createContext<ContextParametersType>({
greenValue: {
greenStatus: new Set<FirmStatusEnum>(), setGreenStatus: () => {
}
},
redValue: {
redStatus: new Set<FirmStatusEnum>(), setRedStatus: () => {
}
},
// searchValue: {
// searchTerm: <string>(''), setSearchTerm: () => {
// }
// },
getResults()
// Not Sure how to initalsie this?
});
Does this make sense? Am I understanding the issues here correctly?
I would really like some help in understanding how to configure the context interfaces for this.
It is essentially a typescript adaptation of this tutorial
Here:
Thanks, please let me know if I need to provide clarity

process is not defined on Next Page that use a HOC only on first render

I am trying to use a Higher-Order Component to read some values from the server (As environment configuration) in NextJS and React. The versions are 12.0.3 and 17.0.2 respectively. Also I am using Typescript.
I am tryng to use a HOC in order to reuse this functionality. When I was using a getServerSideProps method everything was fine, but now the first render when I start the app is failing. It is important to mention that everytime I just refresh the page, all seems to work correctly
This is my HOC:
// src/components/app/withEnvironment.tsx
import {
EnvironmentValues,
readPublicEnvironmentValues,
} from 'config/environment';
import {NextPage} from 'next';
import {EnvironmentProvider} from 'providers';
export function withEnvironment<TProps>(
Page: NextPage,
): (props: TProps & {environmentValues: EnvironmentValues}) => JSX.Element {
const PageWithEnvironment = ({
environmentValues,
...props
}: TProps & {environmentValues: EnvironmentValues}) => {
return (
<EnvironmentProvider value={environmentValues}>
<Page {...props} />
</EnvironmentProvider>
);
};
(PageWithEnvironment as NextPage).getInitialProps = () => {
return {
environmentValues: readPublicEnvironmentValues(),
};
};
return PageWithEnvironment;
}
And this is how I am trying to implement it:
// src/pages/change-sets/index.tsx
import {withEnvironment} from 'components/app';
import {useEnvironment} from 'providers'
function ChangeSetPage(): JSX.Element {
const environmentValues = useEnvironment();
return (
<div>
<h1>ChangeSetPage</h1>
</div>
);
}
export default withEnvironment(ChangeSetPage);
EnvironmentProvider and useEnvironment are part of a context that I would like to use to use to share values. This is how I defined them:
import {EnvironmentValues} from 'config/environment';
import React, {useContext} from 'react';
const EnvironmentContext = React.createContext<EnvironmentValues>(
{} as EnvironmentValues,
);
export const EnvironmentProvider = EnvironmentContext.Provider;
export const EnvironmentConsumer = EnvironmentContext.Consumer;
export function useEnvironment(): EnvironmentValues {
return useContext(EnvironmentContext);
}
Finally, the method readPublicEnvironmentValues only try to extract ENV values from the server, but as I mentioned on the first load process variable is always undefined;
export function readPublicEnvironmentValues(): EnvironmentValues {
// Here is where the exception throws becaouse of there is no process variable only at first attempt
const typedOptions = process.env as EnvironmentOptions;
const currentEnv = ENVIRONMENTS.find(
e => e === typedOptions.NEXT_PUBLIC_ENV || e === typedOptions.APP_ENV,
);
if (currentEnv) {
const values: EnvironmentValues = {
currentEnv,
skipAuth:
typedOptions.DEBUG === 'true' &&
typedOptions.SKIP_AUTH_ON_DEBUG === 'true',
};
if (typedOptions.BACKEND_PIVOT)
values.backendPivotApi = typedOptions.BACKEND_PIVOT;
if (typedOptions.BACKOFFICE_PIVOT)
values.backofficePivotApi = typedOptions.BACKOFFICE_PIVOT;
return values;
} else {
throw 'It is necessary to define an environment for the application. Please set NEXT_PUBLIC_ENV or APP_ENV';
}
}
It seems the getInitialProps method is being executed on the client side. This is the error message:

Is separating data retrieval code into interfaces and impl. of those interfaces a good idea in React?

My question is: is the below pattern a good idea in React or no? I come from Java world where this type of code is standard. However, I've ran into several things that, while being a good idea in Java, are NOT a good idea in ReactJS. So I want to make sure that this type of code structure does not have weird memory leaks or hidden side-effects in the react world.
Some notes on below code: I'm only putting everything in the same file for brevity purposes. In real life, the react component the interface and the class would all be in their own source files.
What I'm trying to do: 1) Separate the display logic from data access logic so that my display classes are not married to a specific implementation of talking to a database. 2) Separating DAO stuff into interface + class so that I can later use a different type of database by replacing the class implementaton of the same DAO and won't need to touch much of the rest of the code.
so, A) Is this a good idea in React? B) What sort of things should I watch out for with this type of design? and C) Are there better patterns in React for this that I'm not aware of?
Thanks!
import { useState, useEffect } from 'react';
interface Dao {
getThing: (id: string) => Promise<string>
}
class DaoSpecificImpl implements Dao {
tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
}
getThing = async (id: string) => {
// use a specific database like firebase to
// get data from tabled called tablename
return "herp";
}
}
const dao: Dao = new DaoSpecificImpl("thingies");
const Display: React.FC = () => {
const [thing, setThing] = useState("derp");
useEffect(() => {
dao.getThing("123").then((newThing) =>
setThing(newThing));
});
return (
<div>{thing}</div>
)
}
export default Display;
https://codesandbox.io/s/competent-taussig-g948n?file=/src/App.tsx
The DaoSpecificImpl approach works however I would change your component to use a React hook:
export const useDAO = (initialId = "123") => {
const [thing, setThing] = useState("derp");
const [id, setId] = useState(initialId);
useEffect(() => {
const fetchThing = async () => {
try{
const data = await dao.getThing(id);
setThing(data);
}catch(e){
// Handle errors...
}
}
fetchThing();
}, [id]);
return {thing, setId};
}
using the hook in your component:
const Display = () => {
const {thing, setId} = useDao("123"); // If you don't specify initialId it'll be "123"
return <button onClick={() => setId("234")}>{thing}</button> // Pressing the button will update "thing"
}
Side note: You could also use a HOC:
const withDAO = (WrappedComponent, initialId = "123") => {
.... data logic...
return (props) => <WrappedComponent {...props} thing={thing} setId={setId}/>
};
export default withDAO;
E.g. using the HOC to wrap a component:
export default withDao(Display); // If you don't specify initialId it'll be "123"

How to use useEffect correctly with useContext as a dependency

I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.

How do I update an array using the useContext hook?

I've set a Context, using createContext, and I want it to update an array that will be used in different components. This array will receive the data fetched from an API (via Axios).
Here is the code:
Context.js
import React, { useState } from "react";
const HeroContext = React.createContext({});
const HeroProvider = props => {
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
const [heroesContext, setHeroesContext] = useState(heroInformation);
return (
<HeroContext.Provider value={heroesContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };
See above that I created the context, but set nothing? Is it right? I've tried setting the same name for the array and function too (heroesContex and feedHeroes, respectively).
Component.js
import React, { useContext, useEffect } from "react";
import { HeroContext } from "../../context/HeroContext";
import defaultSearch from "../../services/api";
const HeroesList = () => {
const context = useContext(HeroContext);
console.log("Just the context", context);
useEffect(() => {
defaultSearch
.get()
.then(response => context.feedHeroes(response.data.data.results))
.then(console.log("Updated heroesContext: ", context.heroesContext));
}, []);
return (
//will return something
)
In the Component.js, I'm importing the defaultSearch, that is a call to the API that fetches the data I want to push to the array.
If you run the code right now, you'll see that it will console the context of one register in the Just the context. I didn't want it... My intention here was the fetch more registers. I have no idea why it is bringing just one register.
Anyway, doing all of this things I did above, it's not populating the array, and hence I can't use the array data in another component.
Does anyone know how to solve this? Where are my errors?
The issue is that you are declaring a piece of state to store an entire context object, but you are then setting that state equal to a single destructured array.
So you're initializing heroesContext to
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
But then replacing it with ...arrayFromAPI.
Also, you are not spreading the array properly. You need to spread it into a new array or else it will return the values separately: setHeroesContext([...arrayFromAPI]);
I would do something like this:
const HeroContext = React.createContext({});
const HeroProvider = props => {
const [heroes, setHeroes] = useState([]);
const heroContext = {
heroesContext: heroes,
feedHeroes: arrayFromAPI => {
setHeroes([...arrayFromAPI]);
}
};
return (
<HeroContext.Provider value={heroContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };

Resources