I have a React stateless function component (SFC). I want a user to click a button and onclick a http call will be made from the SFC and when the response is received, I want to open a modal. Is it something achievable using SFC? Or do I need to keep a stateful component?
This is my code which makes the http call on load and then onClick opens the modal. But I want both the things to happen in sequence on the onClick event.
//HTTP CALL FUNCTION
function httpListCall(url) {
const [list, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
axios
.get(url)
.then(function (response) {
setData(response.data.ResultList);
})
.catch(function (error) {
setError(error);
})
}, []);
return { list, error };
};
//SFC
const ListContainer = () => {
const { list, error } = httpListCall("/list.json"); //THIS IS ON LOAD NOW - I WANT TO CALL IT onClick
const [modalShow, setModalShow] = React.useState(false);
return(
<React.Fragment>
<div>
<button onClick={() => setModalShow(true)}/> //WANT TO MAKE API CALL HERE AND THEN OPEN THE MODAL
</div>
<ModalWidget show={modalShow} list={advisorList} error={error} onHide={() => setModalShow(false)}/>
</React.Fragment>
)
}
export default ListContainer;
ReactDOM.render(<ListContainer/>, document.getElementById("app"));
I have tried to make the http call from a function but it gives me an error:
"Invalid hook call. Hooks can only be called inside of the body of a function component."
SOLUTION (updated):
You have to follow custom hooks implementation requirement as I have mentioned in comment under your question - have to use name "use" in front of your custom hook function name and update useEffect dependencies.
function useHttpListCall(url) {
const [list, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
axios
.get(url)
.then(function(response) {
setData(response.data);
})
.catch(function(error) {
setError(error);
});
}, [url]);
return { list, error };
}
And update your functional component to use your custom Hook like this.
const ListContainer = () => {
const [modalShow, setModalShow] = React.useState(false);
const [endpoint, setEndpoint] = React.useState(null);
const { list } = useHttpListCall(endpoint);
const handleClick = () => {
setModalShow(true);
setEndpoint("https://api.github.com/users?since=135");
};
return (
<React.Fragment>
<div>
<button onClick={handleClick}>Show modal of Github users</button>
</div>
{modalShow && list && (
<div>
{list.map(user => {
return <div key={user.id}>{user.login}</div>;
})}
</div>
)}
</React.Fragment>
);
};
I didn't implement modal as you did not provide the code for this component.
Fully working code is available here (for you to use as a reference): https://codesandbox.io/s/simple-custom-hook-n6ysw
You cant use the new react hooks (useState, useEffect etc.) in a "normal" function, it has to be a function component.
You can put the hooks inside the component scope and keep the axios request in a seperate function.
Related
Iam newbie and now learning to make customize react hooks
here i am trying to call the function i made in app.js file, i want to use it onClick button. but fail to do so. please help me to find the error and understand it.
import React, {
useEffect,
useState
} from "react";
const useRandomJoke = () => {
const [jokes, setJokes] = useState();
useEffect(() => {
const jokeFetch = async() => {
await fetch("https://api.icndb.com/jokes/random")
//we'll run 2 "then"
.then(
// this will give us response and will return inform of res.json
(res) => res.json()
) //.json is a format
.then((data) => {
setJokes(data.value.joke);
}); // now calling data from te returned values in res.json
};
jokeFetch();
}, []);
return jokes;
};
export default useRandomJoke;
//With onClick function
function App() { const [jokes, setJokes] = useState();
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{jokes}</p>
<button onClick={()=>{setJokes(useRandomJoke)}}>
Click for Jokes</button>
</div>
); } export default App;
`
useRandomJoke is a custom hook. Hooks should only be called at the top level of a component and as the custom hook already has the joke state, you don't need an additional state in the App component.
If you want to get a new joke after the component renders and every time the button gets clicked, you can do this:
const useRandomJoke = () => {
const [joke, setJoke] = useState("");
const fetchJoke = useCallback(() => {
fetch("https://api.icndb.com/jokes/random")
.then((res) => res.json())
.then((data) => {
setJoke(data.value.joke);
});
}, []);
return [joke, fetchJoke];
};
export default function App() {
const [joke, fetchJoke] = useRandomJoke();
useEffect(() => {
fetchJoke();
}, [fetchJoke]);
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{joke}</p>
<button onClick={fetchJoke}>Click for a random joke</button>
</div>
);
}
You can't conditionally call React hooks, like in the onClick handler of the button, as this breaks the rules of hooks. I suggest refactoring the useRandomJoke hook to return the fetched joke and a function to fetch the next random joke. You also shouldn't mix async/await with Promise chains as this is an anti-pattern.
const useRandomJoke = () => {
const [jokes, setJokes] = useState(null);
const jokeFetch = async () => {
const res = await fetch("https://api.icndb.com/jokes/random");
const data = await res.json();
setJokes(data.value.joke)
};
return [jokes, jokeFetch];
};
Then use the hook in the app.
function App() {
const [joke, getJoke] = useRandomJoke();
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{joke}</p>
<button onClick={getJoke}>Click for Joke</button>
</div>
);
}
Well, there is more than one point to talk about here:
1- in React.js, you can only call custom hooks at the top level of your function's body (react recognizes any function starting with the keyword use as a hook)
function App() {
// top level is here
const randomJokes = useRandomJoke()
const [jokes, setJokes] = useState();
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{jokes}</p>
<button onClick={()=>{setJokes(useRandomJoke)}}>
Click for Jokes
</button>
</div>
); }
export default App;
2- In your example I understand you want to have a new joke each time onClick triggers, in order to do so, I don't think using a custom hook is the ideal solution here, since your custom hook runs the fetchJokes method only once on initial render (as you described in your useEffect hook), I understand a lot of people mention that useEffect is the place to make API calls, but it doesn't necessarily applies to all use cases, in your example it is simple, you don't have to use useEffect neither create a custom hook.
a possible simple solution:
function App() {
// we always call hooks at the top level of our function
const [jokes, setJokes] = useState();
const fetchNewJoke = () => {
fetch("https://api.icndb.com/jokes/random")
//we'll run 2 "then"
.then(
// this will give us response and will return inform of
res.json
(res) => res.json()
) //.json is a format
.then((data) => {
setJokes(data.value.joke);
}); // now calling data from te returned values in res.json
};
};
return (
<div className="App">
<h1>Random Jokes</h1>
<p>{jokes}</p>
<button onClick={fetchNewJoke}>Click for Joke</button>
</div>
);
} export default App;
I'm struggling with React Hooks here. I looked online, but couldn't figure out how to adapt the examples to my code. I have the following component which triggers a "Too many re-renders" error:
const EmailVerification = () => {
const [showMessage, setShowMessage] = useState(true);
const [text, setText] = useState("...Loading. Do not close.");
const { data, error } = useQuery(VERIFY_EMAIL);
if (error) {setText(genericErrorMessage);}
if (data) {setText(emailVerificationMessage);}
return (
<Wrapper>
<Message setShowMessage={setShowMessage} text={text} />
</Wrapper>
)
}
How can I reorganize my code to avoid this error? I know that the useEffect hook should be used to perform side effects, although I wouldn't know how to use it in this case (supposing it is necessary).
The error is triggered because you are using setText directly in the render function. This function renders the component after calling it. Because in the next render, data and error are still set, it calls setText again.
You are right about useEffect. With useEffect you can make sure that the setText function is only being called when a change occurs in the data. In your case, that is for the data and/or error variables.
import { useEffect } from 'react';
const EmailVerification = () => {
const [showMessage, setShowMessage] = useState(true);
const [text, setText] = useState("...Loading. Do not close.");
const { data, error } = useQuery(VERIFY_EMAIL);
useEffect(() => {
if (error) setText('message');
if (data) setText('emailVerificationMessage');
}, [error, data]);
return (
<Wrapper>
<Message setShowMessage={setShowMessage} text={text} />
</Wrapper>
)
}
However, since you are only changing the text variable using already existing props, you can also do this in JS(X) only:
const EmailVerification = () => {
const [showMessage, setShowMessage] = useState(true);
const { isLoading, data, error } = useQuery(VERIFY_EMAIL);
const text = isLoading ? 'Loading... Do not close' : error || !data ? 'Error message' : 'emailVerificationMessage';
return (
<Wrapper>
<Message setShowMessage={setShowMessage} text={text} />
</Wrapper>
)
}
This uses a nested ternary operator (not a fan) which can be replaced with any other method.
setText will cause a rerender and will be called again on the next render. As I understand, you want to set the text once the query returns either an error or the data.
To avoid this, either use onError and onCompleted that you can pass to useQuery like so :
const { data, error } = useQuery(VERIFY_EMAIL, {
onCompleted: () => setText(emailVerificationMessage),
onError: () => setText(genericErrorMessage)
});
and remove these two lines:
if (error) {setText(genericErrorMessage);}
if (data) {setText(emailVerificationMessage);}
or call setText in a useEffect:
useEffect(() => {
if (error) {
setText(genericErrorMessage)
}
}, [error])
I am having a weird issue inside useEffect() in my React component. I have to make 2 separate axios requests to get data when the page loads. I am trying to use a hook variable to see if the data objects are populated before passing them to the JSX. Here's my current configuration:
import React, { useState, useEffect } from 'react';
import Navbar from '../components/layout/Navbar';
import ContactsCard from '../components/layout/ContactsCard';
import EmailCard from '../components/layout/EmailCard';
import MeetingsCard from '../components/layout/MeetingsCard';
import { useParams } from "react-router-dom";
import config from './../config/config';
import axios from "axios";
function SummaryPageNew() {
let { selectName } = useParams();
const [contactData, setContactData] = useState();
const [meetingData, setMeetingData] = useState();
const [loadingData, setLoadingData] = useState(true);
//API calls
async function getContactData() {
axios
.get(config.apiURL + `/affiliations/name/${selectName}`)
.then((response) => {
return setContactData(response.data[0]);
});
}
async function getMeetingData() {
axios
.get(config.apiURL + `/meetings_attendees/name/${selectName}`)
.then((response) => {
return setMeetingData(response.data);
});
}
useEffect((loadingData) => {
getContactData();
getMeetingData();
setLoadingData(false);
if (loadingData) {
//if the result is not ready so you make the axios call
getContactData();
getMeetingData();
setLoadingData(false);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div>
<Navbar />
<div>
<div style={{ textAlign: "center" }}>
<h3>Contact Information</h3>
<h5>Profile: {selectName}</h5>
</div>
{loadingData ? (
<p>Loading Please wait...</p>
) : (
<div className="row">
<ContactsCard contactData={contactData} />
<EmailCard emailData={meetingData} />
<MeetingsCard meetingData={meetingData} />
</div>
)}
</div>
</div>
)
}
export default SummaryPageNew
I have tried moving the setLoadingData(false) method inside the axios calls. If I move it inside the getMeetingData() call. This works ... sometimes. Apparently, on some occasions, it loads first and then the contactData doesn't get returned. In the current configuration, the DOM renders with "Loading Please wait...". What am I doing wrong here? How can I resolve this issue?
There are many issues with your code.
useEffect functions don't take any parameters. Your declaration of loadingData as a parameter is covering the actual loadingData variable in your component, and React will not pass a value for this.
You're missing a dependency on loadingData in your call to useEffect. As is, the function will only execute once and then never again as long as the component stays mounted. So, loadingData never gets set to false. Generally, it is a bad idea to avoid warnings about useEffect dependencies unless you have a very good reason.
My recommended solution would be to avoid storing extra state for the "loading" status. Instead, I would just check whether the two state values have been populated yet, and show the "Loading..." text if either is not.
This leaves you with:
function SummaryPageNew() {
let { selectName } = useParams();
const [contactData, setContactData] = useState();
const [meetingData, setMeetingData] = useState();
const isReady = contactData !== undefined && meetingData !== undefined;
//API calls
async function getContactData() { ... }
async function getMeetingData() { ... }
useEffect((loadingData) => {
getContactData();
getMeetingData();
}, []);
return (
<div>
<Navbar />
<div>
<div style={{ textAlign: "center" }}>
<h3>Contact Information</h3>
<h5>Profile: {selectName}</h5>
</div>
{isReady ? (
<div className="row">
<ContactsCard contactData={contactData} />
<EmailCard emailData={meetingData} />
<MeetingsCard meetingData={meetingData} />
</div>
) : (
<p>Loading Please wait...</p>
)}
</div>
</div>
)
}
react-query is a very powerful library for fetching data asynchronously using hooks. This avoids having to manage complex state which can easily fall out of sync. However, I'd learn the fundamentals of react hooks first!
You're dealing with async function calls. Javascript doesn't wait for your async functions to complete before it continues with your program. This means your calls are probably still fetching, while you already set loadingData to false. You can fix this by using Promise.all to get a callback when the async functions resolve:
//API calls
async function getContactData() {
return axios
.get(config.apiURL + `/affiliations/name/${selectName}`)
.then((response) => {
return setContactData(response.data[0]);
});
}
async function getMeetingData() {
return axios
.get(config.apiURL + `/meetings_attendees/name/${selectName}`)
.then((response) => {
return setMeetingData(response.data);
});
}
useEffect(() => {
let mounted = true
return () => { mounted = false }
Promise.all([getContactData(), getMeetingData()]).then(() => {
if (mounted) setLoadingData(false)
})
}, []); // eslint-disable-line react-hooks/exhaustive-deps
Also note the let mounted = true I've added: you want to make sure this component still exists whenever your async calls complete. If the calls take a while, it's not unthinkable you might have navigated away, for instance.
Finally, it's not a wise idea to disable react-hooks/exhaustive-deps. With a few changes you can setup your code in such a way that this ignore is no longer needed.
React want you to provide getContactData, getMeetingData in the dependency array. You can fix that by moving the data fetching function outside of you component. This means they no longer have access to the selectName variable, but you can provide that variable as an argument:
function SummaryPageNew() {
let { selectName } = useParams();
const [contactData, setContactData] = useState();
const [meetingData, setMeetingData] = useState();
const [loadingData, setLoadingData] = useState(true);
//API calls
useEffect(() => {
let mounted = true
Promise.all([
getContactData({ selectName }),
getMeetingData({ selectName })
]).then(([contactData, meetingData]) => {
if (!mounted) return
setContactData(contactData)
setMeetingData(meetingData)
setLoadingData(false)
})
return () => { mounted = false }
}, [selectName]);
return () // Render your component
}
async function getContactData({ selectName }) {
return axios
.get(config.apiURL + `/affiliations/name/${selectName}`)
.then((response) => {
return setContactData(response.data[0]);
});
}
async function getMeetingData({ selectName }) {
return axios
.get(config.apiURL + `/meetings_attendees/name/${selectName}`)
.then((response) => {
return setMeetingData(response.data);
});
}
So I am working on a small personal project that is a todo app basically, but with a backend with express and mongo. I use useEffect() to make an axios get request to the /all-todos endpoint that returns all of my todos. Then inside of useEffect()'s dependency array I specify the todos array as dependency, meaning that I want the useEffect() to cause a rerender anytime the tasks array gets modified in any way. Take a look at this:
export default function () {
const [todos, setTodos] = useState([]);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
function populateTodos () {
axios.get(`http://localhost:8000/api/all-todos/${currentUser}`)
.then(res => setTodos(res.data))
.catch(err => console.log(err));
}
populateTodos();
}, [todos]);
console.log(todos);
return (
<div className="App">
...
</div>
);
}
So I placed that console.log() there to give me a proof that the component gets rerendered, and the problem is that the console.log() gets printed to the console forever, until the browser gets slower and slower.
How can I make it so that the useEffect() gets triggered obly when todos gets changed?
You should execute the hook only if currentUser changes:
export default function () {
const [todos, setTodos] = useState([]);
const [error, setError] = useState(null);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
function populateTodos () {
axios.get(`http://localhost:8000/api/all-todos/${currentUser}`)
.then(res => setTodos(res.data))
.catch(err => setError(err));
}
populateTodos();
}, [currentUser]);
console.log(todos);
if (error) return (
<div className="App">
There was an error fetching resources: {JSON.stringify(error)}
</div>
)
return (
<div className="App">
...
</div>
);
}
How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "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."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate