Passing data between react context - javascript

I'm having troubles passing data from one context to the other in React. I have some job data that is received from a SignalR connection and I need to pass it to a specific job context, but I'm not sure how to do this.
I have the following code:
SignalRContext
export interface SignalRContextProps {
connect: () => void;
}
export const SignalRContext = createContext<SignalRContextProps>(null!);
export const useSignalRContext = (): SignalRContextProps => {
const {onProgressReceived} = useContext(JobsContext);
const connect = () => {
//Removed a lot of connection setup code for readability
const connection = new HubConnectionBuilder().build();
connection.on('JobReportProgress', onProgressReceived);
connection.start();
};
return {
connect,
};
};
SignalRContextProvider
type Props = {
children: ReactElement | ReactElement[];
}
export const SignalRContextProvider = (props: Props) => {
const {children} = props;
const signalRContext = useSignalRContext();
return (
<SignalRContext.Provider value={signalRContext}>
{children}
</SignalRContext.Provider>
);
};
JobsContext
export const JobsContext = createContext<JobsContextProps>(null!);
export const useJobsContext = (): JobsContextProps => {
const [jobs, setJobs] = useState<Job[]>([]);
const load = async (): Promise<void> => {
const jobs = await getAllJobs();
setJobs(jobs);
};
const onProgressReceived = (progress: JobProgress) => {
console.log(jobs);
const currentJob = jobs.find((job) => job.id === progress.id);
console.log(currentJob); //currentJob will always be empty because jobs array is NULL on receiving progress.
}
};
return {
load, onProgressReceived, jobs
};
};
JobsContextProvider
interface Props {
children: ReactElement | ReactElement[];
}
export const JobsContextProvider = (props: Props): ReactElement => {
const {children} = props;
const jobsContext = useJobsContext();
return (
<JobsContext.Provider value={jobsContext}>
{children}
</JobsContext.Provider>
);
};
index
ReactDOM.render(
<JobsContextProvider>
<SignalRContextProvider>
<App />
</SignalRContextProvider>
</JobsContextProvider>,
document.getElementById('root'),
);
Flow
In my app.tsx I start the SignalR connection by calling signalRContext.connect() created by const signalRContext = useContext(SignalRContext);
I go to my job page where my 6 jobs are loaded from the backend via my context
const {load} = useContext(JobsContext);
await load();
I trigger a job and I see that the SignalR context is calling onProgressReceived on the jobContext. But for some reason the jobs array is empty so I can't update the correct job. Is seems that a new context is created instead of reusing the existing context.
Anyone has an idea how I can make my SignalRContext pass data to my JobContext? Or maybe there is a better system that using context for this?
UPDATE 1:
I have the feeling that there is something strange going on with the HubConnection instance. When I register my 'onProgressReceived' function as a callback function on the 'JobReportProgress' event then it doesn't work. But when I first save the progress with setState and trigger the 'onProgressReceived' function with useEffect it seems to be working. Small example:
const {jobs, onProgressReceived} = useContext(JobsContext);
const [progress, setProgress] = useState<JobProgress | null>(null);
//Change the connection.on line with the following
connection.on('JobReportProgress', onReceived);
//And then we trigger the function on change
const onReceived = (progress: JobProgress) => {
setProgress(progress);
};
useEffect(() => {
if (progress !== null) {
onProgressReceived(progress);
}
}, [progress]);
This seems to be working, but not sure why I first need to save my progress with useState.

I had very same issue a while ago. I even asked question about it Here
There are ways to connect nested context to each other, but then code gets messy.
At first I used event listeners and was dispatching events when some data received, but didn't worked well.
Then I tried passing callbacks in context value. Pass callback from JobsContextProvider ex: onSignalData and call this callback when data is updated in SignalRContextProvider and save in JobsContextProvider.
Combined these 2 contexts into one and it worked well, but didn't liked it as well. Code got messy.
Then I give up using contexts in this type of data and used redux with Redux Toolkit and RTK Query. It works very vell, code is well organized and this is the best solution I have found so far.
Let me know in comments which solution works best for you and I can write exact pseudocode for that solution.

Related

create a component for Axios that can be used in different places with different values

