React router component calls function multiple times - javascript

I am trying to learn blockchain development, so I started to learn Solidity a few weeks ago and I had to create a front-end application for my contract I made with React, which I also don't know.
So, I have read documents and watched tutorials to make use of web3 libraries and some page transitions. Now I can navigate between pages in my application, but when I route to a specific page, my functions get called multiple times.
This is my index.js which gets loaded every time I run the application. I have set my routes like so.
index.js (I don't have app.js and use index.js like app.js)
import React from 'react';
import ReactDOM from 'react-dom';
import './style.css';
import Web3 from 'web3'
import { ConnectPage } from './ConnectPage';
import { MintPage } from './MintPage';
import { AllCryptonauts } from './AllCryptonauts';
import { MyCryptonauts } from './MyCryptonauts';
import { NotFound } from './NotFound';
import { BrowserRouter, Route, Switch } from 'react-router-dom'
function App() {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
window.ethereum.enable()
} else {
alert("Could not detect MetaMask. Please install MetaMask before proceeding!");
}
return (
<div>
<Switch>
<Route exact path="/" component={ConnectPage} />
<Route exact path="/MintPage" component={MintPage} />
<Route exact path="/MyCryptonauts" component={MyCryptonauts} />
<Route exact path="/AllCryptonauts" component={AllCryptonauts} />
<Route path="*" component={NotFound} />
</Switch>
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<BrowserRouter><App /></BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
I have made a connect button in my first page, which redirects me to the mint section.
ConnectPage.js
import React from 'react'
export const ConnectPage = (props) => {
async function Connect() {
const web3 = window.web3
const networkId = await web3.eth.net.getId()
if (networkId === 4) {
props.history.push("/MintPage")
} else {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x4' }],
});
props.history.push("/MintPage")
}
}
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}><img src="https://nftornek.000webhostapp.com/frontend/cnlogo.png"></img></div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: '5%'}}><button className="connectButton" onClick={Connect}>Enter the Universe</button></div>
</div>
)
}
This is where I make my mint transactions. I have put console.log("called checkChainID") to see how many times checkChainID function gets called. And it gets called 12 times every time page is loaded, and twice after I try to navigate to the same page.
As a beginner in all of these, I have gathered the information around I got from tutorials which are not much obviously, and tried to make a test application with routing(which I have made following by a tutorial too)
The routing worked as I exactly done everything in tutorial, and I wanted to keep going on this routing example for my app, but probably due to my lack of fundamental knowledge of React, I am doing something wrong inside the pages. I have been researching this issue for hours, but couldn't really understand what I can do to solve it.
I think it is because of useState because it renders the application every time when I call it, but I'm not sure if there is any way to prevent that from happening, or come up with a smarter way.
MintPage.js
import React, { useState } from 'react';
import Web3 from 'web3'
import Contract from "../src/build/Contract.json";
export const MintPage = (props) => {
const web3 = window.web3;
var [currentAccount, setCurrentAccount] = useState("0x0");
var [currentBalance, setCurrentBalance] = useState("0");
var [mintAmount, setMintAmount] = useState(1);
const [addAmount, setAddAmount] = useState(true);
const [subtractAmount, setSubtractAmount] = useState(false);
window.ethereum.on('chainChanged', (_chainId) => checkChainID());
window.ethereum.on('accountsChanged', (_accounts) => loadBlockchainData());
checkChainID();
async function checkChainID() {
const networkId = await web3.eth.net.getId();
if (networkId !== 4) {
props.history.push("/")
} else {
loadBlockchainData();
}
console.log("called checkChainID")
}
async function loadBlockchainData() {
window.web3 = new Web3(window.ethereum);
const accounts = await web3.eth.getAccounts();
setCurrentAccount(accounts[0]);
getBalance(accounts[0]);
}
async function getBalance(acc) {
const balance = await web3.eth.getBalance(acc);
var balanceEth = web3.utils.fromWei(balance, 'ether');
setCurrentBalance(parseFloat(balanceEth).toFixed(3) + " ETH");
const SmartContractObj = new web3.eth.Contract(Contract.abi, "0x187FF2d65dd7204f11ea0487F2EED36378946902");
}
function MintPage() {
props.history.push("/MintPage")
}
function MyCryptonauts() {
props.history.push("/MyCryptonauts")
}
function AllCryptonauts() {
props.history.push("/AllCryptonauts")
}
function Disconnect() {
props.history.push("/")
}
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}><img src="https://nftornek.000webhostapp.com/frontend/cnlogo.png" width='500' height='180'></img></div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<button className="regularButton divide" onClick={MintPage}>Mint</button>
<button className="regularButton divide" onClick={MyCryptonauts}>My Cryptonauts</button>
<button className="regularButton divide" onClick={AllCryptonauts}>All Cryptonauts</button>
<button className="regularButton divide" onClick={Disconnect}>Disconnect</button>
</div>
<div style={{ display: 'flex', justifyContent: 'center' }}><p className="accountText">Current Account: {currentAccount}</p></div>
<div style={{ marginTop: '25%' }}></div>
<div style={{ display: 'flex', justifyContent: 'center' }}><p className="accountText">Mint {mintAmount} Cryptonaut for XXX ETH</p></div>
<div style={{ display: 'flex', justifyContent: 'center' }}><button className="amountButton divide" disabled={subtractAmount ? 0 : 1} onClick={() => {
if (mintAmount <= 1) {
setMintAmount(1);
setSubtractAmount(false);
} else {
setMintAmount(mintAmount - 1);
} if (mintAmount === 2) {
setSubtractAmount(false);
} if (mintAmount >= 1) {
setAddAmount(true);
}
}}>-
</button>
<button className="mintButton divide" onClick={() => {
console.log(mintAmount);
}}>MINT
</button>
<button className="amountButton divide" disabled={addAmount ? 0 : 1} onClick={() => {
if (mintAmount >= 5) {
setMintAmount(5);
} else {
setMintAmount(mintAmount + 1);
}
if (mintAmount === 4) {
setAddAmount(false);
} if (mintAmount >= 1) {
setSubtractAmount(true);
}
}}>+
</button>
</div>
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '7px' }}><p className="accountText">Current Balance: {currentBalance}</p></div>
</div>
)
}

The problematic piece is these three lines:
window.ethereum.on('chainChanged', (_chainId) => checkChainID());
window.ethereum.on('accountsChanged', (_accounts) => loadBlockchainData());
checkChainID();
You usually don't want to call a function directly in the render cycle of a component, since components can re-render for lots of different reasons. Every time this component renders it's adding an event listener to a global ethereum object, so you'll get one additional listener per component render. Since this is a functional component, you should wrap those lines in an effect hook so you can control when they run.
useEffect(() => {
window.ethereum.on('chainChanged', (_chainId) => checkChainID());
window.ethereum.on('accountsChanged', (_accounts) => loadBlockchainData());
checkChainID();
return () => {
// remove the 'chainChanged' and 'accountsChanged' event listeners
}
}, [])
The empty array tells this effect to only run once, when the component first renders. If you need it to run when some state changes, or you need access to any state variables inside the functions run in the effect, you need to list those in the dependency array.
You'll also want to return a function from the hook that removes your listeners so they don't hang around and continue to multiply every time a user visits the page, leaves, and returns.
You can read more about useEffect here.

Related

How should I access this promise result (React, DataStore, AWS Amplify)

Following this tutorial https://www.youtube.com/watch?v=QrkkNte1onA&t=742s (2:36:48).
I am trying to access a promise that is found in useState value: dishes.
Here is my code on the third (and last useEffect) I query order DataDish (which contains the following info: quantity, orderID, orderDishDishId) with the condition that the orderID equals the order id of the order I'm returning. I set the dishes into a state with .then() but when accessing dishes the Dish data (name of the item, price, etc) is a promise. (See image below)
import {Card, Descriptions, Divider, List, Button} from 'antd';
import { useEffect, useState } from 'react';
import {useParams} from 'react-router-dom';
import {DataStore} from '#aws-amplify/datastore';
import {Order, OrderDish, User} from '../../models';
const DetailedOrder = () => {
const {id} = useParams();
const [order, setOrder] = useState(null);
const [customer, setCustomer] = useState(null);
const [dishes, setDishes] = useState([]);
useEffect(() => {
DataStore.query(Order, id).then(setOrder);
}, [id]);
useEffect(() => {
if(order?.userID) {
DataStore.query(User, order.userID).then(setCustomer);
}
}, [order?.userID]);
useEffect(() => {
if(!order?.id){
return;
}
DataStore.query(OrderDish, c => c.orderID.eq(order.id)).then(setDishes);
}, [order?.id])
console.log(dishes);
return (
<Card title={`Order #${id}` } style={{ margin: 20 }}>
<Descriptions bordered column={{ lg:1, md:1, sm:1 }}>
<Descriptions.Item label="Customer">{customer?.name}</Descriptions.Item>
<Descriptions.Item label="Customer Address">{customer?.address}</Descriptions.Item>
</Descriptions>
<Divider />
<List
dataSource={dishes}
renderItem={(dishItem) => (
<List.Item>
<div style={{fontWeight: 'bold'}}>{dishItem.Dish.name} x{dishItem.quantity}</div>
<div>${dishItem.Dish.price}</div>
</List.Item>
)}/>
<Divider />
<div style={styles.totalSumContainer}>
<h2 style={{fontWeight: '400'}}>Total:</h2>
<h2 style={styles.totalPrice}>${order?.total?.toFixed(2)}</h2>
</div>
<Divider />
<div style={styles.buttonsContainer}>
<Button block type='danger' size='large' style={styles.button}>
Decline Order
</Button>
<Button block type='primary' size='large' style={styles.button}>
Accept Order
</Button>
</div>
<Button block type='primary' size='large'>
Ready For Pickup
</Button>
</Card>
);
};
const styles = {
totalSumContainer: {
flexDirection: 'row',
display: 'flex',
},
totalPrice: {
marginLeft: 'auto',
fontWeight: 'bold',
},
buttonsContainer: {
display: 'flex',
paddingBottom: 30,
},
button: {
marginRight: 10,
marginLeft: 10,
color: 'white',
},
};
export default DetailedOrder;
The console.log(dishes) returns the folowing in the console:
I am trying to access the data found in Dish but its a promise and cant figure out how to bring it into state.
I've tried another .then() but I couldn't figure it out (just starting out with javascript). I've read up on async/await but I couldn't understand how to implement it.
Let me know if you might know but need more info. I'm happy to provide whatever is necessary. This is my first question on StackOverflow and it was much harder to formulate a 'good question' than I thought.
Vadim (The guy in the youtube video) does not encounter this problem (2:37:08). I suspect it is because of updates to amplify. But I'm not 100% sure.
Any info would be extremely appreciated.
Thanks for reading!
If you want to convert the api calls to async then you can try the following
useEffect(() => {
const fetchOrder = async () => {
const orderResponse = await DataStore.query(Order, id);
console.log('order', orderResponse);
setOrder(orderResponse)
// ideally fetch all data here to avoid unnecessary rerenders
// and check if the response user is different from previous use to prevent refetch of user
if(orderResponse?.userID) {
const customerResponse = await DataStore.query(User, orderResponse.userID);
console.log('order', customerResponse);
setCustomer(customerResponse)
}
if(!orderResponse?.id) {
const dishResponse = await DataStore.query(OrderDish, c => c.orderID.eq(orderResponse.id));
console.log('dish', dishResponse);
setDishes(dishResponse)
}
fetchOrder();
}, [id]);
I am hoping this will help you to debug to solve your issue and the write maintainable code.
Cheers

Creating an array in one function and then using it somewhere else

TL:DR, I am creating a randomly-ordered array in one React component, that I need to use in another component - but the second component keeps re-rendering and therefore re-shuffling my array - but I need its order to be fixed once it gets imported for the first time.
First things first - if I am doing this in a needlessly roundabout way, please do say so, I'm not set on this way.
I am making a flashcard program, and I want to give users the option to play games with random selections of their cards.
The way I am currently trying to do this, is that I have a functional component (because I need to do things like dispatch in it) which works as follows - I've added comments to explain what each bit does:
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getPosts } from "../../actions/posts";
export function WORDS() {
const dispatch = useDispatch();
const [localUser, setLocalUser] = useState();
//get all users
useEffect(() => {
dispatch(getPosts());
}, []);
const users = useSelector((state) => state.posts);
//set user to match the user in the local storage
const [user, setUser] = useState();
useEffect(() => {
setLocalUser(JSON.parse(localStorage.getItem("profile")));
}, [localStorage]);
useEffect(() => {
setUser(users.filter((user) => user.code == localUser?.code)[0]);
}, [users, localUser]);
//create an array of 8 random words from the users cards object
let RandomArray = []
if (user) {
for (let i = 0; RandomArray.length < 8; i++) {
let RanNum = Math.floor(Math.random() * user.cards.length);
!RandomArray.includes(user.cards[RanNum]) && RandomArray.push(user.cards[RanNum])
}
}
//create duplicates of each word and make an array of them all, once with the front of the card in first place and once with the back in first place
let shuffledWords = [];
RandomArray.map((word) => {
let newWord = { Q: word.front, A: word.back };
let newWord2 = { Q: word.back, A: word.front };
shuffledWords.push(newWord);
shuffledWords.push(newWord2);
});
//shuffle that array
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
shuffleArray(shuffledWords);
//return this array so I can call it in other functions
return { shuffledWords }
};
The game is then a kind of 'memory' game, where users try and match words with their translation. That's currently running like this, again with comments (please excuse the excessive inline styling - I will move that all to the stylesheet eventually but I've left it in here just in case any of that is what's causing the problems.):
import React, { useState } from "react";
import { WORDS as ImportedWords } from "./WORDS";
export const Memory = () => {
//import words from function
const ImportedArray = ImportedWords().shuffledWords
//create state variables for the first and second card a user pics
const [selectL1, setSelectL1] = useState("");
const [selectL2, setSelectL2] = useState("");
//create state variables for whether to display a card or not and whether or not it was correct
const [show, setShow] = useState([]);
const [correct, setCorrect] = useState([]);
//variable to make cards unclickable while they're spinning over
const [clickable, setClickable] = useState(true);
//if user has picked two cards, check if they match and either set them to correct or turn them back over
if (selectL1 && selectL2) {
clickable == true && setClickable(false);
let selectQ = ImportedArray.filter((word) => word.Q == selectL1)[0];
console.log("selectQ: ", selectQ);
selectQ && selectL2 == selectQ.A
? correct.push(selectL1, selectL2)
: console.log("Incorrect!");
setTimeout(function () {
setSelectL1("");
setSelectL2("");
setShow([]);
setClickable(true);
//correct.length == shuffledWords.length * 2 && alert("You win!");
}, 800);
}
//show the card a user's clicked
const selectCard = (wordId) => {
!selectL1 ? setSelectL1(wordId) : setSelectL2(wordId);
show.push(wordId);
};
return (
<div className="memory-game-wrapper">
<div
style={{ perspective: "2000px", pointerEvents: !clickable && "none" }}
>
{/* filter through the imported array and display them */}
{ImportedArray.map((word) => {
return (
<div
className={
show.includes(word.Q) || correct.includes(word.Q)
? "card flip"
: "card"
}
id={word.Q}
style={{
borderRadius: "5px",
display: "inline-block",
width: "100px",
height: "180px",
backgroundColor: "rgb(185, 204, 218)",
margin: "5px",
}}
onClick={() =>
!correct.includes(word.Q) &&
!show.includes(word.Q) &&
selectCard(word.Q)
}
>
<div
className="back-face"
style={{
position: "absolute",
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<center>
<span style={{ userSelect: "none" }}></span>
</center>
</div>
<div
className="front-face"
style={{
position: "absolute",
height: "100%",
display: "flex",
width: "100%",
borderRadius: "5px",
border: "5px solid rgb(185, 204, 218)",
boxSizing: "border-box",
backgroundColor: correct.includes(word.Q)
? "white"
: "rgb(185, 204, 218)",
justifyContent: "center",
alignItems: "center",
}}
>
<h3 style={{ userSelect: "none" }}>{word.Q}</h3>
</div>
</div>
);
})}
</div>
</div>
);
};
I suspected that what was happening was that the whole array is being re-rendered any time a user clicks on any of the cards, which means the order gets shuffled again - and makes the game unplayable, so I whacked in a console.log(ImportedArray[0]) to check and yes, that is definitely happening. But I can't work out how to stop it?
Any ideas??
If the Memory component is not conditionally mounted/unmounted in the parent, like {condition && <Memory}, you can use the useMemo hook to memoize the imported words at the first render.
const ImportedArray = useMemo(() => ImportedWords().shuffledWords, []);
Anyway the WORDS component is a candidate to be a custom hook where you can encapsulate the words logic. it should be named useWords

Importing and displaying multiple images from GraphQL using gatsby-plugin-image

Hi I'm trying to add an instagram feed to my website but I can not find a way to make it work. I haven't really used these tools before so I don't really understand it, but made it to point where I think I have the feed in GraphQL. Now the problem is I don't know how to display the images. Can anyone help?
Here is some code:
import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import { useStaticQuery, graphql } from "gatsby"
const CapabilityList = () => {
const data = useStaticQuery(graphql`
query InstagramQuery {
allInstagramContent {
edges {
node {
localImage {
childImageSharp {
fixed(width: 200, height: 200) {
...GatsbyImageSharpFixed
}
}
}
}
}
}
}
`)
let arrayOfInstaImages = getImage(data, 'allInstagramContent.edges')
return (
<div style={{ maxWidth: `900px`, marginBottom: `1.45rem`, display: 'flex' }}>
{arrayOfInstaImages.map((item, i) => {
return (
<div key={i} style={{ width: "200px", height: "200px" }}>
<GatsbyImage image={item.node.localImage.childImageSharp.fixed} />
</div>)
})}
</div>
)
}
export default CapabilityList;
This doesn't work and displays error: Property 'map' does not exist on type 'IGatsbyImageData'.
So I think I need some other way to display a few images from the array.
Thanks a lot for every help!
getImage is a helper function (you don't really need it) that returns an image object given a path. Not an array:
Safely get a gatsbyImageData object. It accepts several different
sorts of objects, and is null-safe, returning undefined if the object
passed, or any intermediate children are undefined.
Simply do:
return (
<div style={{ maxWidth: `900px`, marginBottom: `1.45rem`, display: 'flex' }}>
{data.allInstagramContent.edges.node.map((item, i) => {
return (
<div key={i} style={{ width: "200px", height: "200px" }}>
<Img image={item.node.localImage.childImageSharp.fixed} />
</div>)
})}
</div>
)
Note: Img for gatsby-image
Your array of iterable data is the node so you should look and loop there (you can console.log it to help you understand the data structure).
The main problem in your code, besides the wrong loop, is that you are using a GraphQL query structure for Gatsby Image (from version 2, Img from gatsby-image) while you are using GatsbyImage component (from v3 onwards, GatsbyImage from gatsby-image-plugin). You can check for the migration details at: https://www.gatsbyjs.com/docs/reference/release-notes/image-migration-guide/
If you want to use a GatsbyImage, your code should look like:
import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import { useStaticQuery, graphql } from "gatsby"
const CapabilityList = () => {
const data = useStaticQuery(graphql`
query InstagramQuery {
allInstagramContent {
edges {
node {
localImage {
gatsbyImageData(
width: 200
placeholder: BLURRED
formats: [AUTO, WEBP, AVIF]
)
}
}
}
}
}
}
`)
return (
<div style={{ maxWidth: `900px`, marginBottom: `1.45rem`, display: 'flex' }}>
{data.allInstagramContent.edges.node.map((item, i) => {
return (
<div key={i} style={{ width: "200px", height: "200px" }}>
<GatsbyImage image={item.node.localImage.childImageSharp.gatsbyImageData} />
</div>)
})}
</div>
)
}
export default CapabilityList;
Note: the use of Img or GatsbyImage will strictly rely on your installed packages and your gatsby-config.js structure. Check it out because you are mixing concepts.
Both components are similar but they accept different image props. While GatsbyImage accepts an image prop containing gatsbyImageData, Img accepts a fluid/fixed prop of childImageSharp (fluid or fixed) data, so the query will be slightly different depending on the component used so as it should be the configuration.
Of course, this is an approximation. Your GraphQL structure may be slightly different depending on your data structure. Check the query at the GraphiQL playground (localhost:8000/___graphql)

React Native - how to re render a component every x milliseconds? "this.setState is not a function?"

Alright, Im trying to update the properties and/or text of a component every x milliseconds. I dove into https://www.npmjs.com/package/react-interval-renderer and the likes, however I got errors pertaining to s.
Im now looking at simply react native (IOS). how to update value date (millisecond) however am having trouble formatting this to my file.
I have:
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
});
this.state ={
color: "#fff",
colorAnimTime: 36000,
time: 0
}
setInterval(() => {
this.setState({
time: new Date().getMilliseconds()
});
}, 1000);
//------------------------------------------------------------------->
if (!fontsLoaded) {
return <AppLoading />;
} else {
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
textWrapper: {
height: hp('70%'), // 70% of height device screen
width: wp('80%'), // 80% of width device screen
justifyContent: 'center',
alignItems: 'center',
},
myText: {
fontSize: hp('5.5%'), // End result looks like the provided UI mockup
fontFamily: 'SequelSans-BlackDisp'
}
});
return (
<AnimatedBackgroundColorView
color='#00acdd'
delay={0.5}
duration={36000}
initialColor={this.state.color}
style={styles.container}>
<View style={styles.textWrapper}>
<Text style={styles.myText}>COLOR</Text>
</View>
</AnimatedBackgroundColorView>
);
}
The referenced answer uses componentDidMount() { to enclose the setInterval, however I get syntax errors if I put that where I currently have the setInterval
Now Im getting
this.setState is not a function
And have tried binding the setInterval to the right this but I believe Im not understanding how to set up my file.
After 1000 ms I want to change the 'color' of my <AnimatedBackgroundColorView> and just reload the component. (so it animates again - https://www.npmjs.com/package/react-native-animated-background-color-view )
How can I do this?
Your component is written as a functional component, not a class. To create a stateful functional component, you need to use the hook setState. You're getting this error as there's no object property setState on the component or this. You'll also want to use the useEffect hook to set your interval.
https://reactjs.org/docs/hooks-reference.html
import React, { useState } from 'react';
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
});
const color = "#fff";
const colorAnimTime = 36000;
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date().getMilliseconds));
}, 1000);
return () => clearInterval(interval);
}, []);
//------------------------------------------------------------------->

