I have 2 React context providers, both of which contain actions functions that update their state. I need to call one of these action function from the parent react context, but the context is undefined because it's not wrapped by the provider.
I cannot change the order of the wrapped providers. I also cannot put all the code inside the same provider (the code would be too large and not separated by concern).
import React, {useContext, createContext} from 'react'
const AAAAContext = createContext()
const BBBBContext = createContext()
function AAAAProvider(props) {
const bbbbContext = useContext(BBBBContext) // this is always undefined because not wrapped in BBBBProvider
const aaaaActions = {
doSomething: () => {
bbbbContext.bbbbActions.doSomething()
}
}
const aaaaContext = {aaaaActions}
return <AAAAContext.Provider value={aaaaContext}>{props.children}</AAAAContext.Provider>
}
function BBBBProvider(props) {
const aaaaContext = useContext(AAAAContext)
const bbbbActions = {
doSomething: () => {
aaaaContext.aaaaActions.doSomething()
}
}
const bbbbContext = {bbbbActions}
return <BBBBContext.Provider value={bbbbContext}>{props.children}</BBBBContext.Provider>
}
function App() {
return (
<AAAAProvider>
<BBBBProvider>
Hello
</BBBBProvider>
</AAAAProvider>
)
}
One possible way is to store child context in the parent context, and every time the child context changes in useEffect, call a function in the parent to update the child context in the parent context.
import React, {useContext, createContext, useState, useEffect} from 'react'
const AAAAContext = createContext()
const BBBBContext = createContext()
function AAAAProvider(props) {
const [bbbbActions, setBBBBActions] = useState()
const aaaaActions = {
doSomething: () => {
bbbbActions.doSomething()
},
setBBBBActions
}
const aaaaContext = {aaaaActions}
return <AAAAContext.Provider value={aaaaContext}>{props.children}</AAAAContext.Provider>
}
function BBBBProvider(props) {
const aaaaContext = useContext(AAAAContext)
const bbbbActions = {
doSomething: () => {
console.log('bbbbActions.doSomething')
}
}
const bbbbContext = {bbbbActions}
useEffect(() => {
aaaaContext.aaaaActions.setBBBBActions(bbbbActions)
}, [])
return <BBBBContext.Provider value={bbbbContext}>{props.children}</BBBBContext.Provider>
}
function Component() {
const aaaaContext = useContext(AAAAContext)
window.aaaaContext = aaaaContext
return 'Hello'
}
function App() {
const aaaaContext = useContext(AAAAContext)
window.aaaaContext = aaaaContext
return (
<AAAAProvider>
<BBBBProvider>
<Component/>
</BBBBProvider>
</AAAAProvider>
)
}
export default App
Related
I have a component A
export default const A = () => {
const GetItemX = () => {
alert("Devine calling);
}
const GetItemY = () => {
alert("Another function in this component");
}
}
And another component B
export default const B = () => {
const callGetItemX = getItemX();
}
}
How would I get the function getItemX to work inside component B?
I'm using next with typescript and trying to load a webworker.
For some reason - when I'm trying to initialize the webworker when I'm creating a hook - next tells me that Worker is not defined.
I'm using comlink but I suspect it's an issue with Next since comlink isn't in the mix at the point of error.
You can find the problematic line in below code snippet useWorker.hooks.ts in the function makeWorkerApiAndCleanup
Does anyone understand why this is happening?
const worker = new Worker(url);
useWorker.hooks.ts
import { wrap, releaseProxy } from "comlink";
import { useEffect, useState, useMemo } from "react";
function makeWorkerApiAndCleanup() {
const url = new URL("./useWorker.worker", import.meta.url)
const worker = new Worker(url);
const workerApi = wrap<import("./useWorker.worker").WorkerType>(worker);
const cleanup = () => {
workerApi[releaseProxy]();
worker.terminate();
};
const workerApiAndCleanup = { workerApi, cleanup };
return workerApiAndCleanup;
}
function useWorker() {
const workerApiAndCleanup = useMemo(() => makeWorkerApiAndCleanup(), []);
useEffect(() => {
const { cleanup } = workerApiAndCleanup;
return () => {
cleanup();
};
}, [workerApiAndCleanup]);
return workerApiAndCleanup;
}
function useImplementation() {
const [data, setData] = useState({});
const { workerApi } = useWorker();
useEffect(() => {
workerApi.workerFunction();
setData({});
}, [workerApi]);
return data;
}
export default useImplementation;
useWorker.worker.ts
import { expose } from 'comlink';
import { workerFunction } from './functions';
const worker = {
workerFunction,
};
export type WorkerType = typeof worker;
expose(worker);
useWorker/index.ts
import useWorker from './useWorker.hooks';
export { useWorker };
I have created one wrapper component where I put my react context.
Inside that wrapper component I have used useEffect() hook where I fetch values from api and try to update context default values.
In my child component I tried to fetch context values but only default value of that context is fetched. So it seems that useEffect hook didnt updated my context object.
Here is wrapper component:
export const CorporateWrapper = ({ apiBaseUrl, children }) => {
const [corporateContextDefaults, setCorporateContextDefaults] = useState({});
useEffect(() => {
(async () => {
try {
const json = await fetchCorporateUserDetails(apiBaseUrl, getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log(json.message);
return {
notFound: true,
};
}
console.log(json.data);
setCorporateContextDefaults({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
} catch (e) {
console.log(e.message);
}
})();
}, []);
return (
<CorporateProvider value={corporateContextDefaults}>
{children}
</CorporateProvider>
);
};
Here is CorporateProvider component:
import React, { useState, useContext } from "react";
const CorporateContext = React.createContext({corporateId: null, corporateRole: null,
corporateAdmin: null, corporateSuperAdmin: null});
const UpdateCorporateContext = React.createContext({});
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const useUpdateCorporateContext = () => {
return useContext(UpdateCorporateContext);
};
export const CorporateProvider = ({ value, children }) => {
const [details, setDetails] = useState(value);
return (
<CorporateContext.Provider value={details}>
<UpdateCorporateContext.Provider value={setDetails}>
{children}
</UpdateCorporateContext.Provider>
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Here is how I try to fetch context value from child component which is wrapped under wrapper component:
const { corporateId } = useCorporateContext();
I'm writing a test code with Jest for a custom hook in my web application.
It uses Recoil for state management, but the error message appears when I run npm run test.
This is the error message.
This component must be used inside a <RecoilRoot> component.
16 | const useIds = () => {
17 | // const [ids, setIds] = React.useState([]);
> 18 | const [ids, setIds] = useRecoilState(idsState);
| ^
This is the test code.
import * as React from 'react';
import { render, fireEvent } from '#testing-library/react';
import { useIds } from '#/hooks/useIds';
import { RecoilRoot } from 'recoil';
it('unit test for custom hook useIds', () => {
const TestComponent: React.FC = () => {
const ids = useIds();
return (
<RecoilRoot>
<div title='ids'>{ ids }</div>
</RecoilRoot>
)
}
const { getByTitle } = render(<TestComponent />);
const ids = getByTitle('ids');
})
This is the custom hook code
import * as React from 'react';
import { useRouter } from 'next/router';
import { atom, useRecoilState } from 'recoil';
import { fetchIdsByType } from '#/repositories';
const initialState: {
[type: string]: number[];
} = {};
export const idsState = atom({
key: 'idsState',
default: initialState,
});
const useIds = () => {
const [ids, setIds] = useRecoilState(idsState);
const router = useRouter();
const { type } = router.query;
React.useEffect(() => {
if (router.asPath !== router.route) {
// #ts-ignore
fetchIdsByType(type).then((ids: number[]) => {
setIds((prevState) => {
return {
...prevState,
// #ts-ignore
[type]: ids,
};
});
});
}
}, [router]);
// #ts-ignore
return ids[type];
};
export { useIds };
I know why the error is happening but I have no idea where the RecoilRoot should be in?
You might need to put where to wrap the component which is using your custom hook as following:
it('unit test for custom hook useIds', () => {
const TestComponent: React.FC = () => {
const ids = useIds();
return (
<div title='ids'>{ ids }</div>
)
}
const { getByTitle } = render(
// Put it here to wrap your custom hook
<RecoilRoot>
<TestComponent />
</RecoilRoot>
);
const ids = getByTitle('ids');
})
I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}