Using Hooks for fetch - javascript

I am trying to use hooks to fetch data from an endpoint and I keep getting an error saying
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Uncaught TypeError: Object(...) is not a function
I have tried fetching data without hooks and it works fine.
import React, { useEffect, useState } from 'react';
const fetchData = () => {
const [data, setData] = useState([]);
useEffect(() => {
const getData = async () => {
const response = await fetch('someendpoint');
const jsonResponse = await response.json();
setData(jsonResponse);
}
getData();
}, []);
return (
<div></div>
)
}
export default fetchData;

Rename the component to a valid name: FetchData with a capital letter to pass the linter check (matches inside capitalized function name scope a function with use prefix).
See User-Defined Components Must Be Capitalized.
When an element type starts with a lowercase letter, it refers to a built-in component like <div> or <span> and results in a string 'div' or 'span' passed to React.createElement.

Related

Axios request keeps returning twice undefined and twice the data

I'm trying to fetch an api on a custom reactjs hook using Axios. I keep getting twice the response as undefined and after that twice as a successful fetch with the data. The undefined breaks my app.
Btw I'm fetching from the randomuser api.
import axios from "axios";
import { useState, useEffect } from "react"
export const useFetch = (url) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState('')
const getData = () => {
setLoading(true)
try {
axios.get(url)
.then(response => setData(response.data));
setLoading(false)
} catch (error) {
setError(error)
}
};
useEffect(() => {
getData()
}, [url])
return {loading, data, error}
}
Trying to use it here and map over it
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useFetch } from '../custom_hooks/useFetch';
const PersonDetails = () => {
const { loading, data , error } = useFetch('https://randomuser.me/api?results=20');
const { results } = data;
const { id } = useParams();
const [person, setPerson] = useState({})
useEffect(() => {
const newPerson = results?.find(person => person.login.uuid === parseInt(id))
setPerson(newPerson)
console.log(newPerson)
}, [])
return (
<div>
{person.name.first}
</div>
)
}
export default PersonDetails
This is the thing I actually Im trying to do, but now because it is undefined, I get that cannot read properties of undefined...
When the effect runs you:
setLoading(true)
Send the Ajax request
setLoading(false)
Later, then the Ajax response arrives you:
setData(response.data)
Since you depend on loading to determine if data is set or not, it breaks.
There are two things you could do:
Move setLoading(false) inside the then callback so it doesn't get set until after you have setData(response.data)
Get rid of loading entirely and base your logic off data being undefined or having a different value.
you should define the getData function inside the useeffect or pass it in dependency array and wrap the function by usecallback to avoid unnecessary rerenders.
you should use abortcontroller in case of cancelations and to have cleanup function in useeffect. (in this case it's better to define getdata body in useeffect)
useEffect(() => {
const controller = new AbortController();
const getData = async () => {
setLoading(true)
try {
await axios.get(url, {signal: controller.signal})
.then(response => setData(response.data));
} catch (error) {
setError(error)
}
}
getData()
return()=>controller.abort()
},[url]}
you can read more about fetching data with hooks in following url and where to setloading and other needed states.
https://www.robinwieruch.de/react-hooks-fetch-data/
Just in case, this solution helped me : https://github.com/axios/axios/issues/2825#issuecomment-883635938
"The problem in my case was caused by React development server.
The strict mode in react caused the issue!
I had to remove the strict mode
This solved the problem of sending double requests!
The strict mode checks are only run in development mode.
Doc: https://reactjs.org/docs/strict-mode.html
"

Save value from custom hook to state

I am trying to save a value from a custom hook, which is fetching data for the server, to functional component state with useState, because I later need to change this value and after the change it needs to rerender. So desired behaviour is:
Set State variable to value from custom hook
Render stuff with this state variable
Modify state on button click
Rerender with new state
What I tried is:
Set the inital value of useState to my hook:
const [data, setData] = useState<DataType[] | null>(useLoadData(id).data)
but then data is always empty.
Set the state in a useEffect() hook:
useEffect(()=>{
const d = useLoadData(id).data
setData(d)
}, [id])
But this is showing me the Error warning: Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Doing this:
const [data, setData] = useState<DocumentType[]>([])
const dataFromServer = useLoadData(id).data
useEffect(()=>{
if (dataFromServer){
setData(dataFromServer)
}
}, [dataFromServer])
Leading to: ERROR: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
What would be a proper solution for my use case?
It looks like your custom hook returns a new array every time it is used.
Solution 1: change your hook to return a 'cached' instance of an array.
function useLoadData(id) {
const [data, setData] = useState([]);
useEffect(() => {
loadData(id).then(setData);
}, [id]);
// good
return data;
//bad
//return data.map(...)
//bad
//return data.filter(...)
//etc
}
codesandbox.io link
Solution 2: change your hook to accept setData as a parameter.
function useLoadData(id, setData) {
useEffect(() => {
loadData(id).then(setData);
}, [id]);
}
Here I am telling the hook where to store data so that both custom hook and a button in a component can write to a same place.
codesandbox.io link
Full example:
import React from "react";
import ReactDOM from "react-dom";
import { useState, useEffect } from "react";
// simulates async data loading
function loadData(id) {
return new Promise((resolve) => setTimeout(resolve, 1000, [id, id, id]));
}
// a specialized 'stateless' version of custom hook
function useLoadData(id, setData) {
useEffect(() => {
loadData(id).then(setData);
}, [id]);
}
function App() {
const [data, setData] = useState(null);
useLoadData(123, setData);
return (
<div>
<div>Data: {data == null ? "Loading..." : data.join()}</div>
<div>
<button onClick={() => setData([456, 456, 456])}>Change data</button>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));

Why am I unable to return an HTML element after awaiting a promise in React

In the following example the index.js calls the component DiffState.js which is supposed to wait for 2 seconds (to simulate an async operation) and then return an H1 element that says "hello".
index.js
import React, { lazy, Suspense } from "react";
import ReactDOM from "react-dom";
const DiffState = lazy(() => import("./DiffState"));
ReactDOM.render(
<Suspense fallback={<h1>Waiting for component fallback.</h1>}>
<DiffState />
</Suspense>,
document.getElementById("root")
);
DiffState.js
import React from "react";
const DiffState = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return <h1>hello</h1>;
};
export default DiffState;
What happens instead, for reasons I am unable to understand is that I end up with the following error:
Error Objects are not valid as a React child (found: [object
Promise]). If you meant to render a collection of children, use an
array instead.
this error occurs in the DiffState component btw.
From React's documentation:
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component
What happens in your example is that the import function receives (as argument) a function that returns DiffState - which is a Promise, rather than a default export containing a React component.
In other words, your lazy function renders into a Promise that's resolved into another Promise.
Hence, a component should be a synchronous function; although its content can still be updated:
const DiffState = () => {
const [content, setContent] = useState(null);
useEffect(() => {
setTimeout(() => {
setContent(<h1>hello</h1>);
}, 2000);
}, []);
return content;
};

