How to trigger a custom hook on onClick event in react - reactjs

I want to call a custom hook that will send a email on clicking a button.
customHook.ts
async function sendEmail(userName: string, userEmail: string, userPhone: string) {
const mailToUser = {
to: userEmail,
subject: mail.subject,
body: mail.body,
};
await fetch(`/api/sendEmail`, {
method: `POST`,
headers: { 'Content-Type': `application/json` },
body: JSON.stringify(mailToUser),
});
console.log(mailToUser);
}
export default sendEmail;
This is the custom hook file that needs to be call to send the mail when the button is clicked
contact.tsx
import sendEmail from 'src'
export const Contact = (props:any) {
const userName = `Name`;
const userEmail = `email`;
const userPhone = `333333333`;
return (
<button onClick={() => sendEmail(userName, userEmail, userPhone)}>Contact</button>
)
}
The error that comes when I click the button is:
**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 can't use the hook directly like error said, but in your case, this is normally a method, This is not a hook.
Basically hook can be useful if you are managing some sort of state/context inside it to manage your application workflow and data. But if you simply want to send email only then you dont need any kind of hook for that. You can simply create a method and call that method.
Like in here in example:
// app.js
const sendEmail = async (email, subject, body) => {
const mailToUser = {
to: email,
subject: subject,
body: body
};
await fetch(`/api/sendEmail`, {
method: `POST`,
headers: { "Content-Type": `application/json` },
body: JSON.stringify(mailToUser)
});
console.log(mailToUser);
};
export default function App() {
return (
<div className="App">
<button onClick={() => sendEmail("test#gmail.com", "subject", "body")}>
Send Email
</button>
</div>
);
}
But if you're trying to implement a hook, you can simply do like that:
// useEmail.js
const useEmail = () => {
const sendEmail = async (email, subject, body) => {
const mailToUser = {
to: email,
subject: subject,
body: body
};
await fetch(`/api/sendEmail`, {
method: `POST`,
headers: { "Content-Type": `application/json` },
body: JSON.stringify(mailToUser)
});
console.log(mailToUser);
};
return { sendEmail };
};
export default useEmail;
and in your component you can implement it:
// app.js
import useEmail from "./hook/sendEmail";
export default function App() {
const { sendEmail } = useEmail();
return (
<div className="App">
<button onClick={() => sendEmail("test#gmail.com", "subject", "body")}>
Send Email
</button>
</div>
);
}

Its seems customHook.ts is not actually a hook,
Read Hooks rules at React hook Rules

It doesn't necessarily needs to be a custom hook in this case, but in case if you want to stick this with the approach, we can try to return a function from your customHook which invokes the API call.
May be this is how we can tackle this:
useSendEmail.ts
const useSendEmail = () => {
const sendEmail = async(userName: string, userEmail: string, userPhone: string) {
const mailToUser = {
to: email,
subject: subject,
body: body
};
try {
await fetch(`/api/sendEmail`, {
method: `POST`,
headers: { 'Content-Type': `application/json` },
body: JSON.stringify(mailToUser),
});
} catch (error) {
// Include your error handling here.
console.log(error);
}
}
return { sendEmail };
}
Now, you can use it in your component in this way:
contact.tsx
import { useSendEmail } from './useSendEmail.ts';
export const Contact = (props:any) {
const { sendEmail } = useSendEmail(); // Receiving the function from hook here.
const userName = `Name`;
const userEmail = `email`;
const userPhone = `333333333`;
const onClickHandler = () => {
sendEmail(userName, userEmail, userPhone)
}
return (
<button onClick={onClickHandler}>Contact</button>
)
}

Related

React and Websocket messaging to server

