React JS: Get Context Data After getting success on API Call - reactjs

I am stuck at getting context data.
I have a context and a component which uses its data.
I need to get the updated data of context's variable on API call success in my component.
so How can I do that ?
Here what I have tried.
context.js
import React, { useState, createContext,useEffect } from 'react';
import {getData} from './actionMethods';
const NewContext = createContext();
function newContextProvider(props) {
const [dataValue, setData] = useState([])
useEffect(() => {
const fetchMyData = async () => {
const dataValue = await getData(); // this is an API call
setData(dataValue)
};
fetchMyData();
}, []);
return (
<NewContext.Provider
value={{
state: {
dataValue
},
actions: {
}
}}
>
{props.children}
</NewContext.Provider>
);
}
const newContextConsumer = newContext.Consumer;
export { newContextProvider, newContextConsumer, newGridContext };
myComponent.js
import React, { useState, useContext } from 'react'
import context from './context'
import deleteAPI from './actionMethods'
function myComponent(props) {
const id= 10
const {state,actions} = useContext(context)
deleteAPI(id).then(res => {
if (res){
// what should I write here to get the updated Data from the context which will call an API to get the updated data.
}
})
}
Any help would be great.
Thank You.

As a generic example, one option is to fetch the data from the server when the app loads in the front-end. From there you can send requests to modify the server data and at the same time update your local version. Something like:
Fetch data and save it to the local store: [{id: 0, name: 'first'},{id: 1, name: 'second'}]
Modify the data sending a request to the server. For example deleting an item. id: 0
Once the server responds confirming the operation was successful you can modify that data in the local store. [{id: 1, name: 'second'}]
You can handle the data using a Redux store or a React Context. For example, using a Context:
export const ItemsContext = createContext([]);
export const ItemsContextProvider = props => {
const [items, setItems] = useState([]);
const deleteItem = id => {
deleteItemsAxios(id).then(() => {
setItems(items => items.filter(item => item.id !== id));
});
};
useEffect(() => {
const fetchItems = async () => {
const items_fetched = await fetchItemsAxios();
if (items_fetched) {
setItems(items_fetched);
} else {
// Something went wrong
}
};
fetchItems();
}, []);
return (
<ItemsContext.Provider
value={{
items,
deleteItem
}}
>
{props.children}
</ItemsContext.Provider>
);
};
We define a Component that will manage the data fetch. The data items are inside a state. When the Component mounts we fetch the items and save them in the state. If we want to delete an item we first call the corresponding fetch function. Once it finishes, if it was successful, we update the state and remove that item. We use React Context to pass the items data, as well as the deleteItem function, to any component that needs them.
Let me know if you need more explanation.

Related

Why component is not refreshing in react hooks?

