React with Inversify makes useEffect infinite loop - reactjs

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.

Related

Adding functions to lit web components in react with typescript

I have a web component i created in lit, which takes in a function as input prop. but the function is not being triggered from the react component.
import React, { FC } from 'react';
import '#webcomponents/widgets'
declare global {
namespace JSX {
interface IntrinsicElements {
'webcomponents-widgets': WidgetProps
}
}
}
interface WidgetProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
var1: string,
successCallback: Function,
}
const App = () =>{
const onSuccessCallback = () =>{
console.log("add some logic here");
}
return(<webcomponents-widgets var1="test" successCallBack={onSuccessCallback}></webcomponents-widgets>)
}
How can i trigger the function in react component? I have tried this is vue 3 and is working as expected.
Am i missing something?
As pointed out in this answer, React does not handle function props for web components properly at this time.
While it's possible to use a ref to add the function property imperatively, I would suggest the more idiomatic way of doing things in web components is to not take a function as a prop but rather have the web component dispatch an event on "success" and the consumer to write an event handler.
So the implementation of <webcomponents-widgets>, instead of calling
this.successCallBack();
would instead do
const event = new Event('success', {bubbles: true, composed: true});
this.dispatch(event);
Then, in your React component you can add the event listener.
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () => {
console.log("add some logic here");
}
useEffect(() => {
widgetRef.current?.addEventListener('success', onSuccessCallback);
return () => {
widgetRef.current?.removeEventListener('success', onSuccessCallback);
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>);
}
The #lit-labs/react package let's you wrap the web component, turning it into a React component so you can do this kind of event handling declaratively.
React does not handle Web Components as well as other frameworks (but it is planned to be improved in the future).
What is happening here is that your successCallBack parameter gets converted to a string. You need to setup a ref on your web component and set successCallBack from a useEffect:
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () =>{
console.log("add some logic here");
}
useEffect(() => {
if (widgetRef.current) {
widgetRef.current.successCallBack = onSuccessCallback;
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>)
}

how to use mobx store in cypress component test?

I have mobx (mobx6) store in React18 application:
export class RootStore {
public usersStore ;
constructor() {
this.usersStore = new UsersStore(this);
}
}
const StoresContext = React.createContext(new RootStore());
export const useStores = () => React.useContext(StoresContext);
I have two components, one of them initializes the store first rendering and the other one using the data from store.
initialize - Component A:
const { usersStore } = useStores();
useEffect(() => {
startTransition(() => {
usersStore.load();
});
}, []);
reading the data - Component B:
useEffect(() => {
if (usersStore.data) {
doSomething();
}
}, [usersStore.data]);
I'm using Cypress10 to test Component B:
beforeEach(() => {
const store = new RootStore();
store.usersStore.data = createData();
const StoresContext = React.createContext(store);
cy.mount(<StoresContext.Provider value={store}>
<ComponentB />
</StoresContext.Provider>);
});
but when the test is running, the data of usersStore that is read in ComponentB, is always undefined.
I believe something is wrong with the context, because the index.ts creates context of new RootStore.
the question is - how can I pass specific store to cypress component test when mobx is defined this way?
I tried to stub React.createContext, but it caused error probably because this hook is used a lot, and I didn't find way to stub it by the parameter type, but by the parameter value what I don't have.
I tried to stub my custom hook useStores but it didn't help.
any other idea?

Can't stop React from updating the main component after state change

I'm having the following issue.
I have a component called "BackgroundService" who has a setInterval for requesting data from an API every 5 seconds. The received data from API is stored in "backgroundServiceResult" hook with useState, located in App and shared by a context provider.
_app.js:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
console.log("App reloaded")
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<BackgroundService/>
<Component {...pageProps} />
</AppContext.Provider>
)
}
BackgroundService.js:
import { useState, useEffect, useContext } from "react"
import AppContext from '#/hooks/AppContext'
export const BackgroundService = () => {
const { getLatestSyncInfo } = api()
const { isDBSet, getJson } = OfflineStorage()
const appContext = useContext(AppContext);
const [alreadyNotified, setalreadyNotified] = useState(false)
useEffect(async () => {
const intervalId = setInterval(async () => {
// REQUIRE DATA FROM API STUFF, AND CAll:
appContext.setBackgroundServiceResult(data or stuff);
}, 5000)
return () => clearInterval(intervalId);
}, [])
return (
<></>
)
}
The problem is, every time the appContext.setBackgroundServiceResult is called from BackgroundService.js, the entire App component is re-rendered! so the "console log" in App is called, and all the components mounted again.
How can I store the received data from API through all my application without rendering again all from App?
Any way for solving this?
Thanks you
Your application is following expected behaviour, when state or props update the component will re-render.
There are many options you could use to prevent this from negatively affecting parts of your application.
useEffect could be used to only run code in child components when the component is initially mounted or when specific props or state change.
useMemo could be used to only recalculate values upon specific props or state change.
useCallback could be used to only recreate a function when specific props or state change.
In your specific case here it doesn't make sense to create the BackgroundService if it isn't going to render anything. Instead you should be creating a hook like this:
import { useState, useEffect, useContext } from "react"
import AppContext from '#/hooks/AppContext'
export const useBackgroundService = () => {
const appContext = useContext(AppContext);
// Also bear in mind that the `useEffect` callback cannot be `async`
useEffect(() => {
// the `async` over here is fine though
const intervalId = setInterval(async () => {
appContext.setBackgroundServiceResult(data or stuff);
}, 5000)
return () => clearInterval(intervalId);
}, [])
}
And then call it in your app as follows:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
useBackgroundService();
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
Don't worry about the console.log going off, it won't negatively affect your application. If you had to do something like sort a massive list at the top level of your app component you could do something like this:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
useBackgroundService();
const sortedList = useMemo(() => pageProps.myList.sort(), [pageProps.myList]);
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
Then the sortedList value would only update when it needs to and your updated backgroundServiceResult wouldn't cause that value to be recalculated.
In the same way you could make use of useEffect in the children components to make sure code only runs on initial mount and not on the components being re-rendered.
If you update your question to be more specific about what problems your App being rendered are causing we could come up with a better solution to tackle that specific issue.

Update React context from child component

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!

React context provider not setting value on page load/refresh

I have the following hook for Pusher and I use it to share one instance across the application.
import React, { useContext, useEffect, useRef } from "react";
import Pusher from "pusher-js";
const PusherContext = React.createContext<Pusher | undefined>(undefined);
export const usePusher = () => useContext(PusherContext)
export const PusherProvider: React.FC = (props) => {
const pusherRef = useRef<Pusher>();
useEffect(() => {
pusherRef.current = new Pusher(PUSHER_APP_KEY, {
cluster: 'eu'
})
return () => pusherRef.current?.disconnect()
}, [pusherRef]);
return (
<PusherContext.Provider value={pusherRef.current}>
{props.children}
</PusherContext.Provider>
)
}
The problem is that the provider always has an undefined value on page refresh/load. But when I trigger a re-render the value is correctly set. I would like to have the instance without the need of re-rendering.
Why is this happening?
I believe you can use the next construction:
export const PusherProvider = (props) => {
const pusher = useMemo(() => new Pusher(APP_PUSHER_KEY, { cluster: 'eu' }), [])
useEffect(() => () => pusher.disconnect(), [pusher])
return <PusherContext.Provider value={pusher}>{props.children}</PusherContext.Provider>
}
I have solved this issue by following way.
If you set State/const data inside "useEffect" will not work. as that will not run when page refresh but the state declaration those are outside the "useEffect" will run. Hence it will reset default values.
So I resolved by setting the state/const value outside of "useEffect" and done.

Resources