Wagmi NextJS App is sending a transaction without a button click - reactjs

I have the following NextJS App (using reactjs, ethers.js, wagmi and connectkit), that just sends Eth from one connected Metamask Wallet to another Metamask Wallet, both of which I have access to. The problem is that before clicking the send button, it is automatically sending the transaction. I did a workaround where I have a boolean (hasClicked) that stops the transaction from taking place, but it seems a bit hamfisted. I am going off of the sendTransaction found here: https://wagmi.sh/examples/send-transaction
Below is my code:
// comps/transaction.tsx
import * as React from 'react';
import { useDebounce } from 'use-debounce';
import { usePrepareSendTransaction, useSendTransaction, useWaitForTransaction } from 'wagmi'
import { utils } from 'ethers';
export function SendTransaction() {
let hasClicked = false;
const [to, setTo] = React.useState('');
const [debouncedTo] = useDebounce(to, 500);
const [amount, setAmount] = React.useState('');
const [debouncedAmount] = useDebounce(amount, 500);
const { config } = usePrepareSendTransaction({
request: {
to: to,
value: (amount && hasClicked) ? utils.parseEther(amount) : undefined,
},
});
const { data, sendTransaction } = useSendTransaction(config);
const { isLoading, isSuccess } = useWaitForTransaction({
hash: data?.hash,
});
return (
<div>
<input
aria-label="Recipient"
onChange={(e) => {
console.log("Address Set:", e.target.value);
hasClicked = false;
e.preventDefault();
setTo(e.target.value);
}}
placeholder="0xA0Cf…251e"
value={to}
/>
<input
aria-label="Amount (ether)"
onChange={(e) => {
console.log("Amount Set:", e.target.value);
e.preventDefault();
hasClicked = false;
setAmount(e.target.value);
}}
placeholder="0.05"
value={amount}
/>
<button
onClick={(e) => {
e.preventDefault();
hasClicked = true;
sendTransaction?.();
}}
disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send'}
</button>
{isSuccess && (
<div>
Successfully sent {amount} ether to {to}
<div>
<a href={`https://etherscan.io/tx/${data?.hash}`}>Etherscan</a>
</div>
</div>
)}
</div>
)
}
// pages/_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { WagmiConfig, createClient, useAccount, useConnect, useDisconnect, goerli } from 'wagmi';
import { mainnet, polygon, optimism, arbitrum } from 'wagmi/chains';
import { ConnectKitProvider, getDefaultClient } from 'connectkit';
import { SendTransaction } from '../comps/transaction';
const client = createClient(
getDefaultClient({
appName: 'ConnectKit Next.js demo',
//infuraId: process.env.NEXT_PUBLIC_INFURA_ID,
//alchemyId: process.env.NEXT_PUBLIC_ALCHEMY_ID,
chains: [mainnet, polygon, optimism, arbitrum, goerli],
})
);
function MyApp({ Component, pageProps }: AppProps) {
const { isConnected } = useAccount();
const loading = (
<div>
<SendTransaction />
</div>
);
return (
<WagmiConfig client={client}>
<ConnectKitProvider>
{isConnected ? (loading) : (<Component {...pageProps} />)}
</ConnectKitProvider>
</WagmiConfig>
);
}
export default MyApp;
// pages/index.tsx
import type { NextPage } from 'next';
import { ConnectKitButton } from 'connectkit';
const Home: NextPage = () => {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<ConnectKitButton />
</div>
);
};
export default Home;
Again, without the hasClicked, the transaction is automatically sent.
I would also like to validate the constants "to" and "amount" before the transaction is sent, as this would be best practice, and would also like the "Send" button disabled until to and amount are validated as correct.
I am positive there must be a better way of doing this, but I am just not sure what that is.

Related

How to reassign a ref to a component in App.js

I am new to react and i am trying to get the reference value from DisplayMapClass bellow is the code. The main objective is to get the MAP object from the bellow code.
DisplayMapClass.js
import * as React from 'react';
export class DisplayMapClass extends React.Component {
mapRef = React.createRef();
state = {
map: null
};
componentDidMount() {
const H = window.H;
const platform = new H.service.Platform({
apikey: "{enter api key}"
});
const defaultLayers = platform.createDefaultLayers();
const map = new H.Map(
this.mapRef.current,
defaultLayers.vector.normal.map,
{
center: { lat: 50, lng: 5 },
zoom: 4,
pixelRatio: window.devicePixelRatio || 1
}
);
const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
const ui = H.ui.UI.createDefault(map, defaultLayers);
this.setState({ map });
}
componentWillUnmount() {
this.state.map.dispose();
}
render() {
return (
<div ref={this.mapRef} style={{ height: "500px" }} />
);
}
}
When I reassign the ref the whole page breaks so i don't know what i am doing wrong.
const map = <DisplayMapClass ref={"newRef"}/>
Bellow is the code for App.js
App.js
import logo from './logo.svg';
import './App.css';
import React, { useEffect, useState } from 'react';
import axios from 'axios'
import Button from '#mui/material/Button';
import TextField from '#mui/material/TextField';
import {BounceLoader, BarLoader, BeatLoader} from 'react-spinners'
import {DisplayMapClass} from './DisplayMapClass';
const H = window.H;
function App() {
const [isLoading, setLoading] = useState(false)
const map = <DisplayMapClass ref={"newRef"}/>
const asyncFunc = async () => {
setLoading(true);
const response = await fetch('http://000.000.000.000:000/test/8.8.8.8' );
const data = await response.json();
for(var x in data){
addMarker(map, x["lat"], x["lng"])
}
setLoading(false)
}
const addMarker = (map, x, y) => {
var marker = new H.map.Marker({"lat": x, "lng": y})
map.addObject(marker);
}
return (
<div className="App">
{ isLoading ?
<header className="App-header">
<p>please type an ip address and when done press the button</p>
<TextField
sx=
{
{
input:
{
color: 'white'
}, root:
{
color: 'white'
}, floatingLabelFocusStyle:
{
color: "white"
}
}
}
id="ipTextArea" label="Enter an IP address" variant="filled"
/>
<Button onClick={asyncFunc} variant="outlined">Map My Stalkers</Button>
<BarLoader/>
</header>
:
<header className="App-header">
<p>please type an ip address and when done press the button</p>
<TextField
sx=
{
{
input:
{
color: 'white'
}, root:
{
color: 'white'
}, floatingLabelFocusStyle:
{
color: "white"
}
}
}
id="ipTextArea" label="Enter an IP address" variant="filled"
/>
<Button onClick={asyncFunc} variant="outlined">Map My Stalkers</Button>
</header>
}
<p> <br /> </p>
<div> {map} </div>
</div>
);
}
export default App;

How can i pass a state to the file app.js in react?

I am new to react, i have this code which i got from my manager, i have to add an event, actually on button click i have to add a class to other component, the button is in Header component, and the class i have to add is in LeftNav.
Here is my header.js
export class Header extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
anchorEl: null,
gettingUser: false,
loggingOut: false,
menuOpen: false
};
this.cookies = new Cookies();
}
i made a state menuOpen.
and tried setting state to true like this in header.js
<Button className="menu-btn-wrapper header-button" onClick={() => self.setState({ menuOpen: true })}>
<i className="icon circle zf-cs-icon-burger"></i>
</Button>
and moving on to my app.js, i have this
<Header history={history} />
<LeftNav className="leftFixed leftNav" />
How can i get the state of menuOpen from Header here?
i tried
<Header history={history} menuOpen={this.state.menuOpen} />
but i am getting a stateless error.
There is no class present in app.js
Here is my full app.js
import React, { useEffect } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import IdleTimer from 'react-idle-timer';
import Cookies from 'universal-cookie';
import { LinearProgress, IconButton, Tooltip } from '#material-ui/core';
import { Cancel as CancelIcon } from '#material-ui/icons';
import { getToken, updateMaxAge, expirySet } from '../auth';
import { cancelFileUpload, setSnackbar } from '../actions/appActions';
import { logoutUser, saveUserSettings, setAccountActivationCode, setShowConnectingDialog } from '../actions/userActions';
import { privateRoutes, publicRoutes } from '../config/routes';
import { onboardingSteps } from '../constants/onboardingConstants';
import SharedSnackbar from '../components/common/sharedsnackbar';
import ErrorBoundary from '../errorboundary';
import { getOnboardingState } from '../selectors';
import { colors } from '../themes/mender-theme';
import Tracking from '../tracking';
import { getOnboardingComponentFor } from '../utils/onboardingmanager';
import LiveChatBox from './livechatbox';
import ConfirmDismissHelptips from './common/dialogs/confirmdismisshelptips';
import DeviceConnectionDialog from './common/dialogs/deviceconnectiondialog';
import Header from './header/header';
import LeftNav from './leftnav';
import 'react-perfect-scrollbar/dist/css/styles.css';
import PerfectScrollbar from 'react-perfect-scrollbar';
const activationPath = '/activate';
const timeout = 900000; // 15 minutes idle time
const cookies = new Cookies();
export const AppRoot = ({
cancelFileUpload,
currentUser,
history,
logoutUser,
onboardingState,
setAccountActivationCode,
setShowConnectingDialog,
showDeviceConnectionDialog,
showDismissHelptipsDialog,
setSnackbar,
snackbar,
trackingCode,
uploadProgress
}) => {
useEffect(() => {
if (trackingCode) {
if (!cookies.get('_ga')) {
Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
if (trackingConsentGiven) {
Tracking.initialize(trackingCode);
Tracking.pageview();
}
});
} else {
Tracking.initialize(trackingCode);
}
}
history.listen(trackLocationChange);
trackLocationChange(history.location);
}, []);
const trackLocationChange = location => {
// if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
let page = location.pathname || '';
if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
const splitter = page.lastIndexOf('/');
const filters = page.slice(splitter + 1);
const keyOnlyFilters = filters.split('&').reduce((accu, item) => `${accu}:${item.split('=')[0]}&`, ''); // assume the keys to filter by are not as revealing as the values things are filtered by
page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
} else if (page.startsWith(activationPath)) {
setAccountActivationCode(page.substring(activationPath.length + 1));
history.replace('/settings/my-profile');
}
Tracking.pageview(page);
};
const onIdle = () => {
if (expirySet() && currentUser) {
// logout user and warn
return logoutUser('Your session has expired. You have been automatically logged out due to inactivity.').catch(() => updateMaxAge());
}
};
const onboardingComponent = getOnboardingComponentFor(onboardingSteps.ARTIFACT_CREATION_DIALOG, onboardingState);
const containerProps = getToken() ? { id: 'app' } : { className: 'flexbox centered', style: { minHeight: '100vh' } };
return (
<div {...containerProps}>
{getToken() ? (
<>
<IdleTimer element={document} onAction={updateMaxAge} onIdle={onIdle} timeout={timeout} />
<Header history={history} />
<LeftNav className="leftFixed leftNav" />
<PerfectScrollbar className="rightFluid container main">
<ErrorBoundary>{privateRoutes}</ErrorBoundary>
</PerfectScrollbar>
{onboardingComponent ? onboardingComponent : null}
{showDismissHelptipsDialog && <ConfirmDismissHelptips />}
{showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => setShowConnectingDialog(false)} />}
{
// eslint-disable-next-line no-undef
ENV === 'production' && <LiveChatBox />
}
</>
) : (
publicRoutes
)}
<SharedSnackbar snackbar={snackbar} setSnackbar={setSnackbar} />
{Boolean(uploadProgress) && (
<div id="progressBarContainer">
<p className="align-center">Upload in progress ({Math.round(uploadProgress)}%)</p>
<LinearProgress variant="determinate" style={{ backgroundColor: colors.grey, gridColumn: 1, margin: '15px 0' }} value={uploadProgress} />
<Tooltip title="Abort" placement="top">
<IconButton onClick={cancelFileUpload}>
<CancelIcon />
</IconButton>
</Tooltip>
</div>
)}
</div>
);
};
const actionCreators = { cancelFileUpload, logoutUser, saveUserSettings, setAccountActivationCode, setShowConnectingDialog, setSnackbar };
const mapStateToProps = state => {
return {
currentUser: state.users.currentUser,
onboardingState: getOnboardingState(state),
showDismissHelptipsDialog: !state.onboarding.complete && state.onboarding.showTipsDialog,
showDeviceConnectionDialog: state.users.showConnectDeviceDialog,
snackbar: state.app.snackbar,
trackingCode: state.app.trackerCode,
uploadProgress: state.app.uploadProgress
};
};
export default compose(withRouter, connect(mapStateToProps, actionCreators))(AppRoot);
and in main.js
export const Main = () =>
render(
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ErrorBoundary>
<Router basename="/ui/#">
<App />
</Router>
</ErrorBoundary>
</MuiThemeProvider>
</Provider>,
document.getElementById('main') || document.createElement('div')
);
Main();
The root component is AppRoot, that is a Functional Component.
define the menuOpen state in AppRoot with useState:
const [menuOpen,setMenuOpen]=useState(false)
then define a function in AppRoot to handle menuOpen changes:
const handleMenuOpen=()=>{
setMenuOpen(true)
}
then pass the handler function to the Header via props:
<Header history={history} onButtonClick={handleMenuOpen} />
inside the Header:
<Button className="menu-btn-wrapper header-button" onClick={() => this.props.onButtonClick()}>
<i className="icon circle zf-cs-icon-burger"></i>
</Button>
AppRoot after all above steps:
export const AppRoot = ({
cancelFileUpload,
currentUser,
history,
logoutUser,
onboardingState,
setAccountActivationCode,
setShowConnectingDialog,
showDeviceConnectionDialog,
showDismissHelptipsDialog,
setSnackbar,
snackbar,
trackingCode,
uploadProgress
}) => {
const [menuOpen,setMenuOpen]=useState(false)
const handleMenuOpen=()=>{
setMenuOpen(true)
}
useEffect(() => {
if (trackingCode) {
if (!cookies.get('_ga')) {
Tracking.cookieconsent().then(({ trackingConsentGiven }) => {
if (trackingConsentGiven) {
Tracking.initialize(trackingCode);
Tracking.pageview();
}
});
} else {
Tracking.initialize(trackingCode);
}
}
history.listen(trackLocationChange);
trackLocationChange(history.location);
}, []);
const trackLocationChange = location => {
// if we're on page whose path might contain sensitive device/ group/ deployment names etc. we sanitize the sent information before submission
let page = location.pathname || '';
if (page.includes('=') && (page.startsWith('/devices') || page.startsWith('/deployments'))) {
const splitter = page.lastIndexOf('/');
const filters = page.slice(splitter + 1);
const keyOnlyFilters = filters.split('&').reduce((accu, item) => `${accu}:${item.split('=')[0]}&`, ''); // assume the keys to filter by are not as revealing as the values things are filtered by
page = `${page.substring(0, splitter)}?${keyOnlyFilters.substring(0, keyOnlyFilters.length - 1)}`; // cut off the last & of the reduced filters string
} else if (page.startsWith(activationPath)) {
setAccountActivationCode(page.substring(activationPath.length + 1));
history.replace('/settings/my-profile');
}
Tracking.pageview(page);
};
const onIdle = () => {
if (expirySet() && currentUser) {
// logout user and warn
return logoutUser('Your session has expired. You have been automatically logged out due to inactivity.').catch(() => updateMaxAge());
}
};
const onboardingComponent =
getOnboardingComponentFor(onboardingSteps.ARTIFACT_CREATION_DIALOG, onboardingState);
const containerProps = getToken() ? { id: 'app' } : { className: 'flexbox centered', style: { minHeight: '100vh' } };
return (
<div {...containerProps}>
{getToken() ? (
<>
<IdleTimer element={document} onAction={updateMaxAge} onIdle={onIdle} timeout={timeout} />
<Header history={history} onButtonClick={handleMenuOpen} />
<LeftNav className="leftFixed leftNav" />
<PerfectScrollbar className="rightFluid container main">
<ErrorBoundary>{privateRoutes}</ErrorBoundary>
</PerfectScrollbar>
{onboardingComponent ? onboardingComponent : null}
{showDismissHelptipsDialog && <ConfirmDismissHelptips />}
{showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => setShowConnectingDialog(false)} />}
{
// eslint-disable-next-line no-undef
ENV === 'production' && <LiveChatBox />
}
</>
) : (
publicRoutes
)}
<SharedSnackbar snackbar={snackbar} setSnackbar={setSnackbar} />
{Boolean(uploadProgress) && (
<div id="progressBarContainer">
<p className="align-center">Upload in progress ({Math.round(uploadProgress)}%)</p>
<LinearProgress variant="determinate" style={{ backgroundColor: colors.grey, gridColumn: 1, margin: '15px 0' }} value={uploadProgress} />
<Tooltip title="Abort" placement="top">
<IconButton onClick={cancelFileUpload}>
<CancelIcon />
</IconButton>
</Tooltip>
</div>
)}
</div>
);
};