I tried to dispatch the API call using redux in useEffect hooks. After the response came to redux-saga response goes to reducer and the reducer updated the state successfully but my component is not refreshing.
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import SubscriptionComponent from '../../Components/Subscription/Subscription';
import SubscriptionActions from '../../Redux/Subscription/Actions';
import {
getMySubscriptions,
getMySubscriptionByName,
getMySubscriptionByGroup,
} from '../../Redux/Subscription/Selectors';
const Subscription = (props) => {
const { navigation } = { ...props };
const [visible, setVisible] = useState(false);
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const dispatch = useDispatch();
const [data, setData] = useState(subscriptionList);
const payload = {
memberId: '604f2ad047bc495a0a7fad26',
vendorId: '5fd484c39590020dc0dfb82a',
vendorOrgId: '5fd484439590020dc0dfb829',
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
const onHandleSubscriptionByName = () => {
setVisible(false);
const subscription = getMySubscriptionByName(data);
setData(subscription);
};
const onHandleSubscriptionByGroup = () => {
setVisible(false);
const subscription = getMySubscriptionByGroup(data);
setData(subscription);
};
return (
<SubscriptionComponent
list={data}
navigation={navigation}
onPressList={(val) =>
navigation.navigate('SubscriptionDetails', { _id: val._id, name: val.name })
}
visible={visible}
openMenu={() => setVisible(!visible)}
closeMenu={() => setVisible(!visible)}
sortByName={() => onHandleSubscriptionByName()}
sortBySub={() => onHandleSubscriptionByGroup()}
/>
);
};
export default Subscription;
used reselect to get the state from redux.
export const getMySubscriptions = createSelector(mySubscriptionSelector, (x) => {
const mySubscriptions = x.map((item) => {
return {
_id: item._id,
image: 'item.image,
description: item.description,
name: item.name,
subscriptionGroup: item.subscriptionGroup,
subscriptionAmount: item.subscriptionAmount,
status: item.status,
delivery: item.delivery,
product: item.product,
};
});
return mySubscriptions ;
});
Why component is not refreshing.
Thanks!!!
You're storing the selection result in local state.
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const [data, setData] = useState(subscriptionList);
useState(subscriptionList) will only set data initially not on every update.
EDIT:
Your setup is a little odd:
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
Using data in the dependency array of useEffect, will cause refetching the data, whenever data is updated. Why? I looks like your sorting is working locally, so no need to refetch?
I would suggest to store the sort criteria (byName, byGroup) also in Redux and eliminate local component state, like that:
// ToDo: rewrite getMySubscriptions so that it considers sortCriteria from Redux State
const subscriptionList = useSelector(getMySubscriptions);
const dispatch = useDispatch();
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
// Empty dependency array, so we're only fetching data once when component is mounted
}, []);
const onHandleSubscriptionByName = () => {
dispatch(SubscriptionActions.setSortCriteria('byName'));
};
const onHandleSubscriptionByGroup = () => {
dispatch(SubscriptionActions.setSortCriteria('byGroup'));
};
As mentioned in the comments you will need to add a new action setSortCriteria and reducer to handle the sorting and adjust your selector, so that it filters the subscription list when a sortCriteria is active.
You do not update data after fetching new subscription.
const [data, setData] = useState(subscriptionList);
Only initializes data, but does not update it, you need to add useEffect to update data:
useEffect(() => {
setData(subscriptionList);
}, [JSON.stringify(subscriptionList)]);
JSON.stringify only used for deep compare complex objects, since useEffect only runs shallow compare and might miss, changes in objects.
-----EDIT------
Other problem might be that your getMySubscriptions function might need deep compare, since useSelector by itself doesn't do that, example might be:
import { useSelector, shallowEqual } from 'react-redux';
const subscriptionList = useSelector((state) => getMySubscriptions(state), shallowEqual);
Note that both solutions must be used.

Set default state after object has been loaded

I am using custom made useFirestore hook to retrieve file objects from my firestore collection.
import { useState, useEffect } from "react";
import { app } from "../firebase/firebase_storage";
const useFirestore = (collection) => {
const [songs, setSongs] = useState([]);
useEffect(() => {
const unsub = app
.firestore()
.collection(collection)
.onSnapshot((snap) => {
let songsArr = [];
snap.forEach((song) => {
songsArr.push({ ...song.data(), id: song.id });
});
setSongs(songsArr);
});
return () => unsub();
}, [collection]);
return { songs };
};
export default useFirestore;
Then I am storing this collection into a variable
const tracks = useFirestore("songs");
console.log(tracks) is giving
{songs: Array(2)}
I want to set the first track to be the default one which will be loaded into the waveform and I am using useState.
const [selectedTrack, setSelectedTrack] = useState(tracks.songs[0]);
This is not working because when the default state is set, the value I am passing is undefined because I am assuming it is not loaded. When I set some objects with placeholder data this works.
So my idea was that somehow postpone assigning default state until the object loads and it is not undefined.
Does anyone have an idea on how to achieve this?
You can use useEffect and set default state after tracks is initialized.
Try:-
const tracks = useFirestore("songs");
const [selectedTrack, setSelectedTrack] = useState('');
useEffect(() => {
setSelectedTrack(tracks.songs[0]);
}, [tracks])

React Table v7 not populating with data when using useState within the component