How to pass route params in Context API

I'm using the context API and I have this in my context file:
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${props.match.alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (props.match) getSingleCountryData();
}, [props.match]);
In the component I'm using, it doesn't work because it doesn't know what the props.match.alpha3Code is. How can I can pass the value? The alpha3Code is coming from the URL: localhost:3000/country/asa where asa is the alpha3Code, how can I get this value?
Basically, what I'm trying to do is. I have a list of countries I listed out on the home page. Now I'm trying to get more information about a single country. The route is /country/:alpha3Code where alpha3Code is gotten from the API.
FWIW, here is my full context file:
import React, { useState, createContext, useEffect } from 'react';
import axios from 'axios';
export const CountryContext = createContext();
export default function CountryContextProvider(props) {
const [countries, setCountries] = useState([]);
const [country, setCountry] = useState([]);
useEffect(() => {
const getCountryData = async () => {
const result = await axios.get(
'https://cors-anywhere.herokuapp.com/https://restcountries.eu/rest/v2/all'
);
setCountries(result.data);
};
getCountryData();
}, []);
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${props.match.alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (props.match) getSingleCountryData();
}, [props.match]);
return (
<CountryContext.Provider value={{ countries, country }}>
{props.children}
</CountryContext.Provider>
);
}
In the component I'm using the country, I have:
const { country } = useContext(CountryContext);
I know I can do this from the component itself, but I'm learning how to use the context API, so I'm handling all API calls in my context.
The API I'm making use of is here
Codesandbox Link
Project Github link
You can update the context from a component using it by passing down a setter function which updates the context state.
export default function CountryContextProvider({ children }) {
const [countries, setCountries] = useState([]);
const [country, setCountry] = useState([]);
const [path, setPath] = useState('');
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(`your/request/for/${path}`);
setCountry(result.data);
}
if(path) getSingleCountryData();
}, [path]);
return (
<CountryContext.Provider value={{ countries, country, setPath }}>
{children}
</CountryContext.Provider>
);
}
Now use setPath to update the request endpoint with the route match once this component is mounted.
const Details = ({ match }) => {
const {
params: { alpha3Code }
} = match;
const { country, setPath } = useContext(CountryContext);
useEffect(() => {
setPath(alpha3Code);
}, [alpha3Code]);
return (
<main>Some JSX here</main>
);
};
export default withRouter(Details);
Linked is a working codesandbox implementation
In the component I'm using, it doesn't work because it doesn't know
what the props.match.alpha3Code is. How can I can pass the value? The
alpha3Code is coming from the URL: localhost:3000/country/asa where
asa is the alpha3Code, how can I get this value?
I guess the root of your problem is this one. You have no idea which the aplha3Code parameter comes from. I have dived into your GitHub repo to make it clearer.
First, match is one of react-router provided terms. When you use something like props.match, props.history, props.location, you must have your component wrapped by the withRouter, which is a Higher Order Component provided by react-router. Check it out at withRouter. For example, below is the withRouter usage which is provided by react-router:
// A simple component that shows the pathname of the current location
class ShowTheLocation extends React.Component {
render() {
const { match, location, history } = this.props;
return <div>You are now at {location.pathname}</div>;
}
}
const ShowTheLocationWithRouter = withRouter(ShowTheLocation);
ShowTheLocation is wrapped by the withRouter HOC, which will pass all the route props (match, history, location...) to ShowTheLocation through props. Then inside ShowTheLocation, you are able to use something like props.match. Clear enough?
So back to your problem! You have not wrapped any components by withRouter yet, have you? Stick to it and have some fun! You will figure it out soon!
Also, please be aware that you must place your component under the BrowserRouter to be able to use the react-router things
If you want to go with Hooks, please take a look at this super useful one:
https://usehooks.com/useRouter/
It wraps all the useParams, useLocation, useHistory, and use useRouteMatch hooks up into a single useRouter that exposes just the data and methods we need. Then, for example, inside your component, do it like this:
import { useRouter } from "./myCustomHooks";
const ShowMeTheCode = () => {
const router = useRouter();
return <div>This is my alpha3Code: {router.math.params.alpha3Code}</div>;
}
Update 1 from Peoray's reply:
This is where the problem occurs:
https://github.com/peoray/where-in-the-world/blob/cb09871fefb2f58f5cf0a4f1db3db2cc5227dfbe/src/pages/Details.js#L6
You should avoid calling useContext() straightly like that. Have a look at my example below:
// CountryContext.js
import { useContext, createContext } from "react";
const CountryContext = createContext();
export const useCountryContext = () => useContext(CountryContext);
Instead, you should wrap it by a custom hook like useCountryContext above. And then, inside your Details component, import it and do like:
import React, from 'react';
import { useCountryContext } from '../contexts/CountryContext';
const Details = (props) => {
const { country } = useCountryContext();
...
}
Update 2 from Peoray's reply:
Although I have stated it in advance for you, I just feel like you did not make enough effort to go through what I said.
Also, please be aware that you must place your component under the
BrowserRouter to be able to use the react-router things
In your codesandbox, it shows the Cannot read property 'match' of undefined error. Okay, as I said above, you have not moved the ContextCountryProvider to under the BrowserRouter to get the useRouter work.
I have fixed it for you, and the screen popped out, please check it at updated codesanbox here. You will get what you need at App.js file.
Although it still throws some Axios bugs there, I think my job is done. The rest is up to you.
You might use useParams hook to get everything you need inside your context provider. Docs
Something like this:
import useParams in file where your Provider component is
in your CountryContextProvider add this at the top of the component:
const { alpha3Code } = useParams();
update useEffect which needs props.match
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (alpha3Code) getSingleCountryData(); // or if you need `match` - do not destructure useParams()
}, [alpha3Code]);