How to match an element with data clicked in react hooks?

I am trying to create a modal that will display all the data on the element clicked.
For exemple, if i clicked the id(367) of a movie, it will display the title, the genres and the released date.
First i created a Context.js that will send data in the component Movie.js:
Context.js
import React, { useState, useEffect, createContext } from "react";
import { getAxios } from "../Function/index";
const MovieContext = createContext();
const MovieProvider = ({ children }) => {
const [datas, setDatas] = useState([]);
const [testdatas, testsetDatas] = useState([]);
const getMovie = async () => {
const data = await getAxios(
"https://api.themoviedb.org/3/movie/upcoming?api_key={}"
);
setDatas(data.results);
};
useEffect(() => {
getMovie();
}, []);
return (
<MovieContext.Provider value={{ datas, testdatas }}>{children}</MovieContext.Provider>
);
};
export { MovieContext, MovieProvider };
Movie.js
import React, { useContext } from "react";
import { MovieContext } from '../Context'
import { Card, Row, Tag } from 'antd'
const Main = () => {
const { datas } = useContext(MovieContext)
return (
<Row gutter={16}>
{datas.map((item, id) => (
<Card
key={id}
style={{ width: 300, margin: 10 }} bordered={true}
hoverable
>
<div>
<p style={{ textAlign: "left" }}>{item.title} <span style={{ float: "right" }}>{item.vote_average}</span></p>
</div>
<p><img src={`https://image.tmdb.org/t/p/w500/${item.poster_path}`} alt="#" width="200" height="200" /></p>
<Tag color="red"> {item.title}</Tag>
</Card>
))}
</Row >
);
};
export default Main;

