I am sure if I follow the rules but I have this error on this function
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
in this:
import React, { useState, useEffect } from 'react';
import { getAll } from './../../services/events/api';
import EventsList from '../../containers/EventsList';
import IEvent from './../../services/events/models/IEvent';
import Loading from './../../components/Loading';
import Error from './../../components/Error';
const EventList = () => {
const [events, setEvents] = useState<Array<IEvent>>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetch = async () => {
try {
setIsLoading(true);
const events: Array<IEvent> = await getAll();
setEvents(events);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
setError(error);
console.log(error);
}
};
useEffect(() => {
fetch();
}, []);
return (
<>
{error ? (
<Error title="Sorry" subtitle="Error loading events" />
) : isLoading ? (
<Loading />
) : (
<EventsList events={events} />
)}
</>
);
};
export default EventList;
where is the error? for Invalid hook call
Related
I am new to React JS, coming from iOS and so trying to implement MVVM. Not sure it's the right choice, but that's not my question. I would like to understand how react works and what am I doing wrong. So here's my code:
const ViewContractViewModel = () => {
console.log('creating view model');
const [value, setValue] = useState<string | null>(null);
const [isLoading, setisLoading] = useState(false);
async function componentDidMount() {
console.log('componentDidMount');
setisLoading(true);
// assume fetchValueFromServer works properly
setValue(await fetchValueFromServer());
console.log('value fetched from server');
setisLoading(false);
}
return { componentDidMount, value, isLoading };
};
export default function ViewContract() {
const { componentDidMount, value, isLoading } = ViewContractViewModel();
useEffect(() => {
componentDidMount();
}, [componentDidMount]);
return (
<div className='App-header'>
{isLoading ? 'Loading' : value ? value : 'View Contract'}
</div>
);
}
So here's what I understand happens here: the component is mounted, so I call componentDidMount on the view model, which invokes setIsLoading(true), which causes a re-render of the component, which leads to the view model to be re-initialised and we call componentDidMount and there's the loop.
How can I avoid this loop? What is the proper way of creating a view model? How can I have code executed once after the component was presented?
EDIT: to make my question more general, the way I implemented MVVM here means that any declaration of useState in the view model will trigger a loop every time we call the setXXX function, as the component will be re-rendered, the view model recreated and the useState re-declared.
Any example of how to do it right?
Thanks a lot!
A common pattern for in React is to use{NameOfController} and have it completely self-contained. This way, you don't have to manually call componentDidMount and, instead, you can just handle the common UI states of "loading", "error", and "success" within your view.
Using your example above, you can write a reusable controller hook like so:
import { useEffect, useState } from "react";
import api from "../api";
export default function useViewContractViewModel() {
const [data, setData] = useState("");
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
(async () => {
try {
const data = await api.fetchValueFromServer();
setData(data);
} catch (error: any) {
setError(error.toString());
} finally {
setLoading(false);
}
})();
}, []);
return { data, error, isLoading };
}
Then use it within your view component:
import useViewContractViewModel from "./hooks/useViewContractViewModel";
import "./styles.css";
export default function App() {
const { data, error, isLoading } = useViewContractViewModel();
return (
<div className="App">
{isLoading ? (
<p>Loading...</p>
) : error ? (
<p>Error: {error}</p>
) : (
<p>{data || "View Contract"}</p>
)}
</div>
);
}
Here's a demo:
On a side note, if you want your controller hook to be more dynamic and can control the initial data set, then you can pass it props, which would then be added to the useEffect dependency array:
import { useEffect, useState } from "react";
import api from "../api";
export default function useViewContractViewModel(id?: number) {
const [data, setData] = useState("");
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
(async () => {
try {
const data = await api.fetchValueFromServer(id);
setData(data);
} catch (error: any) {
setError(error.toString());
} finally {
setLoading(false);
}
})();
}, [id]);
return { data, error, isLoading };
}
Or, you return a reusable callback function that allows you to refetch data within your view component:
import { useCallback, useEffect, useState } from "react";
import api from "../api";
export default function useViewContractViewModel() {
const [data, setData] = useState("");
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState("");
const fetchDataById = useCallback(async (id?: number) => {
setLoading(true);
setData("");
setError("");
try {
const data = await api.fetchValueFromServer(id);
setData(data);
} catch (error: any) {
setError(error.toString());
} finally {
setLoading(false);
}
}, [])
useEffect(() => {
fetchDataById()
}, [fetchDataById]);
return { data, error, fetchDataById, isLoading };
}
When my component name is WithErrorHandler I get the following error:
React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
But when I change it to withErrorHandler it works fine. (first letter is lower cased)
Can someone please explain what am I doing wrong here?
import React, { useState, useEffect } from 'react';
import Modal from '../../components/UI/Modal/Modal';
import WrapperComponent from '../WrapperComponent/WrapperComponent';
const WithErrorHandler = (WrappedComponent, axios) => {
return props => {
const [error, setError] = useState(null);
const reqInterceptor = axios.interceptors.request.use(req => {
setError(null);
return req;
});
const resInterceptor = axios.interceptors.response.use(res => res, error => {
setError(error);
});
useEffect(() => {
return () => {
axios.interceptors.request.eject(reqInterceptor);
axios.interceptors.response.eject(resInterceptor);
};
}, [reqInterceptor, resInterceptor]);
const closeModalHandler = () => setError(null);
return (
<WrapperComponent>
<Modal show={error} hide={closeModalHandler}>
{error ? error.message : null}
</Modal>
<WrappedComponent {...props} />
</WrapperComponent>
)
}
}
export default WithErrorHandler;
It looks like it's tripping the safeguard about hooks even though it's still valid code.
Just name it withErrorHandler as it's not a component, it's a function returning a component, known as an Higher-Order Component (HOC).
You could also give a name to the returned component.
// Use camelCase for the HOC function.
const withErrorHandler = (WrappedComponent, axios) => {
// Use PascalCase for the name of the component itself (optional but encouraged).
return function WithErrorHandler(props) => {
// This hook is at the right place already!
const [error, setError] = useState(null);
// ...
return /*...*/
}
}
export default withErrorHandler;
Learning React Hooks, I am developing a testing app. My app loads data from an API and then populates a grid. Piece of kake. The problem shows up when I try to show a loading indicator. It enters in a re-render loop. Here is what I did:
import React, { useState, useEffect } from 'react';
function MyComp(props) {
const [loading, setLoading] = useState(false);
const [info, setInfo] = useState(null);
useEffect(() => {
setLoading(!loading);
const getData = response => {
if (response.response) {
setInfo(response.response);
} else {
console.log('there was an error');
}
setLoading(!loading);
};
fetch(URL, {})
.then(resp => ({ getData({response: resp}))
.catch(error => ({ getData({ error });
}, []);
return (
<>
{loading ? "Loading" : "Not Loading"}
</>
);
}
export default MyComp;
What am I missing here?
You're setting loading in the useEffect hook. Changing a state causes the app to rerender which also you're infinitely doing in the useEffect.
So once the app loads useEffect hook runs and changes loading, which again causes the app to re-render and the cycle continues.
import React, { useState, useEffect } from 'react';
function MyComp(props) {
const [loading, setLoading] = useState(false);
const [info, setInfo] = useState(null);
useEffect(() => {
setLoading(loading => !loading);
const getData = response => {
if (response) {
setInfo(response);
} else {
console.log('there was an error');
}
setLoading(loading => !loading);
};
fetch(URL, {})
.then(resp => getData( resp))
.catch(error => getData(error));
}, []);
return (
<>
{loading ? "Loading" : "Not Loading"}
</>
);
}
export default MyComp;
After some "try and error" (I have to admit) I found this solution:
import React, { useState, useEffect } from 'react';
function MyComp(props) {
const [loading, setLoading] = useState(false);
const [info, setInfo] = useState(null);
useEffect(() => {
setLoading(false);
}, [info]);
function getData(response) {
if (response.response) {
setInfo(response.response);
} else {
console.log('there was an error');
}
}
useEffect(() => {
setLoading(true);
fetch(URL, {})
.then(resp => ({ getData({response: resp}))
.catch(error => ({ getData({ error });
}, []);
return (
<>
{loading ? "Loading" : "Not Loading"}
</>
);
}
export default MyComp;
Maybe not the best, or prettiest, way... but it is working for me.
I need to get data from server on changes in search input but I don't want to send a new request on every new character there so I'm trying use debounce from use-debounce package https://github.com/xnimorz/use-debounce. But my code below causes only endless requests before even any changes in search input happens.
App.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import moment from "moment";
import { useDebounce } from "use-debounce";
import { Layout } from "./../Layout";
import { List } from "./../List";
import { Loader } from "./../Loader";
import { Header } from "./../Header";
import { Search } from "./../Search";
import { Licenses } from "./../Licenses";
import { Pagination } from "./../Pagination";
import "./App.css";
const PER_PAGE = 20;
export const App = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [nameSearch, setNameSearch] = useState("");
const [license, setLicense] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const debouncedNameSearch = useDebounce(nameSearch, 2000);
const fetchData = async () => {
setHasError(false);
setIsLoading(true);
try {
const prevMonth = moment()
.subtract(30, "days")
.format("YYYY-MM-DD");
const licenseKey = (license && license.key) || "";
const url = `https://api.github.com/search/repositories?q=${nameSearch}+in:name+language:javascript+created:${prevMonth}${
licenseKey ? `+license:${licenseKey}` : ""
}&sort=stars&order=desc&page=${currentPage}&per_page=${PER_PAGE}`;
const response = await axios(url);
setData(response.data.items);
setTotal(response.data.total_count);
} catch (error) {
setHasError(true);
setData([]);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, [license, nameSearch, currentPage]);
return (
<Layout>
<Header>
<Search
handleNameSearchChange={setNameSearch}
nameSearch={nameSearch}
/>
<Licenses license={license} handleLicenseChange={setLicense} />
</Header>
<main>
{hasError && <div>Error...</div>}
{isLoading && <Loader />}
{data && !isLoading && !hasError && (
<>
<List data={data} />
<Pagination
currentPage={currentPage}
total={total}
itemsPerPage={PER_PAGE}
handlePageChange={setCurrentPage}
/>
</>
)}
</main>
</Layout>
);
};
Search.js
import React from "react";
import PropTypes from "prop-types";
export const Search = ({ handleNameSearchChange, nameSearch }) => (
<div className="flex-grow-1 mx-lg-3 mb-4 mb-lg-0">
<input
type="text"
name="search"
placeholder="Enter name..."
onChange={e => handleNameSearchChange(e.target.value)}
className="form-control"
value={nameSearch}
/>
</div>
);
Search.propTypes = {
nameSearch: PropTypes.string,
handleNameSearchChange: PropTypes.func
};
How to make debounce work properly?
You never refer to debouncedNameSearch.
I think the issue is with your useEffect:
useEffect(() => {
fetchData();
}, [license, nameSearch, currentPage]);
The first issue is that it will fire every time nameSearch changes, so you should change it to use debouncedNameSearch:
useEffect(() => {
fetchData();
}, [license, debouncedNameSearch, currentPage]);
You are also firing the request on initial render when debouncedNameSearch is an empty string, so you could wrap the call to fetchData in a conditional to prevent the request firing when debouncedNameSearch === "":
useEffect(() => {
if(debouncedNameSearch) {
fetchData();
}
}, [license, debouncedNameSearch, currentPage]);
Also, your request is using nameSearch when it should be using debouncedNameSearch:
const url = `https://api.github.com/search/repositories?q=${nameSearch}...
Change to:
const url = `https://api.github.com/search/repositories?q=${debouncedNameSearch}...
And it's recommended that any function that is declared inside a component and called inside a useEffect should either be declared inside the useEffect, or set as a dependency of that useEffect:
Read the docs: is it safe to omit functions from the list of dependencies?
So you can either do something like this:
useEffect(() => {
// Declare fetchData inside useEffect
const fetchData = async () => {...};
if (debouncedNameSearch) {
// Call it inside useEffect too
fetchData();
}
}, [
// Don't forget to add the function's dependencies
license,
debouncedNameSearch,
currentPage,
setHasError,
setIsLoading,
setData,
setTotal
]);
Or you can make the function itself a dependency of the useEffect, but you should wrap the function in a useCallback to make sure its state dependencies are up to date (as per the documentation linked above):
const fetchData = useCallback(
async () => {
// Function defined here
},
[ // function dependencies
setHasError,
setIsLoading,
license,
debouncedNameSearch,
currentPage,
setData,
setTotal,
setHasError
]
);
useEffect(() => {
if(debouncedNameSearch) {
fetchData();
}
}, [license, debouncedNameSearch, currentPage, fetchData]); // Add as dependency
I'm obviously not cleaning up correctly and cancelling the axios GET request the way I should be. On my local, I get a warning that says
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
On stackblitz, my code works, but for some reason I can't click the button to show the error. It just always shows the returned data.
https://codesandbox.io/s/8x5lzjmwl8
Please review my code and find my flaw.
useAxiosFetch.js
import {useState, useEffect} from 'react'
import axios from 'axios'
const useAxiosFetch = url => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
let source = axios.CancelToken.source()
useEffect(() => {
try {
setLoading(true)
const promise = axios
.get(url, {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log(`request cancelled:${thrown.message}`)
} else {
console.log('another error happened')
}
})
.then(a => {
setData(a)
setLoading(false)
})
} catch (e) {
setData(null)
setError(e)
}
if (source) {
console.log('source defined')
} else {
console.log('source NOT defined')
}
return function () {
console.log('cleanup of useAxiosFetch called')
if (source) {
console.log('source in cleanup exists')
} else {
source.log('source in cleanup DOES NOT exist')
}
source.cancel('Cancelling in cleanup')
}
}, [])
return {data, loading, error}
}
export default useAxiosFetch
index.js
import React from 'react';
import useAxiosFetch from './useAxiosFetch1';
const index = () => {
const url = "http://www.fakeresponse.com/api/?sleep=5&data={%22Hello%22:%22World%22}";
const {data,loading} = useAxiosFetch(url);
if (loading) {
return (
<div>Loading...<br/>
<button onClick={() => {
window.location = "/awayfrom here";
}} >switch away</button>
</div>
);
} else {
return <div>{JSON.stringify(data)}xx</div>
}
};
export default index;
Here is the final code with everything working in case someone else comes back.
import {useState, useEffect} from "react";
import axios, {AxiosResponse} from "axios";
const useAxiosFetch = (url: string, timeout?: number) => {
const [data, setData] = useState<AxiosResponse | null>(null);
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let unmounted = false;
let source = axios.CancelToken.source();
axios.get(url, {
cancelToken: source.token,
timeout: timeout
})
.then(a => {
if (!unmounted) {
// #ts-ignore
setData(a.data);
setLoading(false);
}
}).catch(function (e) {
if (!unmounted) {
setError(true);
setErrorMessage(e.message);
setLoading(false);
if (axios.isCancel(e)) {
console.log(`request cancelled:${e.message}`);
} else {
console.log("another error happened:" + e.message);
}
}
});
return function () {
unmounted = true;
source.cancel("Cancelling in cleanup");
};
}, [url, timeout]);
return {data, loading, error, errorMessage};
};
export default useAxiosFetch;
Based on Axios documentation cancelToken is deprecated and starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
//...
React.useEffect(() => {
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
return () => {
controller.abort();
};
}, []);
//...
The issue in your case is that on a fast network the requests results in a response quickly and it doesn't allow you to click the button. On a throttled network which you can achieve via ChromeDevTools, you can visualise this behaviour correctly
Secondly, when you try to navigate away using window.location.href = 'away link' react doesn't have a change to trigger/execute the component cleanup and hence the cleanup function of useEffect won't be triggered.
Making use of Router works
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'
import useAxiosFetch from './useAxiosFetch'
function App(props) {
const url = 'https://www.siliconvalley-codecamp.com/rest/session/arrayonly'
const {data, loading} = useAxiosFetch(url)
// setTimeout(() => {
// window.location.href = 'https://www.google.com/';
// }, 1000)
if (loading) {
return (
<div>
Loading...
<br />
<button
onClick={() => {
props.history.push('/home')
}}
>
switch away
</button>
</div>
)
} else {
return <div>{JSON.stringify(data)}</div>
}
}
ReactDOM.render(
<Router>
<Switch>
<Route path="/home" render={() => <div>Hello</div>} />
<Route path="/" component={App} />
</Switch>
</Router>,
document.getElementById('root'),
)
You can check the demo working correctly on a slow network
Fully cancellable routines example, where you don't need any CancelToken at all (Play with it here):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios"; // cancellable axios wrapper
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
console.log("mount");
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield cpAxios(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
setText(`Failed: ${err}`);
}
return () => {
console.log("unmount");
};
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Abort</button>
</div>
);
}
This is how I do it, I think it is much simpler than the other answers here:
import React, { Component } from "react";
import axios from "axios";
export class Example extends Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
axios.get("/data").then((res) => {
if (this._isMounted && res.status === 200) {
// Do what you need to do
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return <div></div>;
}
}
export default Example;