React custom hooks in callback

I'm trying to use my custom hook inside the callback logic like this:
import React, { useEffect, useState } from 'react';
import useDataChange from '../../hooks/useDataChange';
const SomeComponent = () => {
return (
<Table
handleTableChange={data => useDataChange(data)}
/>
);
};
export default SomeComponent;
And my custom hooks (just to simplify) looks like that:
const useDataChange = data => {
console.log(data);
};
export default useDataChange;
In short, custom hook supposed to be fired when data from table is changed (ie. when handleTableChange in Table component is fired). Instead I'm getting:
React Hook "useDataChange" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
How can I use it when table data is changed?
The key to understanding hooks is to extract pieces of react code out of components. So your first step would be to get it working inside the component
const SomeComponent = () => {
const [data, setData] = useState([])
return (
<Table
handleTableChange={setData}
/>
);
};
Based on your code, I'm not seeing where you'd need a hook or side effect. But let's pretend that you do want to run some simple side effect:
const SomeComponent = () => {
const [data, setData] = useState([])
const [modifiedData, setModifiedData] = useState([])
useEffect(() => {
//here we're just going to save the current data stream into a new state variable for simplicity
setModifiedData(data)
}, [data])
return (
<Table
handleTableChange={setData}
data={modifiedData}
/>
);
};
So now we have some logic that runs a side effect. Now you can extract it to its own hook.
const useModifiedData = (data) => {
const [modifiedData, setModifiedData] = useState(data)
useEffect(() => {
setModifiedData(data)
}, [data])
return modifiedData
}
const SomeComponent = () => {
const [data, setData] = useState([])
const modifiedData = useModifiedData(data)
return (
<Table
handleTableChange={setData}
data={modifiedData}
/>
);
};
Here you have a hook that lives outside the component logic, so it can now go in its own file and be used across your project.
Like it says React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks. React has this limitation so that it can track the state and effects. In your case you can define you custom hook to return a function which does the desired work, instead of directly doing it in your hook.
In this case your custom hook file will look something like this-
const useDataChange = () => data => {
console.log(data);
};
export default useDataChange;
Then in your component you can use it like this -
import React, { useEffect, useState } from 'react';
import useDataChange from '../../hooks/useDataChange';
const SomeComponent = () => {
const callback = useDataChnage();
return (
<Table handleTableChange={callbackdata} />
);
};
export default SomeComponent;

Categories

Resources