Update React context from child component - reactjs

I am not able to update my context object from child component. Here is my provider file for creating react context and that component is wrapped over whole application in root app.tsx file.
I can fetch context in child components but I am not sure how to update it.
const CorporateContext = React.createContext(undefined);
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const CorporateProvider = ({ children }) => {
const [corporateUserData, setCorporateUserData] = useState(undefined);
useEffect(() => {
const localStorageSet = !!localStorage.getItem('corporateDetailsSet');
if (localStorageSet) {
setCorporateUserData({corporateId: localStorage.getItem('corporateId'), corporateRole: localStorage.getItem('corporateRole'), corporateAdmin: localStorage.getItem('corporateAdmin'), corporateSuperAdmin: localStorage.getItem('corporateSuperAdmin')});
} else {
(async () => {
try {
const json = await
fetchCorporateUserDetails(getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log('json invalid');
return;
}
setCorporateUserData({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
addCorporateDetailsToLocalStorage(corporateUserData);
} catch (e) {
console.log(e.message);
}
})();
}
}, []);
return (
<CorporateContext.Provider value={corporateUserData}>
{children}
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Update: Here is what I changed and context seems to be updated (at least it looks updated when I check in chrome devtools) but child components are not re-rendered and everything still stays the same:
I changed react context to:
const CorporateContext = React.createContext({
corporateContext: undefined,
setCorporateContext: (corporateContext) => {}
});
Changed useState hook in a component:
const [corporateContext, setCorporateContext] = useState(undefined);
Passed setState as property to context:
<CorporateContext.Provider value={{corporateContext , setCorporateContext}}>
{children}
</CorporateContext.Provider>

You can also pass in setCorporateUserData as a second value to the provider component as such:
//...
return (
<CorporateContext.Provider value={{corporateUserData, setCorporateUserData}}>
{children}
</CorporateContext.Provider>
);
You can then destructure it off anytime you want using the useContext hook.

How I resolved this issue:
Problem was that I have cloned corporateContext object and changed property on that cloned object and then I passed it to setCorporateContext() method. As a result of that, context was updated but react didnt noticed that change and child components were not updated.
let newCorporateContext = corporateContext;
newCorporateContext.property = newValue;
setCorporateContext(newCorporateContext);
After that I used javascript spread operator to change the property of corporateContext inside setCorporateContext() method.
setCorporateContext({...corporateContext, property: newValue);
After that, react noticed change in hook and child components are rerendered!

Related

Adding state value to JSON object or context

I currently have a context that contains a JS object. I want to be able to add a useState value to this object so that I can later use useEffect on the context to see if the state value has changed.
Here, I have my context:
//This exported as SortContext
export default React.createContext({
sortKeys: [],
setSortKeys: () => {}
})
And here is where I'd like to use the sortKeys array:
export default function useObjectListSort (objectList, sortFunctionMap) {
const sortContext = useContext(SortContext)
useEffect(() => {
//Do something when sortKeys gets updated.
}, sortContext.sortKeys)
And this is a component I made to initialize the context:
export default function SortBlock ({children}) {
const [sortKeyList, setSortKeyList] = useState([])
const sortContextConfig = {
sortKeys: sortKeyList,
setSortKeys: (newKeys) => setSortKeyList(newKeys)
}
return (
<SortContext.Provider value={sortContextConfig}>
{children}
</SortContext.Provider>
)
}
What I would expect is that using the my SortBlock component, and calling the setSortKeys function would update the sortKeyList state, and then the useEffect statement would trigger.
For example:
<SortContext.Consumer>
{sortContext => {
<button onClick={()=>sortContext.setSortKeys(['myKey'])}> Set Keys </button>
}}
</SortContext.Consumer>
Unfortunately, the useEffect is not triggering.
I've figured out the issue. I used the useObjectSortList hook in the same component that I used to initialize the SortBlock. This means that it falls outside of the scope of my context.

React native screens not re-rendering when custom hook state got changed

I am using a custom hook in app purchases.
const useInAppPurchase = () => {
const context = useContext(AuthGlobal)
const [isFullAppPurchased, setIsFullAppPurchased] = useState(false)
useEffect(() => {
console.log(`InAppPurchase useEffect is called`)
getProductsIAP()
return async () => {
try {
await disconnectAsync()
} catch (error) {}
}
}, [])
....
}
When I used this hook at AccountScreen (where I do the purchase) Account screen is getting re-rendered once the payment is done.
i.e. isFullAppPurchased is changing from false -> true
const AccountScreen = (props) => {
const width = useWindowDimensions().width
const {
isFullAppPurchased,
} = useInAppPurchase()
return (
// value is true after the purchase
<Text>{isFullAppPurchased}</Text>
)
}
But I am using the same hook in CategoryList screen and after the payment is done when I navigate to the CategoryList screen, The values (isFullAppPurchased) is not updated (still false).
But when I do the re-rendering manually then I get isFullAppPurchased as true.
const CategoryList = (props) => {
const navigation = useNavigation()
const { isFullAppPurchased } = useInAppPurchase()
return (
// still value is false
<Text>{isFullAppPurchased}</Text>
)
}
What is the reason for this behaviour ? How should I re-render CategoryList screen once the payment is done ?
Thank you.
I see hook make API request only on mount, if whole parent component didn't unmount and rendered a new, value of hook stays same.
E.g. dependencies array is empty - [] so hook doesn't request data again.
Probably better idea is to pass isFullAppPurchased via context or redux from top level.
And put state and function to update that state in same place.

React with Inversify makes useEffect infinite loop

I am using CRA + TS and integrated a custom hook for dependency injection(Inversify). Here is my code:
// DI Provider
export const InversifyContext = React.createContext<{ container: Container | null }>({ container: null });
export const DiProvider: React.FC<Props> = (props) => {
return <InversifyContext.Provider value={{ container: props.container }}>{props.children}</InversifyContext.Provider>;
};
// Custom Hook
export function useInjection<T>(identifier: interfaces.ServiceIdentifier<T>): T {
const { container } = useContext(InversifyContext);
if (!container) {
throw new Error();
}
console.log("This is hook"); // This gets printed infinitely
return container.get<T>(identifier);
}
// Injectable Service
#injectable()
class MyService{
// some methods
}
// index.tsx
const container = new Container();
container.bind<MyService>("myService").to(MyService);
ReactDOM.render(
<DiProvider container={container}>
<MyComponent />,
document.getElementById("root")
);
// MyComponent.tsx
const MyComponent: React.FC = () => {
const myService = useInjection<MyService>("myService");
useEffect(() => {
myService.getData(); // Loop call
}, [myService]);
}
Now when I debugged the code, I see that the provider is getting rendered infinitely, which causes the rerender of the component.
First, you need to understand why is this happening.
When you use your injected service in the useEffect hook as a dependency (which you should), it triggers a component rerender, which will recall the useInjection hook and a new/updated instance of MyService is returned, because the instance is changed, the useEffect will be triggered again and this will end you up with recursive calls.
I agree that you should not ignore the useEffect dependencies. A simple solution here would be to memoize the service within the hook.
So your memoized hook will become:
export function useInjection<T>(identifier: interfaces.ServiceIdentifier<T>): T {
const { container } = useContext(InversifyContext);
if (!container) {
throw new Error();
}
console.log("This is hook"); // This gets printed infinitely
return useMemo(() => container.get<T>(identifier), [container, identifier]);
}
Your hook will now return a memoized version of the service instance.

Update React Context using a REST Api call in a functional component

I am trying to update the context of a React App using data resulted from an API call to a REST API in the back end. The problem is that I can't synchronize the function.
I've tried this solution suggested in this blog post https://medium.com/#__davidflanagan/react-hooks-context-state-and-effects-aa899d8c8014 but it doesn't work for my case.
Here is the code for the textContext.js
import React, {useEffect, useState} from "react";
import axios from "axios";
var text = "Test";
fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
text = json;
})
const TextContext = React.createContext(text);
export const TextProvider = TextContext.Provider;
export const TextConsumer = TextContext.Consumer;
export default TextContext
And this is the functional component where I try to access the data from the context
import TextProvider, {callTextApi} from "../../../../services/textService/textContext";
function Profile()
{
const text = useContext(TextProvider);
console.log(text);
const useStyles = makeStyles(theme => ({
margin: {
margin: theme.spacing(1)
}
}));
I can see the fetch request getting the data in the network section of the browser console but the context is not getting updated.
I've tried doing this in the textContext.js.
export async function callTextApi () {
await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
And I was trying to get the data in the Profile.js using the useEffect function as so
const [text, setText] = useState(null);
useEffect(()=> {
setText (callTextApi())
},[])
It's my first time using React.context and it is pretty confusing. What am I doing wrong or missing?
You have a lot of problems here. fetching and changing should happen inside Provider by modifying the value property. useContext receives an entire Context object not only the Provider. Check the following
//Context.js
export const context = React.createContext()
Now inside your Provider
import { context } from './Context'
const MyProvider = ({children}) =>{
const [data, setData] = useState(null)
useEffect(() =>{
fetchData().then(res => setData(res.data))
},[])
const { Provider } = context
return(
<Provider value={data}>
{children}
</Provider>
)
}
Now you have a Provider that fetches some data and pass it down inside value prop. To consume it from inside a functional component use useContext like this
import { context } from './Context'
const Component = () =>{
const data = useContext(context)
return <SomeJSX />
}
Remember that Component must be under MyProvider
UPDATE
What is { children }?
Everything that goes inside a Component declaration is mapped to props.children.
const App = () =>{
return(
<Button>
Title
</Button>
)
}
const Button = props =>{
const { children } = props
return(
<button className='fancy-button'>
{ children /* Title */}
</button>
)
}
Declaring it like ({ children }) it's just a shortcut to const { children } = props. I'm using children so that you can use your Provider like this
<MyProvider>
<RestOfMyApp />
</MyProvider>
Here children is RestOfMyApp
How do I access the value of the Provider inside the Profile.js?
Using createContext. Let's assume the value property of your Provider is {foo: 'bar'}
const Component = () =>{
const content = useContext(context)
console.log(content) //{ foo : 'bar' }
}
How can you double declare a constant as you've done in the Provider?
That was a typo, I've changed to MyProvider
To access it from inside a class based component
class Component extends React.Component{
render(){
const { Consumer } = context
return(
<Consumer>
{
context => console.log(contxt) // { foo: 'bar' }
}
</Consumer>
)
}
}
First thing that I am seeing is that you are not returning the promise within your function which will lead to setting the state to undefined.
I added the return statement below:
export async function callTextApi () {
return await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
Also your last then-chain could be cleaned up a bit and I am quite sure you can remove the await statement in an async function when returning a promise. It will automatically be awaited:
export async function callTextApi () {
return fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => json)
}
Second step would be to have a look at your useEffect hook. You want to setText after the promise from the api call has been resolved. So you have to make the callback function of useEffect asynchronous as well.
useEffect(async ()=> {
const newText = await callTextApi();
setText (newText);
},[])
Third step, would be to look at how to properly use the context api and the useContext hook. The useContext hook takes a context as a parameter but you passed the ContextProvider as the argument.
const text = useContext(TextContext);
The context and the context-provider are two different entities in the React world. Think of the context as state and functionality that you want to share across your application (like a global state), and think about the provider as a react component that manages one context and offers this context state to it's child components.
return(
<TextContext.Provider value={/* some value */}>
{children}
</TextContext.Provider>);
This is how a return statement of a provider component would look like and I think this code is currently missing in your application.

Using React hook to run Mobx method before initial render of component

As the title suggests, i have my state inside a Mobx store. I have an async method (action) that fetches the data and stores it in a property inside the store, from where i will access it in the necessary component.
Problem is, currently the property will be undefined on the initial component render - resulting in an error in the component.
How do i make use of useEffect() so that it runs an async method once and only once - and before the initial component render, ensuring the state will be available for return()? Something like this:
const Workflow = () => {
const store = useContext(WorkflowContext)
useEffect(async () => {
await store.getWorkflow()
}, [])
...
You can check if the property in the store is undefined and return null from the component if that is the case.
Example
const Workflow = () => {
const store = useContext(WorkflowContext);
useEffect(() => {
store.getWorkflow();
}, []);
if (store.someProperty === undefined) {
return null;
}
return <div>{store.someProperty}</div>;
};

Resources