How to wrap a function that uses hooks inside a useEffect? - reactjs

I wrote a function to make an API call. Typically, I'd just wrap it in a useEffect and throw it in the same file that needs it, but I'm trying to write my code a little cleaner. So I did the following.
In my component.js file, I have the following:
import { apiCall } from '../../../framework/api.js';
import { useEffect, useState } from 'react';
export const Table = () => {
const [ resp, setResp ] = useState();
useEffect(() => {
console.log(apiCall());
}, []);
return(
<>
{ resp &&
resp.map(([key, value]) => {
console.log("key: " + key);
return(
<SomeComponent />
);
})
}
</>
);
}
in my api.js file, I have the following:
import axios from 'axios';
import { useState } from 'react';
export const apiCall = () => {
const [ resp, setResp ] = useState();
axios.get('https://some.domain/api/get').then((response) => {
setResp(response.data);
});
if(resp) return resp;
}
This always returns an error (Invalid hook call. Hook calls can only be called inside the body of a function component.)
If I rewrite my component.js and include the axios call directly inside useEffect instead of calling the function apiCall() from the external file, it obviously works with no problems.
I think I know it has to do with the fact that I'm using hooks in my apiCall function, and wrapping that call in a useEffect in my component.js. However, if I don't wrap it in a useEffect, it'll just run continuously and I don't want that either.

You have to follow the custom hook naming convention for this to be able to work. You can check out the documentation for that here: https://reactjs.org/docs/hooks-custom.html
Anyway, I believe in this case this should work:
import axios from 'axios';
import { useState } from 'react';
export const useApiCall = () => {
const [ resp, setResp ] = useState();
axios.get('https://some.domain/api/get').then((response) => {
setResp(response.data);
});
if(resp) return resp;
}
And then in component.js, you would call useApiCall()

Usually, we do it like this
export const useApiCall = () => {
const [ resp, setResp ] = useState();
useEffect(() => {
axios.get('https://some.domain/api/get').then((response) => {
setResp(response.data);
});
}, []);
return resp;
}
and then use it like so
export const Table = () => {
const resp = useApiCall();
return(
<>
{ resp &&
resp.map(([key, value]) => {
console.log("key: " + key);
return(
<SomeComponent />
);
})
}
</>
);
}
The prefix "use" in the function name is important, this is how we define a custom hook.

React Hook "useState" is called in function "apiCall" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".
You can use following methods.
import { useState } from 'react';
export const ApiCall = () => {
const [state, setState] = useState();
};
or
import { useState } from 'react';
export const useApiCall = () => {
const [state, setState] = useState();
};

Related

How to run functional component in useEffect used for updating context

Trying to get the API functional component that updates Context values in useEffect to execute within another App functional component (on import) when mounted to allow render when values exist/update.
The API function at API.js appears to be working correctly as a stand alone component. The problem (due to lack of understanding) is upon the import to App.js where the API function should execute.
API.js
import { useState, useContext, useEffect } from 'react'
import { AppContext } from './contexts/AppContext';
export const API = () => {
const {updateOrder, updateCustomer} = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
const orderDetails = await fetch('url.com/order');
const customerDetails = await fetch('url.com/customer');
updateOrder(await orderDetails.json())
updateCustomer(await customerDetails .json())
}
fetchData();
}, [])
}
App.js
import React, { useContext, useEffect } from 'react';
import { AppContext } from './contexts/AppContext';
import { API } from './components/API';
const App = ({ isLoading }) => {
const {order, customer} = useContext(AppContext);
useEffect(() => {
isLoading && <API />
}, []) // eslint-disable-line react-hooks/exhaustive-deps
if (order && customer) {
return <SomeComponent/>
}
}
The expected outcome is to be able to use API within the initial mount (as API dependency) and conditionally return/render content in App.
I've tried changing the API component into function and exporting with default, however context is not supported outside of component.
A component is a function that renders something in the UI. You API component is not really a component - it doesn't have any branch that returns any JSX - although that's not always required.
You should look into building a custom hook instead. Try something like this:
App.js
import React, { useContext, useEffect } from "react";
import { AppContext } from "./contexts/AppContext";
// Hooks should start with 'use'
export const useAPI = () => {
const { updateOrder, updateCustomer } = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
// Fetching data from both endpoints in parallel
const res = await Promise.all([fetch("url.com/order"), fetch("url.com/customer")]);
// Converting both payloads to JSON in parallel
const data = await Promise.all([res[0].json(), res[1].json()]);
updateOrder(data[0]);
updateCustomer(data[1]);
};
fetchData();
}, []);
};
const App = ({ isLoading }) => {
// Calling the custom hook
useAPI();
const { order, customer } = useContext(AppContext);
if (order && customer) {
return <SomeComponent />;
}
};