So I'm having issues with a single component that displays a list pulled from a resource server. Then it uses Stompjs to establish a websocket and send messages. When I load the client, the Dev Console shows logs that it tries to call onConnected method() twice as my logs show two newUser messages sent from a single load of the component.
When I try to call the submitBid() method it throws a type error saying that
"TypeError: Cannot read properties of undefined (reading 'send')
at submitBid (AuctionList.js:76:1)"
Which I'm not sure why it would be undefined on that line when it's defined and running fine in the function on line 36 which runs before the method that fails. I've been stuck on this for several days so hopefully someone can tell me what I've got wrong in the code... Here is the component code....
import React from 'react'
import Stomp from 'stompjs';
import SockJS from 'sockjs-client';
import {useState, useEffect } from 'react';
function AuctionList({user, authCredentials, token}) {
const [auctionItems, setAuctionItems] = useState([]);
const [userData, setUserData] = useState({
email: user.email,
name: user.name,
message: ''
});
const [bid, setBid] = useState(0.00);
let stompClient;
let socket;
const connect = async () => {
socket = new SockJS('http://localhost:8080/ws')
stompClient = Stomp.over(socket)
stompClient.connect({}, onConnected, onError)
}
const onConnected = async () => {
stompClient.subscribe('/topic/bids', onMessageReceived)
stompClient.send("/app/socket.newUser",
{},
JSON.stringify({
sender: user.name,
type: 'NEW_USER',
time: Date.now()
})
)
}
const onError = async (err) => {
console.log(err);
}
const handleChange = async (e) =>{
setBid(e.target.value);
}
const submitBid = async (item) => {
let newMessage = {
type: "BID",
newBid: {
itemId: item.id,
email: user.email,
bidPrice: bid,
bidTime: new Date().getTime()
},
sender: userData.email,
time: new Date().getTime()
};
try {
stompClient.send("/socket.send", {}, JSON.stringify(newMessage));
} catch(err){
console.log(err); }
}
const onMessageReceived = async (payload)=>{
console.log("onMessageReceived")
console.log(payload)
}
const getAuctionList = async () => {
const url = "http://localhost:8080/auctionlist";
const init = {
method: "GET",
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${token}`, // notice the Bearer before your token
},
};
fetch(url, init)
.then(response => response.json())
.then(response => {setAuctionItems(response)})
};
useEffect( () => {
getAuctionList();
connect();
}, []);
return (
<ul>
{auctionItems.map( item => {
return( <div key={item.id} className = "auctionItemComponent">
<h3>{item.name}</h3>
<span>{item.desc}</span>
<span>Current Bid: ${item.itemStartingPrice}</span>
<span>Minimum Bid: {item.itemMinBid}</span>
<span>Time left</span>
<input type="number" id="bidInput_" name="bidInput" onChange={handleChange} ></input>
<button type='submit' onClick={submitBid}>Submit bid</button>
</div>)
})}
</ul>
)
}
export default AuctionList
Also I realize I have a bunch of async functions that don't have any awaits. I tried adding those in, but it was no change.
The issue here is not with stompjs but with the scoping. You have stompClient inside React Component but the one from submitBid is different. You can do it in different ways.
Put stompjs in global stage as in example here: https://playcode.io/972045
You can use useRef to have the client inside the React Component and have React do the tracking of any modifications.
I personally think something like a "connection" should stay away from inside a React Component. You should have the connection configs in a different file and import an instance to the JSX file.

json response from mock server not printing but is in the console

I am trying to learn react, and I am making a successful API call, but it only prints in the console. I found examples but many of them recommended to use setData(json) but I am not able to use it because the file is a list of export async function which was also recommended.
export async function GetHellWorld() {
return fetch(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => {
console.log(json)
})
.catch(error => (console.log(error)))
}
and the component
function Test(thisArg, argArray) {
const result = GetHellWorld.apply()
return (
<div className="App">
{JSON.stringify(result)}
</div>
);
}
export default Test;
In the console I see "Hello World" but in the browser is get just {}.
Two questions:
How can I bind the JSON response to an object so I can do something like result.name.
Is this the correct was to call the await function? const result = GetHellWorld.apply()
---- update ----
I decided to try axios because I want to make multiple calls in one file.
const axios = require('axios');
export class AppService {
public async GetHelloWorld(): Promise<any> {
const response = await axios.get(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).catch(() => console.log("Issue in GetHelloWorld"))
return response.data
}
}
component
import React from 'react';
import {AppService} from "../services/app.service";
function Movies() {
const api = new AppService()
const hello = async () => {
const response = await api.GetHelloWorld();
console.log("The response: " + response)
}
return (
<div className="App">
{JSON.stringify(hello)}
</div>
);
}
note I had to add typescript support.
For whatever reason I get
Module not found: Error: Can't resolve '../services/app.service' in '/Users/miketye/programming/test-react/src/components'
While the other answer about using a custom hook can work, I would not recommend it while you're still leaning React.
Look up how to use the "useEffect" hook, that's generally how you want to do any sort of loading logic in React.
First off, you need to fix your async function so it actually returns a value:
// style/convention note, but non-component functions should not start with a capital letter
export async function getHelloWorld() {
return fetch(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => {
return json // will cause this function to return a Promise of type "string", since we're in an async function
})
// better to just let the error get thrown here, for testing
}
Then use it like this:
function Test(thisArg, argArray) {
[fetchResult, setFetchResult] = useState(undefined) // look up useState. State is how you have values that change over time in a resct component
useEffect(() => {
async function fetchData() {
const data = await getHelloWorld()
setFetchResult(data)
}
fetchData()
}, [])
// look up useEffect. Since the second argument (the "dependency array") is empty, useEffect will fire only once, after the component loads
return (
<div className="App">
{result ? JSON.stringify(result) : "no result yet"}
</div>
);
}
export default Test;
You can use a custom hook for this purpose:
import { useState } from "react";
const useFetchData = () => {
const [data, setData] = useState(null);
const fetchData = () => {
fetch("http://localhost:8080/api", {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => { setData(json); })
.catch(error => { console.log(error); })
}
useEffect(() => {
fetchData();
}, []);
return { data, fetchData };
}
export default useFetchData;
And then call it in your component:
import useFetchData from "#/hooks/useFetchData";
const Test = () => {
const { data, fetchData } = useFetchData();
// CALL fetchData IF YOU WANT TO UPDATE THE CURRENT STATE
return (
<div className="App">
{data && JSON.stringify(data)}
</div>
);
}
export default Test;

Next.js - React Custom Hook throws Invalid hook call

Hi I am quite new to react and this is for a learning project.
In react under next.js want to check for the existence of a certain folder on the server. To achieve that I implemented an api twigExists.js in pages/api and a custom hook twigExistsRequest.js in the library folder:
import {useEffect, useRef} from "react";
import {webApiUrl} from "#/library/webHelpers";
export function useTwigExistsRequest({
parentDirSegment,
name,
action,
treeStateDispatch
}) {
const nameExists = useRef('not');
useEffect(() => {
if ('' !== name) {
async function fetchNameValidation() {
try {
const response = await fetch(
webApiUrl() + '/branchName',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({parentDirSegment, name})
}
);
const answer = await response.json();
if (undefined !== answer['exists']) {
nameExists.current = answer['exists'];
}
else if (undefined !== answer['err']) {
console.log(answer['err']);
}
} catch (err) {
console.log(err);
}
}
fetchNameValidation().then(() => {
nameExists.current === 'exists'
&& treeStateDispatch({
action,
name,
dirSegment: parentDirSegment
});
})
}
});
}
The following error is thrown at the useRef line, line 10:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component.
I am using an almost identical approach to get the structure of a special folder with its subfolders and it is working fine. Working example:
import {useEffect, useRef} from "react";
import {webApiUrl} from "#/library/webHelpers";
export default function useChangeBranchRequest({
data,
setData
}) {
let postData;
const storeEffect = useRef(0);
if ('skip' !== data) {
storeEffect.current += 1;
postData = JSON.stringify(data);
}
useEffect(() => {
if (0 !== storeEffect.current) {
async function fetchData() {
try {
const response = await fetch(
webApiUrl() + '/changeBranch',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: postData
});
const json = await response.json();
setData(JSON.parse(json['tree']));
} catch (error) {
console.log(error);
}
}
fetchData()
.then(() => {
return (<></>);
});
}
}, [storeEffect.current]);
}
I can't see: What is wrong in the first example??
Edit due to question: useTwigExistsRequest is called from this file:
import {useTwigExistsRequest} from "#/library/twigExistsRequest";
export default function twigExistsHandler({
parentDirSegment,
name,
action,
treeStateDispatch
}) {
useTwigExistsRequest({
parentDirSegment,
action,
name,
treeStateDispatch
});
}
trying to avoid a direct call from:
import {ACTIONS} from "#/library/common";
import {useState} from "react";
import twigExistsHandler from "#/library/twigExistsHandler";
export default function PlgButton({dirSegment, action, text, treeStateDispatch}) {
const [info, SetInfo] = useState('');
const [parentDirSegment, SetParentDirSegment] = useState('');
// name validation, triggered by SetInfo. Returns strings 'false' or 'true':
// reset Button after execution
if (info) SetInfo('');
return (
<button
className="btn btn-secondary btn-sm new-plg-btn"
onClick={() => {
clickHandler(action);
}}
>
{text}
</button>
);
function clickHandler(action) {
let name;
switch (action) {
case ACTIONS.add:
name = window.prompt('New name:');
twigExistsHandler({
parentDirSegment: dirSegment,
name,
action,
treeStateDispatch
});
break;
case ACTIONS.dup:
name = window.prompt('Dup name:');
twigExistsHandler({
parentDirSegment: dirSegment.slice(0,dirSegment.lastIndexOf('/')),
name,
action,
treeStateDispatch
});
break;
case ACTIONS.del:
window.confirm('Irrevocably delete the whole playground?')
&& treeStateDispatch({
info: '',
dirSegment,
action
});
break;
}
}
}

Embed code onto another page and use their variables

I have created a web app in React with several pages. To make it a bit secure for our department, at least so everyone can't enter the site and see what's going on, there are some code that does some checks, and also creates some variables I can use for each of my pages.
The issue is that this code is the same on every single page.
So basically it looks something like:
const MyApp= () => {
const { instance, accounts, inProgress } = useMsal();
const [accessToken, setAccessToken] = useState<any>(null);
const name = accounts[0] && accounts[0].name;
const email = accounts[0] && accounts[0].username;
const isAuthenticated = useIsAuthenticated();
useEffect(() => {
const request = {
...loginRequest,
account: accounts[0],
};
instance
.acquireTokenSilent(request)
.then((response: any) => {
setAccessToken(response.accessToken);
})
.catch(() => {
instance.acquireTokenPopup(request).then((response: any) => {
setAccessToken(response.accessToken);
});
});
}, [isAuthenticated]);
function POST(path: string, data: any) {
return fetch(`${fetchUrl}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: accessToken,
},
body: JSON.stringify(data),
});
}
<something else>
return (
<>
<div>{name}</div>
<div>{email}</div>
</>
);
};
export default MyApp;
And it's just annoying to write this on every page. So is there some way to create some kind of component, and then embed it into all pages, where I still have the option to use the const variables created ?

