How to setup a function which gets app settings and sets it as localStorage before the page loads. (next.js) - reactjs

I've been working on a Next.JS web application for the past couple of days but I've reached a problem. The app has an API call (/api/settings) which returns some settings about the application from the database. Currently, I have a function which returns these settings and access to the first component:
App.getInitialProps = async () => {
const settingsRequest = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/settings`
);
const settingsResponse = await settingsRequest.json();
return { settings: settingsResponse };
};
This does work and I am able to pass in settings to components but there are two problems with this:
I need to nest the prop through many components to reach the components that I need
This request runs every time a page is reloaded/changed
Essentially, I need to create a system that does this:
runs a function in the _app.tsx getInitialProps to check if the data is already in localStorage, if not make the API request and update localStorage
have the localStorage value accessible from a custom hook.
Right now the problem with this is that I do not have access to localStorage from the app.tsx getInitialProps. So if anyone has an alternative to run this function before any of the page loads, please let me know.
Thanks!

I found a solution, it might be a janky solution but I managed to get it working and it might be useful for people trying to achieve something similar:
First we need to create a "manager" for the settings:
export const checkIfSettingsArePresent = () => {
const settings = localStorage.getItem("app_settings");
if (settings) return true;
return false;
};
export const getDataAndUpdateLocalStorage = async () => {
const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/settings`);
const response = await r.json();
localStorage.setItem("app_settings", JSON.stringify(response));
};
With that created we can add a UseEffect hook combined with a useState hook that runs our function.
const [doneFirst, setDoneFirst] = useState<boolean>(false);
useEffect(() => {
const settingsPreset = checkIfSettingsArePresent();
if (performance.navigation.type != 1)
if (settingsPreset) return setDoneFirst(true);
const getData = async () => {
await getDataAndUpdateLocalStorage();
setDoneFirst(true);
};
getData();
}, []);
//any other logic
if (!doneFirst) {
return null;
}
The final if statement makes sure to not run anything else before the function.
Now, whenever you hot-reload the page, you will see that the localStorage app_settings is updated/created with the values from the API.
However, to access this more simply from other parts of the app, I created a hook:
import { SettingsType } from "#sharex-server/common";
export default function useSettings() {
const settings = localStorage.getItem("app_settings") || {
name: "ShareX Media Server",
};
//#ts-ignore
return JSON.parse(settings) as SettingsType;
}
Now I can import useSettings from any function and have access to my settings.

Related

React does not wait for async function before mounting components

I am currently using a mock json-server to hold user information in my React app. I am working on storing settings and preferences for users. I have a setting page implemented through a Route component. I am displaying the settings configurations on this page. I am fetching the user settings in App.tsx :
const fetchUser = async (id:number) => {
const res = await fetch(`http://localhost:5001/users/${id}`)
const user = await res.json()
return user
}
const getSettings = async () => {
const user = await fetchUser(0)
setSettings(user.settings);
}
Then I am passing down the state variable for settings through useContext.
const [settings, setSettings] = useContext(userContext);
This works fine when I start on the root page and then go to the settings. However, if the user goes directly to the settings page, the setting state is initially null and I cannot access its values. I tried to fetch the settings again in the setting page component with useEffect but React does not wait for async functions to complete before mounting the components.
const getSettings = async (id:number) => {
const res = await fetch(`http://localhost:5001/users/${id}`)
const user = await res.json()
const settings = user.settings
setSettings(settings);
}
useEffect(() => {
if (!settings) getSettings(0);
}, [])
Is there a way to get around this? I would like to access the settings state throughout the app but the user should not have to start with the root component.
Note: It does work if I check that the value is null before use like this :
settings?.test.difficulty
Please try the below change for useEffect:
useEffect(async () => {
if (!settings) await getSettings(0);
}, [])

React Recoil: State not being saved across navigation to different URLs within App