document.querySelector() always return null when clicking on React Router Link the first time but will return correctly after

I'm stucking on a problem with React-Router and querySelector.
I have a Navbar component which contains all the CustomLink components for navigation and a line animation which listens to those components and displays animation according to the current active component.
// Navbar.tsx
import React, { useCallback, useEffect, useState, useRef } from "react";
import { Link, useLocation } from "react-router-dom";
import CustomLink from "./Link";
const Layout: React.FC = ({ children }) => {
const location = useLocation();
const navbarRef = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState({ left: 0, width: 0 });
const handleActiveLine = useCallback((): void => {
if (navbarRef && navbarRef.current) {
const activeNavbarLink = navbarRef.current.querySelector<HTMLElement>(
".tdp-navbar__item.active"
);
console.log(activeNavbarLink);
if (activeNavbarLink) {
setPos({
left: activeNavbarLink.offsetLeft,
width: activeNavbarLink.offsetWidth,
});
}
}
}, []);
useEffect(() => {
handleActiveLine();
}, [location]);
return (
<>
<div className="tdp-navbar-content shadow">
<div ref={navbarRef} className="tdp-navbar">
<div className="tdp-navbar__left">
<p>Todo+</p>
<CustomLink to="/">About</CustomLink>
<CustomLink to="/login">Login</CustomLink>
</div>
<div className="tdp-navbar__right">
<button className="tdp-button tdp-button--primary tdp-button--border">
<div className="tdp-button__content">
<Link to="/register">Register</Link>
</div>
</button>
<button className="tdp-button tdp-button--primary tdp-button--default">
<div className="tdp-button__content">
<Link to="/login">Login</Link>
</div>
</button>
</div>
<div
className="tdp-navbar__line"
style={{ left: pos.left, width: pos.width }}
/>
</div>
</div>
<main className="page">{children}</main>
</>
);
};
export default Layout;
// CustomLink.tsx
import React, { useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router-dom";
interface Props {
to: string;
}
const CustomLink: React.FC<Props> = ({ to, children }) => {
const location = useLocation();
const history = useHistory();
const [active, setActive] = useState(false);
useEffect(() => {
if (location.pathname === to) {
setActive(true);
} else {
setActive(false);
}
}, [location, to]);
return (
// eslint-disable-next-line react/button-has-type
<button
className={`tdp-navbar__item ${active ? "active" : ""}`}
onClick={(): void => {
history.push(to);
}}
>
{children}
</button>
);
};
export default CustomLink;
But it doesn't work as I want. So I opened Chrome Devtool and debugged, I realized that when I clicked on a CustomLink first, the querySelector() from Navbar would return null. But if I clicked on the same CustomLink multiple times, it would return properly, like the screenshot below:
Error from Chrome Console
How can I get the correct return from querySelector() from the first time? Thank you!
It's because handleActiveLine will trigger before setActive(true) of CustomLink.tsx
Add a callback in CustomLink.tsx:
const CustomLink: React.FC<Props> = ({ onActive }) => {
useEffect(() => {
if (active) {
onActive();
}
}, [active]);
}
In Navbar.tsx:
const Layout: React.FC = ({ children }) => {
function handleOnActive() {
// do your query selector here
}
// add onActive={handleOnActive} to each CustomLink
return <CustomLink onActive={handleOnActive} />
}

Add Emoji from emoji picker to react slate

I use Two package
slate-react and emoji-mart
I want to when choose an Emoji , it puts on my editor.
import React from "react";
import { render } from "react-dom";
import { Editor } from "slate-react";
import { initialValue } from "./initialValue";
// Define our app...
class MyEditor extends React.Component {
// Set the initial value when the app is first constructed.
state = {
value: initialValue
};
// On change, update the app's React state with the new editor value.
onChange = ({ value }) => {
this.setState({ value });
};
onKeyDown = (event, change) => {
// This used to have stuff in it, but I moved it all to plugins.
};
clickMe=()=>{
this.setState({ value : this.state.value });
};
// Render the editor.
render() {
return (
<div>
<h1 onClick={this.clickMe}>Slate Editor Demo</h1>
<div style={{ border: "1px solid black", padding: "1em" }}>
<Editor
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
spellCheck={false}
/>
</div>
</div>
);
}
}
export default MyEditor;
import React,{useState} from 'react';
import 'emoji-mart/css/emoji-mart.css';
import { Picker } from 'emoji-mart';
function Emoji() {
const [emoji,setEmoji] = useState(null);
const addEmoji = (e) => {
setEmoji(e.native)
};
return <Picker onSelect={addEmoji} />
}
export default Emoji;
Try passing the editor ref to picker. Then in Emoji component in addEmoji method, try editorRef.current.InsertText(e.native). After hours of trying to solve this:
const YourTextEditor = props => {
const editor = createEditor();
const addEmoji = async emoji => {
await setTimeout(() => {
editor.focus();
}, 100);
editor.insertText(emoji.native);
};
return (
<>
<Editor
value={initialValue}
/>
<Emoji addEmoji={addEmoji} />
</>
);
};
const Emoji = props => {
return (<Picker onSelect={e => props.addEmoji(e)} />);
};

Resources