Access React context in an API service

In my React application I use the context API to store the user information through the useContext hook:
const AuthContext = createContext<AuthContextType>(null!);
const useAuth = () => useContext(AuthContext);
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User>();
// Implementations of values
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export { AuthProvider, useAuth };
Accessing the auth information works all fine and dandy in the components:
export default function CoolComponent() {
const auth = useAuth();
if (auth.user) {
// Do something
}
return <div>Hello {auth.user}</div>;
}
The thing is that my jwt-token is stored in the user object and I need it for my API calls in my service, but hooks are not allowed outside functional components. Can I circumvent this in a clever way? Some things that I can think of is to pass the token on every call to the service (not very DRY) or save the token in localStorage and then retrieve it from there in the service, but it seems unnecessary to store the same information in two different places?
Update:
Now with the service code:
const baseUrl = environment.apiUrl;
function getToken() {
// This is what I would like to get some help with
}
const headers = {
...(getToken() && { Authorization: `Bearer ${getToken()}` }),
"Content-Type": "application/json",
};
function getAllProjects(): Promise<IProject[]> {
return fetch(`${baseUrl}projects`, {
headers,
}).then((response) => response.json());
}
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
// + many more
export { getAllProjects, createProject };
Calling the service in a component:
useEffect(() => {
const fetchProjects = async () => {
setIsLoading(true);
try {
const allProjects = await getAllProjects();
setProjects(allProjects);
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
fetchProjects();
}, []);
The React documentation says that you cannot call hooks inside JavaScript functions.
What can you do?
Use custom hooks. rename functions as useCreateProject and return your function. Then you will be able to call useAuth inside your custom hook:
const useCreateProject =() =>{
const {user} = useAuth();
function createProject(project: CreateProjectDTO): Promise<IProject> {
return fetch(`${baseUrl}projects`, {
method: "POST",
headers,
body: JSON.stringify(project),
}).then((response) => response.json());
}
return createProject
}
Then call it like this:
const createProject = useCreateProject()
useEffect(() => {
const create = async () => {
setIsLoading(true);
try {
await createProject()
} catch (error) {
// Handle error
} finally {
setIsLoading(false);
}
};
create();
}, []);
But my advice is to store the token on localStorage or in cookies. Context data will be lost when user refreshes page. However, if that is not case for you, you can continue using context.

Resources