Context API updating state causes looped requests - javascript

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

Related

Passing data between react context

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.

useMemo for efficient global data availability using reactJS and recoilJS

I am trying to figure out how to solve the following problem in the best way possible:
I have multiple components all requiring a global state (I am using recoil for this, since I have many different "atom" states).
Only if a component gets loaded that requires that state, it will perform an expensive computation that fetches the data. This should happen only once upon initialisation. Other components that require the same piece of data should not re-trigger the data fetching, unless they explicitly call an updateState function.
Ideally, my implementation would look something like this:
const initialState = {
uri: '',
balance: '0',
};
const fetchExpensiveState = () => {
uri: await fetchURI(),
balance: await fetchBalance(),
});
const MyExpensiveData = atom({
key: 'expensiveData',
default: initialState,
updateState: fetchExpensiveState,
});
function Component1() {
const data = useRecoilMemo(MyExpensiveData); // triggers `fetchExpensiveState` upon first call
return ...
}
function Component2() {
const data = useRecoilMemo(MyExpensiveData); // doesn't trigger `fetchExpensiveState` a second time
return ...
}
I could solve this by using useRecoilState and additional variables in the context that tell me whether this has been initialised already, like so:
export function useExpensiveState() {
const [context, setContext] = useRecoilState(MyExpensiveData);
const updateState = useCallback(async () => {
setContext({...fetchExpensiveState(), loaded: true});
}, []);
useEffect(() => {
if (!context.loaded) {
setContext({...context, loaded: true});
updateState();
}
}, []);
return { ...context, updateState };
}
It would be possible to make this solution more elegant (not mixing loaded with the state for example). Although, because this should be imo essential and basic, it seems as though I'm missing some solution that I haven't come across yet.
I solved it first by using a loaded and loading state using more useRecoilStates. However, when mounting components, that had other components as children, that all used the same state, I realized that using recoil's state would not work, since the update is only performed on the next tick. Thus, I chose to use globally scoped dictionaries instead (which might not look pretty, but works perfectly fine for this use case).
Full code, in case anyone stumbles upon a problem like this.
useContractState.js
import { useWeb3React } from '#web3-react/core';
import { useEffect, useState } from 'react';
import { atomFamily, useRecoilState } from 'recoil';
const contractState = atomFamily({
key: 'ContractState',
default: {},
});
var contractStateInitialized = {};
var contractStateLoading = {};
export function useContractState(key, fetchState, initialState, initializer) {
const [state, setState] = useRecoilState(contractState(key));
const [componentDidMount, setComponentMounting] = useState(false);
const { library } = useWeb3React();
const provider = library?.provider;
const updateState = () => {
fetchState()
.then(setState)
.then(() => {
contractStateInitialized[key] = true;
contractStateLoading[key] = false;
});
};
useEffect(() => {
// ensures that this will only be called once per init or per provider update
// doesn't re-trigger when a component re-mounts
if (provider != undefined && !contractStateLoading[key] && (componentDidMount || !contractStateInitialized[key])) {
console.log('running expensive fetch:', key);
contractStateLoading[key] = true;
if (initializer != undefined) initializer();
updateState();
setComponentMounting(true);
}
}, [provider]);
if (!contractStateInitialized[key] && initialState != undefined) return [initialState, updateState];
return [state, updateState];
}
useSerumContext.js
import { useSerumContract } from '../lib/ContractConnector';
import { useContractState } from './useContractState';
export function useSerumContext() {
const { contract } = useSerumContract();
const fetchState = async () => ({
owner: await contract.owner(),
claimActive: await contract.claimActive(),
});
return useContractState('serumContext', fetchState);
}
The reason why I have so many extra checks is that I don't want to re-fetch the state when the component re-mounts, but the state has already been initialised. The state should however subscribe to updates on provider changes and re-fetch if it has changed.

how to save array with useEffect React Native

I want save array data using react useEffect. Follow Example with class:
async componentDidMount() {
const users = await AsyncStorage.getItem('users');
if (users) {
this.setState({ users: JSON.parse(users) });
}
}
componentDidUpdate(_, prevState) {
const { users } = this.state;
if (prevState.users !== users) {
AsyncStorage.setItem('users', JSON.stringify(users));
}
}
how to implement the logic with React Hooks?
For componentDidMount logic you can use useEffect hook:
useEffect(() => {
const asyncFetch = async () => {
const users = await AsyncStorage.getItem("users");
if (users) {
// setter from useState
setUsers(JSON.parse(users));
}
};
asyncFetch();
}, []);
For componentDidMount use useEffect with dep array and useRef reference.
const prevUsers = useRef();
useEffect(() => {
const prevUsers = prevUsers.current;
// Some equal check function
if (!areEqual(prevUsers, users)) {
AsyncStorage.setItem("users", JSON.stringify(users));
}
prevUsers.current = users;
}, [users]);
Notice that in your current code, prevState.users !== users is always truley, you comparing two objects and in JS {} !== {} always results true.
You can try like below and you can use hooks in functional based component not class based component
//state declaration similar to class based component
const [usersdata,setUsers] = useState([]);
const users = await JSON.parse(AsyncStorage.getItem('users'));
//whenever the value of users changes useEffect will reset the value of users in state useEffect handle the lifecycle in function based component
useEffect(()=>{
if(users){
setUsers(JSON.parse(users));
}
},[users])
For hooks the logic changes slightly, you would have to "hook" your effect with a state in order to update the component, so the component would update (componentDidUpdate) when the hooked state has been updated, you can obviously hook multiple states.
If you choose to not hook any state, the effect would execute only at the mounting of the component just like (componentDidMount())
I don't see the logic that makes you decide when to update the user state since you always get it from the storage, so I will assume that you have some kind of a trigger that makes you verify if the users value has changed in the storage.
so you can refactor your code like this:
const [users, setUsers] = useState([]);
const [userHasChanged, setUserHasChanged] = useState(false);
usEffect(async () => {
// comparing the old users with the new users is not useful since you always fetch the users from the storage, so the optimal is to always set the new array/ object to users, this way you avoid comparing the two objects which is a bit costly.
const newUsers = await AsyncStorage.getItem("users");
setUsers(JSON.parse(newUsers));
setUserHasChanged(false);
}, [userHasChanged])
// some code that triggers userHasChanged, you use setUserHasChaned(true)

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.

Categories