How can I create a component for Axios that I can use in different places with different values ​​??
I got stuck, what should I do?
This is what I have achieved so far
thank you for Helping
import axios from "axios";
const Axios = (props) => {
const [posttitle, postbody] = useState([]);
const [postuserid, postid] = useState([]);
const fetchData = () => {
const { postbodyapi } = props.postbodyapi;
const postuseridapi = "https://nba-players.herokuapp.com/players/james/lebron";
const getbody = axios.get(postbodyapi);
const getuseid = axios.get(postuseridapi);
axios.all([getbody, getuseid]).then(axios.spread((...allData) => {
const databody = allData[0].data.first_name;
const datauseid = allData[1].config.url;
postbody(databody);
postid(datauseid);
}))
}
useEffect(() => {
fetchData()
}, [])
return (
<div className="App">
{posttitle}
<img src={postuserid} alt="asd"/>
</div>
);
}
export default Axios;
You should create a custom hook.
Create a hook called for example useAxios and hold only the fetching method inside of it, and the return state from that hook should be just data.
you can make it so it takes params like "URL, data, method", or make a few smaller hooks like useAxiosGet, useAxiosPost.
If you make a few smaller it will be easier to read and change something if needed.
Here is how I did it, an example of one specific Axios custom hook, use this for example to see how to build it.
useGetCar.js // custom axsios hook
import axios from 'axios';
const useGetCar = async (url, id) => {
const result = await axios.post(url, {id: id});
return result.data[0];
}
export default useGetCar
car.js // page component that displays data
import useGetCar from "#hooks/useGetCar";
let car_id = 1; // some that i send to api
// this function here is not exact from my code,
//but I just wanted to provide you an example.
// I didn't include my original code because it is
//from next.js app and I don't want to confuse u with that
async function getData() {
let car = await useGetCar(`http://localhost/get_car.php`, car_id);
return car;
}
Hope you understood what I'm saying, and I did not confuse you.
Feel free to ask anything if you don't understand something clearly.
Happy coding.

ReactJS Hooks dependency an async operation

So I have a hook that on mount, reads data from an indexedDB and stores that data in its internal state
The problem that I have, is that the indexedDB data gets changed from another component (added/removed), and I need to react to those changes in this hook. I'm not 100% familiar with hooks and how this would be done, but the first thought would be to have as hook dependency the value from the indexedDB.
HOWEVER, the reading of the data from the indexedDB is an async operation and the hook dependency would be a.. promise.
So basically, the flow is as follows:
Component 1 calls the hook like so:
const eventItems = useEventListItems({
sortBy,
sortGroupedBy,
eventTimestamp,
events,
assets,
touchedEventIds,
unsyncedEvents, // <--- this is the one that we need
order,
});
The useEventListItems hook, on mount, reads the data from the indexed DB, stores it in its internal state and returns it:
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [setUnsyncedEvents]);
where readUnsyncedEvents is:
export const readUnsyncedEvents = <T>(type: Type): Promise<DebriefStoreEntry<T>> =>
debriefStore
.get(type)
.then((entry) => entry && { data: entry.data, timestamp: entry.timestamp });
The unsyncedEvents from the indexedDB are then changed from another component.
What should happen now, is that the useEventListItems hook should listen to the changes in the IDB and update the unsyncedEvents in its internal state, passing them to the component that uses this hook. How would I achieve this?
My first thought was to have something like this in the useEventListItems hook:
useEffect(() => {
setUnsyncedEvents(..newValueFromIdb);
}, [ await readUnsyncedEvents()]);
but that won't work since it'll be a promise. Is there anyway I can have as hook dependency, a value returned by an async operation?
You can use Context API to refetch the data from IDB.
The idea here is to create a context with a counter variable which will be updated after each IDB update operation. And useEventListItems hook will read that counter variable from context and trigger the useEffect hook.
export const IDBContext = React.createContext({
readFromIDB: null,
setReadFromIDB: () => {}
});
export const IDBContextProvider = ({ children }) => {
const [readFromIDB, setReadFromIDB] = useState(0);
return (
<IDBContext.Provider value={{ readFromIDB, setReadFromIDB }}>
{children}
</IDBContext.Provider>
);
};
This is how your useEventListItems will look like.
const { readUnsyncedEvents } = useDebriefStore();
const [unsyncedEvents, setUnsyncedEvents] = useState<number[]>([]);
const {readFromIDB} = useContext(IDBContext); // this variable will be updated after each IDB update.
useEffectAsync(async () => {
const storedUnsyncedEventIds = await readUnsyncedEvents<number[]>();
if (storedUnsyncedEventIds?.data) {
setUnsyncedEvents(storedUnsyncedEventIds.data);
}
}, [readFromIDB,setUnsyncedEvents]); // added that to dependency array to trigger the hook on value change.
And here are the components:
const IDBUpdateComponent = ()=>{
const {readFromIDB,setReadFromIDB} = useContext(IDBContext);
const updateIDB = ()=>{
someIDBUpdateOpetation().then(res=>{
setReadFromIDB(readFromIDB+1) // update the context after IDB update is successful.
}).catch(e=>{})
}
return(
<div>IDBUpdateComponent</div>
);
}
const IDBConsumerComponent = ()=>{
return (
<div>IDBConsumerComponent</div>
)
}
Just make sure that both the components are wrapped inside the context so that they can access the values.
const App = ()=>{
return(
<div>
<IDBContextProvider>
<IDBUpdateComponent />
<IDBConsumerComponent />
</IDBContextProvider>
</div>
)
}