Edit: final solution at the bottom.
I am trying to build a simple web-app to display some data stored in firestore database, using React Table v7. I'm pretty new to React and javascript in general so forgive me if I use the wrong terminology.
At first I had put the function to fetch data inside App.jsx, passing it to state with setState within a useEffect hook, and it was working without an issue.
Then a colleague suggested that it is a good practice passing data to a component state instead of the app state, and that's where problems started to arise.
As of now, I cannot manage to populate the table. The header gets rendered but there's nothing else, and the only way I can make it show data is to make a small change in Table.jsx while npm start is running (such as adding or changing the output of any console.log) and saving the file. Only then, data is displayed.
I've been trying everything I could think of for about 2 days now (last thing I tried was wrapping Table.jsx into another component, but nothing changed).
I tried console.loging all the steps where data is involved to try and debug this, but I'm failing to understand where the problem is. Here's the output when the app loads first:
My code currently:
utility function to fetch data from Firestore
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
},
);
console.log('func output', dataArray);
return dataArray;
};
export default parseData;
Table.jsx
import { useTable } from 'react-table';
const Table = ({ columns, data }) => {
const tableInstance = useTable({ columns, data });
console.log('table component data received', data);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = tableInstance;
return (
// html boilerplate from https://react-table.tanstack.com/docs/quick-start#applying-the-table-instance-to-markup
);
};
export default Table;
TableContainer.jsx
import { useState, useEffect, useMemo } from 'react';
import parseData from '../utils';
import Table from './Table';
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
const getData = async () => {
const dataFromServer = await parseData(db);
setData(dataFromServer);
console.log('container-useEffect', dataFromServer);
};
getData();
}, [db]);
const columns = useMemo(() => [
{
Header: 'ID',
accessor: 'id',
},
// etc...
], []);
console.log('container data', data);
return (
<>
<Table columns={columns} data={data} />
</>
);
};
export default TableContainer;
App.jsx
import firebase from 'firebase/app';
import 'firebase/firestore';
import TableContainer from './components/TableContainer';
import Navbar from './components/Navbar';
// ############ INIT FIRESTORE DB
const firestoreCreds = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
};
if (!firebase.apps.length) {
firebase.initializeApp(firestoreCreds);
}
const db = firebase.firestore();
function App() {
return (
<div>
<Navbar title="This is madness" />
<div>
<TableContainer db={db} />
</div>
</div>
);
}
export default App;
Edit: in the end, using the suggestions below, this is the final solution I could come up with. I was annoyed by having to wrap the API call in an async func within useEffect, so this is what I did.
utility function to fetch data from Firestore
const parseData = (db) => {
const snapshot = db.collection('collection_name').get();
return snapshot.then(
(querySnapshot) => {
const dataArray = [];
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
return dataArray;
},
);
};
export default parseData;
TableContainer.jsx
Here I also added the flag didCancel within useEffect to avoid race conditions, according to this and this it seems to be a best practice.
// imports
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
let didCancel = false;
parseData(db)
.then((dataFromServer) => (!didCancel && setData(dataFromServer)));
return () => { didCancel = true; }
}, [db]);
// ...
In parseData function this line return dataArray; is execute before the snapshot is resolved. You need to change parseData and return a Promise and resolve when data is ready:
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
return new Promise(resolve => {
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
resolve(dataArray); //--> resolve when data is ready
},
);
})
};
In your TableContainer, you initialize data with an empty array. That's being sent along to Table until you finish getting data from your server. If you don't want that to happen, you should change your default (in useState) to something like false and explicitly handle that case (e.g. display "Please wait. Loading" if data === false).
The asynchronicity in the parseData function is plain wrong (and the implementation a smidge too complex...). Reimplement it something like this...
const parseData = async (db) => {
// Note `await` here.
const snapshot = await db.collection('collection-name').get();
return snapshot.map((doc) => ({ ...doc.data(), id: doc.id }));
};
export default parseData;

Data fetched with axios flickering on page load