How can I include my existing table into my export function?

I am relatively new to React-JS and was wondering how I could pass my variables to my export function. I am using the jsPDF library.
At the time the Summary page is showing up, every thing is already in the database.
The Summary page creates in every round an IdeaTable component, writes it into an array and renders it bit by bit if the users click on the Next button (showNextTable()).
This component can use a JoinCode & playerID to assemble the table that was initiated by this player.
import React, { Component } from "react";
import { connect } from "react-redux";
import { Box, Button } from "grommet";
import IdeaTable from "../playerView/subPages/ideaComponents/IdeaTable";
import QuestionBox from "./QuestionBox";
import { FormUpload } from 'grommet-icons';
import jsPDF from 'jspdf';
export class Summary extends Component {
state = {
shownTable: 0
};
showSummary = () => {};
showNextTable = () => {
const { players } = this.props;
const { shownTable } = this.state;
this.setState({
shownTable: (shownTable + 1) % players.length
});
};
exportPDF = () => {
var doc = new jsPDF('p', 'pt');
doc.text(20,20, " Test string ");
doc.setFont('courier');
doc.setFontType('bold');
doc.save("generated.pdf");
};
render() {
const { topic, players } = this.props;
const { shownTable } = this.state;
const tables = [];
for (let i = 0; i < players.length; i++) {
const player = players[i];
const table = (
<Box pad={{ vertical: "large", horizontal: "medium" }}>
<IdeaTable authorID={player.id} />
</Box>
);
tables.push(table);
}
return (
<Box
style={{ wordWrap: "break-word" }}
direction="column"
gap="medium"
pad="small"
overflow={{ horizontal: "auto" }}
>
<QuestionBox question={topic} />
{tables[shownTable]}
<Button
primary
hoverIndicator="true"
style={{ width: "100%" }}
onClick={this.showNextTable}
label="Next"
/>
< br />
<Button
icon={ <FormUpload color="white"/> }
primary={true}
hoverIndicator="true"
style={{
width: "30%",
background: "red",
alignSelf: "center"
}}
onClick={this.exportPDF}
label="Export PDF"
/>
</Box>
);
}
}
const mapStateToProps = state => ({
topic: state.topicReducer.topic,
players: state.topicReducer.players
});
const mapDispatchToProps = null;
export default connect(mapStateToProps, mapDispatchToProps)(Summary);
So basically how could I include the IdeaTable to work with my pdf export?
If you want to use html module of jsPDF you'll need a reference to generated DOM node.
See Refs and the DOM on how to get those.
Alternatively, if you want to construct PDF yourself, you would use data (e.g. from state or props), not the component references.
Related side note:
On each render of the parent component you are creating new instances for all possible IdeaTable in a for loop, and all are the same, and most not used. Idiomatically, this would be better:
state = {
shownPlayer: 0
};
Instead of {tables[shownTable]} you would have:
<Box pad={{ vertical: "large", horizontal: "medium" }}>
<IdeaTable authorID={shownPlayer} ref={ideaTableRef}/>
</Box>
And you get rid of the for loop.
This way, in case you use html dom, you only have one reference to DOM to store.
In case you decide to use data to generate pdf on your own, you just use this.props.players[this.state.shownPlayer]
In case you want to generate pdf for all IdeaTables, even the ones not shown, than you can't use API that needs DOM. You can still use your players props to generate your own PDF, or you can consider something like React-Pdf

Categories