React hook "useMemo" with array as dependency

I am new to react (that I use with typeScript) and I am facing an issue with the use of the useMemo hook.
Here is my fetching service:
export default class FetchingService {
datas: Data[] = [];
constructor() {
this.fetch();
}
async fetch(): Promise<Data[]> {
const d = // await an async array from an api, using Array.flat()
this.datas = d;
console.log(this.datas);
return d;
}
}
In a component, I try to watch for change of the datas attribute of my service:
import fetchingService from '../services/fetchingService.ts';
const Home: React.FC = () => {
const ds: Data[];
const [datas, setDatas] = useState(ds);
const fetchDatas = useMemo(() => {
console.log('Render datas', fetchingService.datas?.length)
setDatas(fetchingService.datas);
return fetchingService.datas;
}, [fetchingService.datas]);
return (
<ul>{datas.map(d => {
return (
<li key={d.id}>{d.id}</li>
);
</ul>
);
}
The problem I am facing is that the useMemo hook is not recompouted when the datas attribute changes within my fetchService. I am pretty sure that my FetchingService.fetch() function works because the console.log within the fetch function always display the fetched datas.
The observed behavior is that sometimes datas are well rendered (when fetch ends before rendering ...), but sometimes it isn't.
The expected one is that datas are rendered every time and only on refresh, exept when datas are modified
I also tried to put the length of the data array as a dependency in useMemo, but in both cases it doesn't work and I have a warning in my IDE, telling me it is an unnecessary dependency.
I don't really understand if it is a typescript or a specific react behavior issue. I think the reference of the datas attribute should change at the end of the fetch (or at least its length attribute ...), but tell me if I am wrong.
I do appreciate every help !
in fetchingService, when datas change, probably the dependency cannot be accepted. You can use a custom hook in stead of it.
You can use this source about useMemo: useMemo with an array dependency?
import { useState, useLayoutEffect, useCallback } from "react";
export const useFetchingService = () => {
const [fetchedData, setFetchedData] = useState([]);
const fetch = useCallback(async () => {
const d = await new Promise((res, rej) => {
setTimeout(() => {
res([1, 2, 3]);
}, 5000);
}); // await an async array from an api, using Array.flat()
setFetchedData(d);
}, []);
useLayoutEffect(() => {
fetch();
}, []);
return [fetchedData];
};
useLayoutEffect runs before rendering
using:
const [fetchData] = useFetchingService();
const fetchDatas = useMemo(async () => {
console.log("Render datas", fetchData.length);
setDatas(fetchData);
return fetchData;
}, [fetchData]);
You can also use this directly without 'datas' state.
I hope that this will be solution for you.
So I put together a codesandbox project that uses a context to store the value:
App.tsx
import React, { useState, useEffect, createContext } from "react";
import Home from "./Home";
export const DataContext = createContext({});
export default function App(props) {
const [data, setData] = useState([]);
useEffect(() => {
const get = async () => {
const d = await fetch("https://dummyjson.com/products");
const json = await d.json();
const products = json.products;
console.log(data.slice(0, 3));
setData(products);
return products;
};
get();
}, []);
return (
<div>
Some stuff here
<DataContext.Provider value={{ data, setData }}>
<Home />
</DataContext.Provider>
</div>
);
}
Home.tsx
import React, { FC, useMemo, useState, useEffect, useContext } from "react";
import { DataContext } from "./App";
import { Data, ContextDataType } from "./types";
const Home: FC = () => {
const { data, setData }: ContextDataType = useContext(DataContext);
return (
<>
<ul>
{data.map((d) => {
return (
<li key={d.id}>
{d.title}
<img
src={d.images[0]}
width="100"
height="100"
alt={d.description}
/>
</li>
);
})}
</ul>
</>
);
};
export default Home;
This was my first time using both codesandbox and typescript so I apologize for any mistakes

How do I return data from a custom react hook?

I have a custom react hook that is fetching data from an api. I'm getting error: "Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component."
I don't know if this is due to the structure of my custom hook or how I'm using the hook in the component where it's being called.
Here is my custom hook:
import { useState, useEffect } from "react";
const axios = require('axios');
const crypto = require('crypto');
function useFetchNGSData(endpoint) {
const[data, setData] = useState(null);
const dateString = () => {
// MORE CODE
return date.toString();
}
const ngs_username = process.env.REACT_APP_NGS_USERNAME;
const ngs_password = process.env.REACT_APP_NGS_PASSWORD;
const ngs_access_token = process.env.REACT_APP_NGS_ACCESS_TOKEN;
const ngs_secret_key = process.env.REACT_APP_NGS_SECRET_KEY;
const string_to_sign = ngs_username + ngs_password + ngs_access_token + dateString();
const digest = crypto.createHmac('sha1', ngs_secret_key).update(string_to_sign).digest('base64');
const authKey = 'NGS ' + ngs_access_token + ':' + digest;
const url = 'https://api.ngs.com';
useEffect(() => {
axios.defaults.headers.common['Authorization'] = authKey;
axios.get(url + endpoint)
.then((response) => {
setData(response.data);
})
.catch((error) => {
console.log(error);
})
},[endpoint]);
return { data };
}
export default useFetchNGSData;
Here's the component that is using the custom hook:
import React, { useState } from 'react';
import useFetchNGSData from '../useFetchNGSData';
const Data = (props) => {
const [players, setPlayers] = useState([]);
const handleGetPlayers = () => {
teamCollection.forEach(teamId => {
setPlayers(useFetchNGSData('/league/roster/current?teamId=5100')
});
}
return (
<div>
// ... handleGetPlayers
</div>
);
}
export default Data;
I know I'm omitting a lot of code but hopefully this is enough to demonstrate the issue.
You need to move the hook out of the event handler, and in to the body of the component.
import React, { useState } from 'react';
import useFetchNGSData from '../useFetchNGSData';
const Data = (props) => {
const { data: players } = useFetchNGSData('/league/roster/current?teamId=5100')
return (
<div>
// ... handleGetPlayers
</div>
);
}
export default Data;
But really, you don't need to use setPlayers here. You already have that data in your returned values from your custom hook.

React Hooks + Mobx => Invalid hook call. Hooks can only be called inside of the body of a function component

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>
)
}

How to perform a side effect after another side effect rendered a react component

I have the following simple use case:
The component ServerRendered is used to render markup that is retrieved from a server using the property url . In ServerRendered I use useEffect to load the markup from the back-end, set the state to the current markup and render it in a div.
The property init optionally specifies a function that should be executed after having rendered the markup.
How would I run the init function after the markup has been rendered?
/* eslint-disable react/no-danger */
import React, {useState, useEffect} from 'react';
import axios from 'axios';
type ServerRenderedPropsType = {
url: string,
init?: () => void,
};
function ServerRendered(props: ServerRenderedPropsType) {
const [html, setHtml] = useState('');
useEffect(() => {
async function fetchData() {
const result = await axios(props.url);
setHtml(result.data.title);
}
fetchData();
}, [props.url]);
return <div dangerouslySetInnerHTML={{__html: html}} className="serverRendered" />;
}
export default ServerRendered;
You have just to do another effet when you receive your data
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
type ServerRenderedPropsType = {
url: string,
init?: () => void,
};
function ServerRendered(props: ServerRenderedPropsType) {
const [html, setHtml] = useState('');
const fetchData = useCallback(async () => {
const result = await axios(props.url);
setHtml(result.data.title);
}, [props.url])
useEffect(() => {
fetchData();
}, [fetchData]);
useEffect(() => {
// do what you want here
if (!html) return;
props.init()
}, [html])
return <div dangerouslySetInnerHTML={{__html: html}} className="serverRendered" />;
}
export default ServerRendered;

Resources