How to call a react hook fetch request in a functional component to access data then pass to a class component to map?

After a huge amount of trial and error for a complex webGL project I have landed on a solution that will reduce the amount of re-engineering working, threejs code (from another developer) and, as this project is extremely time restrained, reduce the amount of time needed. It's also worth noting my experience of this is limited and I am the only developer left on the team.
The project current accepts a large array of random user data, which is exported from a js file and then consumed here...
import Users from "./data/data-users";
class UsersManager {
constructor() {
this.mapUserCountries = {};
}
init() {
Users.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
Here is my fetch request..
import { useState, useEffect } from "react";
const FetchUsers = () => {
const [hasError, setErrors] = useState(false);
const [users, setUsers] = useState({});
async function fetchData() {
const res = await fetch(
"https://swapi.co/api/planets/4/"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
}, []);
return JSON.stringify(users);
};
export default FetchUsers;
I have run into lots of issues as the UserManager is a class component and if I import my fetchUsers into this file, call it and save it to a variable like so const Users = fetchUsers(); it violates hooks.
I want to be able to return a function that will return my users from the database as an array.
That will then be able to be passed into the UserManager in the same way the hard coded data is and mapped over to be actioned by LOTS of other files.
I've mocked up a small codesandbox with what the flow would be ideally but I know I need a solution outside of hooks...
https://codesandbox.io/s/funny-borg-u2yl6
thanks
--- EDIT ---
import usersP from "./data/data-users";
class UsersManager {
constructor() {
this.mapUserCountries = {};
this.state = {
users: undefined
};
}
init() {
usersP.then(users => {
this.setState({ users });
});
console.log(usersP);
this.state.users.forEach(user => {
const c = user.country;
if (!this.mapUserCountries[c])
this.mapUserCountries[c] = { nbUsers: 0, users: [] };
this.mapUserCountries[c].nbUsers++;
this.mapUserCountries[c].users.push(user);
});
}
getUsersPerCountry(country) {
return this.mapUserCountries[country];
}
}
export default new UsersManager();
console.log (UsersManager.js:16 Uncaught TypeError: Cannot read property 'forEach' of undefined
at UsersManager.init (UsersManager.js:16)
at Loader.SceneApp.onLoadingComplete [as callback] (App.js:39)
at Loader.onAssetLoaded (index.js:20)
at index.js:36
at three.module.js:36226
at HTMLImageElement.onImageLoad)
I fixed your sandbox example.
You cannot load the users synchronously (using import) as you need to make a http call to fetch the users so it's asynchronous.
As a result you can fetch the users inside the componentDidMount lifecycle method and use a state variable to store them once they are fetched
There are a couple guidelines that will help separate functions that are Hooks and functions that are Components (these are true most of the time):
1 Component functions use pascal case (start with a capital letter) and always return JSX.
2 Custom Hooks functions conventionally begin with the word "use" and never return JSX.
In your case you probably want to make a custom Hooks function that must be called in a component;
function useUserData() {
const [hasError, setErrors] = useState(false);
const [users, setUsers] = useState({});
const networkCall = useCallback(async fetchData = () => {
const res = await fetch(
"https://swapi.co/api/planets/4/"
);
res
.json()
.then(res => setUsers(res))
.catch(err => setErrors(err));
} , [])
useEffect(() => {
fetchData();
}, []);
return {users, hasError};
}
Then call that custom hook in one of your components:
function App() {
const {users, hasError} = useUserData();
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>{users}</div>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
}
If you then need to share that fetched data throughout your app, you can pass it down via props or the context API: https://reactjs.org/docs/context.html
(post a message if you'd like an example of this).

How can I use a provider value to useMutation and also dispatch the changes to state afterwards?

I would like to use a Context.Provider value to handle both mutating and dispatching similar changes. I have read about React-Apollo's onComplete method, but I'm not sure which approach will cover more cases where I need to both mutate and dispatch state. Here's what I have:
const CartContext = React.createContext<{
state: State
dispatch: Dispatch<AnyAction>
cartApi: any
}>({ state: initialState, dispatch: () => null, cartApi: mutateUserProductsAndUpdateCart })
function CartProvider({ children }: { children?: React.ReactNode }) {
const [state, dispatch] = useReducer<Reducer<State, AnyAction>>(reducer, initialState)
// feel like i need to do something with the hook here to avoid invariant violation side effects
const [updateUserProducts] = useUpdateUserProducts()
return (
<CartContext.Provider value={{ state, dispatch, cartApi: mutateUserProductsAndUpdateCart}}>
{children}
</CartContext.Provider>
)
}
export const useCartState = () => useContext(CartContext)
And here's what I would like to do with my mutateUserProductsAndUpdateCart:
const mutateUserProductsAndUpdateCart = async (_mutation: any, _mutationParams: any, _dispatchObject: AnyObject) => {
// apollo mutation
const updateUserProductsResult = await updateUserProducts(_mutationParams)
if (updateUserProductsResult.error) throw Error("wtf")
// useReducer dispatch
dispatch(_dispatchObject)
return
}
and here is how I would like to access this on another component:
const { cartApi } = useCartState()
const addProductToCart = async () => {
const result = await cartApi({
mutation,
mutationVariables,
dispatchObject})
}
I feel like this article is sort of the direction I should be taking, but I'm very lost on implementation here. Thanks for reading.
I'm not sure this directly answers your question, but have you considered just using Apollo Client? It looks like you are trying to do two things:
Save items added to the cart to the server
Update the cart locally in the cache
It seems like you could skip creating your own context altogether and just create a hook for mutating (saving your cart items) and then update your local cache for cart items. Something like this:
import gql from 'graphql-tag';
import useMutation from '#apollo/client';
export const mutation = gql`
mutation($items: [CartItem]!) {
saveCartItems(items: $items) {
id
_list_of_properties_for_cache_update_
}
}
`;
export const useSaveCartItems = mutationProps => {
const [saveCartItems, result] = useMutation(
mutation,
mutationProps
);
return [
items => {
saveCartItems({
update: (cache, { data }) => {
const query = getCartQuery; // Some query to get the cart items from the cache
const results = cache.readQuery({ query });
// We need to add new items only; existing items will auto-merge
// Get list of new items from results
const data = []; // List of new items; IMPLEMENT ME!!!
cache.writeQuery({ query, data });
},
variables: { items },
});
},
result,
];
};
Then in your useCartState hook you can just query the local cache for the items using the same query you used for the update and return that. By using the update function you can fix your local cache and anybody can access it from anywhere, just use the hook. I know that isn't exactly what you asked for, but I hope it helps.
Apollo client documentation on handling this may be found here.

Context API updating state causes looped requests

I have some context. I store there user roles.
const RolesContext = createContext({roles: []});
function RolesContextProvider({children}) {
const [roles, setRoles] = useState([]);
async function check(newRoles) {
const missing = compareArrayWithArray(newRoles, roles);
if (missing.length !== 0) {
await User.roles(newRoles).then(((res) => {
const updatedRoles = roles.concat(res.data);
setRoles(updatedRoles);
}));
}
}
const defaultContext = {
roles,
check,
};
return (
<RolesContext.Provider value={defaultContext}>
{children}
</RolesContext.Provider>
);
}
export {RolesContext, RolesContextProvider};
When initiating component I run check roles
export default function Users() {
const UsersComposition = compose(
connect(mapStateToProps, mapDispatchToProps)
)(ConnectedUsers);
const context = useContext(RolesContext);
const {roles, check} = context;
useEffect(() => {
check(['roles', 'to', 'check']);
}, [check]);
return <UsersComposition roles={roles}/>;
};
What it does...App is crashing due to inifite update state loop. It makes dozens of same requests with same payload. How it should be done? Thanks for suggestions.
In order to fix the infinite loop you'll need to preserve check function's identity between renders. One way to do this is to save it with useRef (you'll need to pass existing roles as the second parameter):
const check = useRef(async (newRoles, roles) => {
...
});
const defaultContext = {
roles,
check: check.current,
};
You may also consider implementing Users as a class component and call check in componentDidMount:
class Users extends React.Component {
static contextType = RolesContext;
componentDidMount() {
this.context.check(['roles', 'to', 'check']);
}
render() {
...
}
}
It seems like you're calling check in a loop. See if I'm right.
RolesContextProvider provides the context roles and check
Users access the context and calls check
Check updates roles in RolesContextProvider with setRoles
setRoles triggers update in RolesContextProvider which changes the context
And the context change updates Users
And the cycle repeats

Categories