I have been developing a project for three months. I need to call a method from the child component (class-based component) in the parent component (functional component). I used ref for this but it didn't work.
Here is the parent component:
import React, { useState, useEffect, useRef, createRef } from "react";
import CityPicker from "../../components/public/cityPicker";
import Chip from "../../components/forms/chip";
import Sidebar from "./sidebar";
import { Router } from "../../routes";
import NextRouter, { withRouter } from "next/router";
const Search = props => {
const [filterItem, setFilterItem] = useState();
const deleteFilterItem = createRef();
const deleteChipHandler = event => {
filterItem
? deleteFilterItem.current.onDeleteSearchableFilterItem(
event,
"stateOrProvince",
"selectedStateOrProvince"
)
: "";
};
return (
<>
<div className={filterItem ? "filters-display" : ""}>
{filterItem
? filterItem.map((item, index) => {
return (
<Chip
id={item.id}
key={index + "selected city"}
label={item.value}
onDelete={e => deleteChipHandler(e)}
/>
);
})
: ""}
</div>
<Sidebar ref={deleteFilterItem} />
</>
);
};
export default withRouter(Search);
The onDeleteSearchableFilterItem method belong to the child component.
Write the function like this in your child component. data is the data you get from your state of that component or a state from reducer if you are using redux(like this.props.data):
const onDelete = () => {
this.props.onDelete(data);
}
then call it in the parent:
import { Router } from "../../routes";
import NextRouter, { withRouter } from "next/router";
const Search = props => {
const [filterItem, setFilterItem] = useState();
const deleteFilterItem = createRef();
const deleteChipHandler = (event, data) => {
filterItem
? deleteFilterItem.current.onDeleteSearchableFilterItem(
event,
data
)
: null;
};
return (
<>
<div className={filterItem ? "filters-display" : ""}>
{filterItem
? filterItem.map((item, index) => {
return (
<Chip
id={item.id}
key={index + "selected city"}
label={item.value}
onDelete={deleteChipHandler}
/>
);
})
: ""}
</div>
<Sidebar ref={deleteFilterItem} />
</>
);
};
export default withRouter(Search);
Related
My context is as follows:
import React, {createContext, useEffect, useState} from "react";
export const CartContext = createContext();
const CartContextProvider = (props) => {
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const updateCart = (productId, op) => {
let updatedCart = [...cart];
if (updatedCart.find(item => item.id === productId)) {
let objIndex = updatedCart.findIndex((item => item.id === productId));
if (op === '-' && updatedCart[objIndex].qty > 1) {
updatedCart[objIndex].qty -= 1;
} else if (op === '+') {
updatedCart[objIndex].qty += 1;
}
} else {
updatedCart.push({id: productId, qty: 1})
}
setCart(updatedCart);
}
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{cart, updateCart, removeItem}}>
{props.children}
</CartContext.Provider>
)
};
export default CartContextProvider;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";
function App() {
return (
<div>
<CartContextProvider>
<Router>
<NavigationBar/>
<ShoppingCart/>
<Routes>
<Route exact path="/" element={<Homepage/>}/>
<Route path="/a-propos" element={<AboutUsPage/>} />
<Route path="/contact" element={<ContactPage/>}/>
<Route path="/recherche" element={<SearchPage/>}/>
</Routes>
</Router>
</CartContextProvider>
</div>
);
}
export default App;
In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.
Here's the ShoppingCartQuantity component:
import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";
import styles from './ShoppingCartQuantity.module.css'
const ShoppingCartQuantity = ({productId}) => {
const {cart, updateCart} = useContext(CartContext);
let qty = 0;
if (cart.find((item => item.id === productId))) {
let objIndex = cart.findIndex((item => item.id === productId));
qty = cart[objIndex].qty;
}
return (
<div>
<span>
<span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
<span className={styles.qty}>{qty}</span>
<span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.
So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?
UPDATE
As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!
Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:
import React, { useState, useEffect, useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from 'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"
function Homepage() {
const [categories, setCategories] = useState([]);
const [subCats, setSubCats] = useState([]);
const [loader, setLoader] = useState(false);
const ResponsiveGridLayout = WidthProvider(Responsive);
const scrollRef = useRef();
const productModal = withReactContent(Swal);
const opts = {
// height: '390',
// width: '640',
playerVars: {
autoplay: 1,
}
};
useEffect(() => {
CategoryService.get().then((response) => {
setCategories(response);
});
}, []);
function showSubCatsHandler(catId) {
setLoader(true);
setSubCats([]);
SubCategoryService.get(catId).then((response) => {
setSubCats(response.data);
setLoader(false);
scrollRef.current.scrollIntoView({ behavior: "smooth" });
});
}
function showProductPopupHandler(productId) {
ProductService.get(productId).then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
<h3 className={modalStyles.header}>{product.AMP_Title}</h3>
<h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
<img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
{
product.descriptions.map((desc, _) => (
<div key={desc.AMPD_GUID}>
{
desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
}
{
desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
}
<p className={modalStyles.description}>{desc.AMPD_Description}</p>
</div>
))
}
<br/>
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity productId={product.AMP_GUID} />
</div>
<br/>
{
product.AMP_VideoId !== '' &&
<YouTube
videoId={product.AMP_VideoId}
opts={opts}
/>
}
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (
<div>
<div className="categories-container">
<ResponsiveGridLayout
className="layout"
layouts={ CategoriesLayout }
breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
isDraggable={ false }
>
{
categories.map((cat, index) => (
<div key={index}>
<CategoryCard
category_id = {cat.AMC_GUID}
image = {cat.AMC_Image}
showSubCatsHandler = {showSubCatsHandler}
/>
</div>
))
}
</ResponsiveGridLayout>
{
loader &&
<Triangle
height="100"
width="100"
color='#bcad70'
ariaLabel='loading'
wrapperClass="loader"
/>
}
<div ref={scrollRef}>
{
Object.keys(subCats).map((keyName, _) => (
<Subcat
key={subCats[keyName].AMSC_GUID}
title={ subCats[keyName].AMSC_Title }
products={ subCats[keyName].products }
showProductPopupHandler = {showProductPopupHandler}
/>
))
}
</div>
</div>
</div>
);
}
export default Homepage;
I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?
UPDATE 2
The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.
I updated the component by using the qty as a state.
const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
const [quantity, setQuantity] = useState(qty);
const updateCartHandler = (productId, amount) => {
updateCart(productId, amount);
setQuantity(Math.max(quantity + amount, 1));
}
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCartHandler(productId, -1)}
>
-
</span>
<span className={styles.qty}>{quantity}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCartHandler(productId, 1)}
>
+
</span>
</span>
</div>
)
}
Issue
It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.
See this other similar issue regarding accessing a Redux context in the alert.
Solution
It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.
I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.
Example:
export const withShoppingCartContext = Component => props => {
const { cart, removeItem, updateCart } = useContext(CartContext);
return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}
const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
const qty = cart.find(item => item.id === productId)?.qty ?? 0;
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCart(productId, -1)}
>
-
</span>
<span className={styles.qty}>{qty}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCart(productId, 1)}
>
+
</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.
ShoppingCart
import ShoppingCartQuantityBase, {
withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";
const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);
const ShoppingCart = (props) => {
...
return (
...
<ShoppingCartQuantity productId={....} />
...
);
};
In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.
...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...
function Homepage() {
...
const { cart, updateCart } = useContext(CartContext);
const productModal = withReactContent(Swal);
...
function showProductPopupHandler(productId) {
ProductService.get(productId)
.then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
...
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity
productId={product.AMP_GUID}
{...{ cart, updateCart }}
/>
</div>
...
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (...);
}
export default Homepage;
Additional Issues
Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.
Example:
const CartContextProvider = (props) => {
...
const updateCart = (productId, amount) => {
// only update if item in cart
if (cart.some(item => item.id === productId)) {
// use functional state update to update from previous state
// cart.map creates shallow copy of previous state
setCart(cart => cart.map(item => item.id === productId
? {
...item, // copy item being updated into new object reference
qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
}
: item
));
}
}
const removeItem = (id) => {
setCart(cart => cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cart, updateCart, removeItem }}>
{props.children}
</CartContext.Provider>
);
};
You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component.
Anyway, when you declare a route, you must pass the component, not the root element. So, this line:
<Route exact path="/" element={<Homepage/>}/>
must be
<Route exact path="/" component={Homepage}/>
I am generating my state in the parent component. latestFeed generates a series of posts from my backend:
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Childfrom "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
then latestFeed gets generated into a series of components that all need to hold their own state.
import React, { useState } from "react";
const RenderText = (post, showComment, handleComment) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child= ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
return RenderText(post, showComment, handleComment);
})}
</div>
);
};
export default Child;
In its current form, the state of RenderText's is all set at the same time. I need each child of Child to hold its own state.
Thank you!
Instead of using RenderText as a function, call it as a component:
{posts.map((post) => (
<RenderText key={post.id} post={post} showComment={showComment} />
))}
This is because when used as a component, it will have it's own lifecycle and state. If used as a function call, React does not instantiate it the same way - no lifecycle, no state, no hooks, etc.
I am trying to display the SingleLineText component below that has one input field when a click occurs in the parent component called TextInput. However, after a click, the state is not changed by useState and as a result the child component named SingleLineText is not displayed.
TextInput component is pasted below right after SingleLineText component.
SingleLineText component:
import React from "react";
const SingleLineText = () => {
return(
<form>
<input />
</form>
)
}
export default SingleLineText;
TextInput the Parent component for SingleLineText component:
import React, {useState, useEffect} from "react";
import SingleLineText from "./SingleLineText"
const TextInput = (props) => {
const [showSingleText, setShowSingleText] = useState(false);
useEffect( () => {
}, [showSingleText])
const handleFieldDisplay = (event) => {
if (event.target.value == "Single line text") {
cancel();
setShowSingleText(showSingleText => !showSingleText);
//setShowSingleText(showSingleText => true);
}
}
const cancel = () => {
props.setShowInput(!props.showInput);
}
return (
<>
<div className="dropdown">
<ul key={props.parentIndex}>
{
props.fieldType.map( (val, idx) => {
return(
<option key={idx} value={val} className="dropdown-item" onClick={handleFieldDisplay}> {val} </option>
)
})
}
</ul>
</div>
{showSingleText && <SingleLineText />}
</>
)
}
export default TextInput;
Topmost or grand parent component:
import React, { useState, useEffect, useRef } from "react"
import PropTypes from "prop-types"
import TextInput from "components/pod_table/fields/inputs/TextInput";
const DynamicFields = (props) => {
const fieldType = ['Checkbox', 'Dropdown', 'boolean', 'Single line text'];
const [showDynamicField, setShowDynamicField ] = useState(false);
const[showInput, setShowInput] = useState(false);
useEffect(() => {}, [showDynamicField, showInput]);
const handleShowDynamicField = (event) => {
setShowDynamicField(!showDynamicField);
}
const handleSubDisplay = (event) => {
if (event.target.value == "customise field type") {
setShowDynamicField(!showDynamicField);
setShowInput(!showInput);
}
}
return (
<React.Fragment>
<div>
<i className="bi bi-chevron-compact-down" onClick={handleShowDynamicField}></i>
{ showDynamicField &&
(<div className="dropdown">
<ul key={props.parentIndex}>
{
optionsHash.map( (val, idx) => {
return(
<option key={idx} value={val} className="dropdown-item" onClick={handleSubDisplay}> {val} </option>
)
})
}
</ul>
</div>) }
{showInput && <TextInput fieldType={fieldType} setShowInput={setShowInput} showInput={showInput} /> }
</div>
</React.Fragment>
)
}
The problem is that in the handleFieldDisplayFunction you are calling the cancel function which changes the parent state showInput and hence unmounts the TextInput component itself, so the showSingleText state changes inside TextInput component doesn't have any effect.
Design your code in a way that you are not required to unmount TextInput when you click on an option within it
const handleFieldDisplay = (event) => {
if (event.target.value == "Single line text") {
setShowSingleText(showSingleText => !showSingleText);
}
}
I want to pass the data which is anything like, array, strings, numbers into the App(Parent) Component) from the Child1(Child) components.
There's a parent who is class-based and the child is functional based.
Parent:
import React, { Component } from "react";
import "./App.css";
import Child1 from "./Child1/Child1";
class App extends Component {
state = { message: "" };
callbackFunction = (event) => {
this.setState({
message: event.target.value
});
}
render() {
return (
<div className="App">
<Child1 />
</div>
);
}
}
export default App;
Child
import React from 'react'
const Child1 = (props) => {
return (
<div>
</div>
);
}
export default Child1;
If you want to send some data to the parent component upon clicking a button that's in the child component, you can do it this way:
import React from 'react'
const Child1 = React.memo((props) => {
return (
<div>
<button onClick={() => {props.clicked('some data')}} type="button">Click me to send data<button>
</div>
);
})
export default Child1;
now you can assign a function to the clicked prop in your parent component and that gets called when the button in the child component is clicked:
class App extends Component {
state = { message: "" };
callbackFunction = (event) => {
this.setState({
message: event.target.value
});
}
clicked = (data) => { // this func. will be executed
console.log(data);
}
render() {
return (
<div className="App">
<Child1 clicked={this.clicked}/>
</div>
);
}
}
I have founded another solution for you using combine Functional-Class-functional component pass data. here the live code for https://codesandbox.io Click and check that.
Also same code added bellow here:
app component
import React, { useState } from "react";
import "./styles.css";
import Child from "./child";
export default function App() {
const [name, setName] = useState("Orignal Button");
const [count, setCount] = useState(0);
const handleClick = _name => {
setName(_name);
setCount(count + 1);
};
const resetClick = e => {
setName("Orignal Button");
setCount(0);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Child
name={name + " - " + count}
handleClick={handleClick}
resetClick={resetClick}
/>
</div>
);
}
child1 component
import React from "react";
import Child2 from "./child2";
class Child extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<>
<h2>This is child</h2>
<button onClick={e => this.props.handleClick("New Name changed")}>
{this.props.name}
</button>
<Child2 handleChilc2Click={this.props.resetClick} />
</>
);
}
}
export default Child;
Another component Child2
import React from "react";
const Child2 = props => {
return (
<>
<h2>Child 2</h2>
<button onClick={e => props.handleChilc2Click(e)}>
Click me for reset Parent default
</button>
</>
);
};
export default Child2;
You can some help from it.
Thanks
You can pass a function to Child (called Counter in the example) that will change Parent state. Since you pass the same reference for this function every time (this.increment) the only Children that will render are those that changed.
const Counter = React.memo(
//use React.memo to create pure component
function Counter({ counter, increment }) {
console.log('rendering:', counter.id)
// can you gues why prop={new reference} is not a problem here
// this won't re render if props didn't
// change because it's a pure component
return (
<div>
<div>counter is {counter.count}</div>
<button onClick={() => increment(counter.id)}>
+
</button>
</div>
)
}
)
class Parent extends React.PureComponent {
state = {
counters: [
{ id: 1, count: 0 },
{ id: 2, count: 0 }
]
}
increment = _id =>
this.setState({
counters: this.state.counters.map(val =>
_id === val.id
? { ...val, count: val.count + 1 }
: val
)
})
render() {
return (
<div>
{this.state.counters.map(counter => (
<Counter
counter={counter}
increment={this.increment}
key={counter.id}
/>
))}
</div>
)
}
}
//render app
ReactDOM.render(<Parent />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm changing my class component to a function component and stuck on the render PDF. It was working fine in the class component. The Kendo docs only have a class component usage guide.
This component simply renders HTML and gives the user the option to download a PDF.
import React, { Fragment } from 'react';
import { firestoreConnect } from 'react-redux-firebase';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { PDFExport } from '#progress/kendo-react-pdf';
import { PdfButton } from 'components/StyledButtons';
import {
Container,
CircularProgress
} from '#material-ui/core';
function ViewProject({ _project, props }) {
const exportBriefing = id => e => {
this[id].save();
};
return (
<Fragment>
{_project && (
<Container maxWidth="md">
<PdfButton onClick={exportBriefing(_project.id)} />
<PDFExport
pageTemplate={DocumentTemplate}
paperSize={'A4'}
margin="1.5cm"
scale={0.7}
fileName={'Project_Brief'}
ref={r => (this[_project.id] = r)}
>
<---HTML content here--->
</PDFExport>
</Container>
)}
{!_project && (
<CircularProgress />
)}
</Fragment>
);
}
const mapStateToProps = ({ firestore: { ordered } }) => ({
_project: ordered.project && ordered.project[0],
});
export default compose(
connect(mapStateToProps),
firestoreConnect(props => [
{ collection: 'projects', storeAs: 'project', doc: props.match.params.id },
])
)(ViewProject);
This is my error (where hY2MWyk4piTBjhpJDFIc is the 'id' of the doc):
Thanks in advance for any suggestions.
You have to use the useRefhook, to declare your ref as null and then get the value :
const myRef = useRef(null);
Then attach to the Component that you want to ref:
<Fragment>
{_project && (
<Container maxWidth="md">
<PdfButton onClick={exportBriefing(_project.id)} />
<PDFExport
pageTemplate={DocumentTemplate}
paperSize={'A4'}
margin="1.5cm"
scale={0.7}
fileName={ 'Project_Brief'}
ref={ myRef}
>
Check this docs to get more info related to useRef:
The this context of a functional component does not persist across renders.
You'll want to use the useState hook instead of this.
Thanks to Ricardo Gonzalez for pointing me in the right direction. I was overthinking this and using useRef straight from the docs fixed the problem.
function ViewProject({ _project }) {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.save();
};
return (
<Fragment>
{_project && (
<Container maxWidth="md">
<StyledPaper>
<PdfButton onClick={onButtonClick} />
<PDFExport
pageTemplate={DocumentTemplate}
paperSize={'A4'}
margin="1.5cm"
scale={0.7}
fileName={`Project_Brief: ${
_project.project_number
}`}
ref={inputEl}
>