I'm getting started with Recoil for a React App, but running into some issues, or at least some behavior I'm not expecting.
I'd like to be able to use one component to render many different "views" based on the URL. I have a useEffect in this component that switches based on the location.pathname and based on that pathname, it'll make an API call. But before it makes the API call, it checks the length of the atom to see if it's empty or not, then will call the API and set the atom based on the API call.
However, when I navigate to a different URL and come back to one I've already visited, the API is called again, even though I've previously set the state for that URL.
The behavior I'm expecting is that once a URL has been visited and the return from the API is stored in an Atom, the API call isn't made again when leaving the URL and coming back.
Relevant code below:
Atom.js
export const reports = atom({ key: "reports", default: { country: [], network: [], }, });
the one component that will render different data based on the reports atom.
import { useRecoilState } from "recoil";
import { reports } from "../globalState/atom";
const TableView = ({ columns, }) => {
const location = useLocation();
const [report, setReport] = useRecoilState(reports);
const currentView = location.pathname.split("/")[1];
useEffect(() => {
const getReportsData = async () => {
switch (location.pathname) {
case "/network":
if (report[currentView].length === 0) {
const response = await fetch("/api");
const body = await response.json();
setReport(
Object.assign({}, report, {
[currentView]: body,
})
);
console.log('ran')
break;
}
getReportsData();
}, [])
As previously mentioned, that console.log is ran every time I navigate to /network, even if I've already visited that URL.
I've also tried doing this with selectors.
Atom.js
export const networkState = atom({
key: "networkState",
default: networkSelector,
});
export const networkSelector = selector({
key: "networkSelector",
get: async ({ get }) => {
try {
const body = await fetch("/api/network").then((r) => r.json());
return body;
} catch (error) {
console.log(error);
return [];
}
}
Component
import {useRecoilStateLoadable} from "recoil"
import {networkState} from "../globalState/atom";
const Table = ({columns}) => {
const [networkData, setNetworkData] =
useRecoilStateLoadable(networkState);
And then a switch statement based on networkData.state
}
Any help would be greatly appreciated, thank you!

React useEffect with Stripe - get client secret asynchronously

I've been plopped into the middle of a react project with no prior knowledge so I'm sure this a duplicate.
I want to use the StripeElements, and the react library is easy enough. I'm just not sure how to get the client secret from the server.
This is what I'm trying:
const stripePromise = loadStripe('stripe_key');
const StripeForm = () => {
const [stripeData, setStripeData] = useState({});
const getClientSecret = async () => {
const { data } = await api.get(`payment/stripe/get-client-secret/`);
return data
}
useEffect(() => {
getClientSecret().then(data => setStripeData(data))
}, [])
if(stripeData.clientSecret) {
return (
<QuoteContainer title={"Pay With Credit Card"}>
<SectionCard>
{stripeData.clientSecret}
</SectionCard>
</QuoteContainer>
);
}
return (<b>Loading</b>)
}
The route payment/stripe/get-client-secret/ returns an array with a key 'clientSecret'. This function is working correctly.
I just can't get the <b>Loading</b> text to be replaced with the QuoteContainer component once the promise is resolved.
If you want to rendering Loading component. You have to set falsy value for stripedData state. Because {} value is truthy. So I think the code is not rendering Loading component. The loading component is not rendered because there is no place to set it as a false value anywhere.
what AJAX library are you using to make the API call? Usually you need to call a function on the response object in order to access the data. For instance, if you are using fetch(), you need to call json() then access the response data.

react-query always return stale data and no call is made to server

I recently started using react-query and have encountered the issue that always stale data is returned and no call to server is made. here is the react query related code:
export function useGetAccount(id: number){
return useQuery([`account${id}`, id], async (args) => {
const [key, accountId] = args.queryKey
const [acc, teams, modules] = await Promise.all([
getAccount(),
getTeams(),
getModules()])
let account: AccountDetail = {
accountId: acc.accountId,
userId: acc.userId,
companyId: acc.companyId,
login: acc.login,
email: acc.email,
description: acc.description,
isActive: acc.isActive,
providers: acc.providers,
teams: teams,
modules: modules
}
return account
async function getAccount() {
const api = createApi() // <= axios wrapper
const { data } = await api.get(`accounts/${accountId}`, undefined, undefined)
return data as AccountModel
}
async function getTeams() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/teams`, undefined, undefined)
const { collection } = data as ResponseCollectionType<AccountTeam>
return collection
}
async function getModules() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/resources`, undefined, undefined)
const { collection } = data as ResponseCollectionType<ModuleAccessModel>
return collection
}
})
}
I even reduced the cache time but still to no avail. I do not see any calls made to server side except after a long delay or if I open the browser in incognito mode then first time the data is fetched and then no call is made.
this is used in a component which shows the details and is passed the id as a prop. everything is working fine except that the data is the one which was retrieved first time and even a refresh (F5) returns the stale data.
what changes do I need to make in this case?
[observation]: Ok, it does make a call but only after exact 5 minutes.
well the problem is not in react-query but in axios, described here Using JavaScript Axios/Fetch. Can you disable browser cache?
I used the same solution i.e. appending timestamp to the requests made by axios and everything worked fine.

In React, fetch data conditional on results of an initial fetch

We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}

Resources