I'm building out a new marketing website for my company in Gatsby, utilizing axios to fetch data from a REST api to dynamically display a list of dealers, and then dynamically displaying the contact information of the chosen dealer throughout the site. This is done by setting a cookie, which carries the ID of the dealer on the API, and then will fetch that dealer's info based on the cookie. However, I'm encountering an issue where the name of the dealer, which I'm currently displaying in the header, flickers on every page load. It looks bad, so I'm wondering if there is a way to either cache that data, or not force it to fetch on every page load and eliminate the flicker. I'm still in development, so I've got it staged on Netlify here, and you can take a look at the live version.
Here is my hook.
use-fetch.ts
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useFetch = (url: string) => {
const [status, setStatus] = useState('idle');
const [data, setData] = useState([]);
useEffect(() => {
if (!url) return;
const fetchData = async () => {
setStatus('fetching');
const result = await axios(url);
setData(result.data);
setStatus('fetched');
};
fetchData();
}, [url]);
return { status, data };
};
I'm then able to consume this in the pages like so:
const [query] = useState('https://jsonplaceholder.typicode.com/users/1/');
const url = query && 'https://jsonplaceholder.typicode.com/users/${cookie}';
const { data } = useFetch(url);
This sets an initial state users/1/ that will display the information for the first dealer unless a cookie is set.
I use this in a layout component, and I can pass the data prop down to my Header component.
app-layout.tsx
import React, { ReactNode, useEffect, useState } from 'react';
import Logo from '../../assets/svg/logo.svg';
import { Header } from '../header/Header';
import { Footer } from '../footer/Footer';
import { Devtools } from '../devtools/Devtools';
import s from './AppLayout.scss';
import { useCookie } from 'hooks/use-cookie';
import { useFetch } from 'hooks/use-fetch';
interface AppLayoutProps {
menuItems: any;
children: ReactNode;
}
const isDev = process.env.NODE_ENV === 'development';
// tslint:disable no-default-export
export default ({ children, menuItems }: AppLayoutProps) => {
// copyright year
const [year, setDate] = useState<any>();
// setting cookie to be referenced in the useFetch hook, setting the query for dealer specific information
const [cookie] = useCookie('one-day-location', '1');
// the API call
const [query, setQuery] = useState('https://jsonplaceholder.typicode.com/users/1/');
const url = query && `https://jsonplaceholder.typicode.com/users/${cookie}`;
const { data } = useFetch(url);
const getYear = () => setDate(new Date().getFullYear());
useEffect(() => {
getYear();
}, []);
return (
<div className={s.layout}>
<Header menuItems={menuItems} data={data}></Header>
{children}
<Footer menuItems={menuItems} logo={<Logo />} year={year} />
{isDev && <Devtools />}
</div>
);
};
And this is my use-cookie hook that is referenced throughout these components:
use-cookie.ts
import { useState } from 'react';
import Cookies from 'universal-cookie';
/**
* Custom hook creates and returns cookie values.
*/
export const useCookie = (key: string, value: string) => {
const cookies = new Cookies();
const [cookie] = useState(() => {
if (cookies.get(key)) {
return cookies.get(key);
}
cookies.set(key, value);
});
const updateCookie = (value: string) => {
removeItem(value);
cookies.set(key, value);
};
const removeItem = (key: string) => {
cookies.remove(key);
};
return [cookie, updateCookie, removeItem];
};
If you notice though, it flickers on every page load. Is there a way to store and display that data differently so that it won't do this?
Thanks in advance.
So, I was able to figure out a better solution with a bit of digging. Rather than trying to debug the hook that I built, I'm using axios-hooks, which has all of the same functionality that I needed, but solves the problem. In my layout, I've got the data being fetched like so:
const [cookie, updateCookie] = useCookie('one-day-location', '1');
const [{ data, loading, error }] = useAxios(
`https://jsonplaceholder.typicode.com/users/${cookie}`,
);
And I'm then able to pass the data down to my Header component, but it doesn't load the API multiple times, and eliminates the flickering. I can absolutely add more documentation here in my answer if anyone has any more questions.

Use useQuery when a state variable changes

I have following structure in a react app:
<SomeProvider id={objectId}>
<SomeApp />
</SomeProvider>
This provider uses a useQuery to fetch an object from the backend. With useContext I make it accessible to all the components of the app:
const EventProvider = ({ id, children }) => {
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
// etc.
}
export const useSomething = () => {
// etc.
}
In a component I can have access to this object:
const Component = ({id}) => {
const { obj } = useSomething()
}
Until here all working. My question is, inside this component, I have a button that changes this object in the backend.
How can I fetch the obj again?
I can of course refresh the page, but this is the solution that I want to avoid. What I've tried so far is:
Try to use the useQuery again in the Component.
const Component = ({id}) => {
const { obj } = useSomething()
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
}
But actually what I would like to do is trigger the query when a State variable changes:
const Component = ({id}) => {
const { obj } = useSomething()
const { isActivated } = useOtherHook()
const {data} = useQuery(SOME_QUERY, { variables: input })
const obj = data ? data.something : {}
useEffect(() => {
// when isActivated changes, I would like to fetch the obj again
}, [isActivated])
}
If I use useQuery inside of useEffect, I get:
Hooks can only be called inside of the body of a function component
What is the best approach to solve this challenge?
The reason for the error messages you get is, that you can only use hooks in the body of your component, not inside of other functions that are not hooks, nor inside of conditions. You can read more about that here in the rules of hooks
As you can read in the documentation for useQuery here, there are multiple ways of keeping data up to date or refetching.
The easiest way of keeping data up to date would be to use the polling feature from apollo.
const { loading, error, data } = useQuery(QUERY, {
variables: input,
skip: !isActivated,
pollInterval: 500, // Update every 500ms
});
One way of refetching on demand would be to use the returned refetch function.
const { loading, error, data, refetch } = useQuery(/* ... */);
useEffect(() => { refetch() }, [isActivated])
Depending on your needs, you could also use the useLazyQuery hook. This does not fetch data on render but only on function call. This is most useful when you want to only request data when the input has been set or changed.
const [getData, { loading, data }] = useLazyQuery(QUERY);
useEffect(() => { getData({ variables: input }) }, []);

Resources