I am building an Electron App that allows user to auto populate a PDF based on fetched data from an internal server. I'm trying to use #react-pdf/renderer to display and create the PDF.
I've been using these two reference pages, official documentation, regarding this topic:
On the fly rendering
Dynamic content
Here's my code:
Home.tsx (abridged)
import PDFDoc from "/components/PDFDoc";
export default function Home(){
// I'm not sure how to use this as the official docs are limited.
const [pdfInstance, setPdfInstance] = ReactPDF.usePDF({ document: PDFDoc });
// I'd display code, here, to display data but it isn't relevant to this case.
return(
<PDFViewer
width="100%"
height="100%"
children={PDFDoc}
/>
)
}
PDFDoc.tsx
const PDFDoc = () => (
<Document>
<Page>
<Text>Test text</Text>
<Text
render={({pageNumber, totalPages}) => `${pageNumber} / ${totalPages}`}
fixed
/>
</Page>
</Document>
)
export default PDFDoc;
If I create the PDF in a normal component style similar to the home page, it displays properly within the PDFViewer.
The issue:
Without passing props to the component, how can I send data to the document so as to display dynamic data fetched via REST API.
I'm hoping that someone more experienced with the library can assist me with understanding how to dynamically display data within the PDF.
Yes, the react-pdf documentation about dynamic content.
So having faced the same challenge, here's what I discovered. It should help you ...
If you look at the codebase, under #react-pdf/renderer/index.d.ts (v2.1.1), you'll find two major optional props - render and children - that <Text/> can accept (at least it vital for you to understand them for this use case of passing dynamic content/data)
declare namespace ReactPDF {
// ...
interface TextProps extends NodeProps {
render?: (props: {
pageNumber: number;
totalPages: number;
subPageNumber: number;
subPageTotalPages: number;
}) => React.ReactNode;
children?: React.ReactNode;
// ...
}
/**
* A React component for displaying text...
*/
class Text extends React.Component<TextProps | SVGTextProps> {}
// ...
}
declare const Text: typeof ReactPDF.Text;
// ...
export { Text, // ... }
Note this:
render prop expects specific props to be passed through it.
So it cannot accept any kind of data fetched via the API calls unless you're obviously passing it stuff related to no. of pages and total pages etc.
On the hand, the children prop is more open versatile in this case.
The type of "children" expected are based off the React.ReactNode.
type React.ReactNode = boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined
So your answer lies in utilizing the children prop. How?
Here's a step-by-step process:
In the Parent component, do this...
const Parent = props => {
React.useEffect(() => {
// make API call
apiCall(...).then(res => {
if (res) {
// store your data in a session or local storage
// Why don't we use "state" and pass it down as props?
sessionStorage.setItem('data', JSON.stringify(res));
};
}).catch(error => ... )
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (...);
};
If you try to pass your fetched data (after the API call) by updating the Parent component state with an aim to pass it to the Child component as props, it does not work!
Why? The behavior is that the passed props are often undefined (I'm yet to understand why!).
Therefore storing the fetched data using those storage methods helps (or if you're using redux you might want to keep it in the store)
In the Child component, do this...
import { Page, Text, View, Document, StyleSheet } from '#react-pdf/renderer';
// Create styles
const styles = StyleSheet.create({
page: { ... },
section: { ... }
});
// Create Document Component
export const Child = () => {
const fetchedData = JSON.parse(sessionStorage.getItem("data"));
// let's destructure our data
const { email, gender, username, lastname, firstname, ... } = fetchedData;
// we define block-scope React Components to be passed into <Text>
// for example ...
const LastName = () => `${lastname}`;
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.section}>
<Text>Last name:</Text>
<Text children={<LastName/>} />
</View>
<View style={styles.section}>
<Text>Section #2</Text>
</View>
</Page>
</Document>
);
};
If you change this line:
<Text children={<LastName/>} />
and replace it with this:
<Text>{lastname}</Text>
It will not work (to your dismay) ... see the "Why?" under Parent component.
Otherwise, here's a screenshot where this approach worked - look towards the bottom-left to see what the Child component renders (that's dynamic).
Related
I have a NavBar component that has a list of dynamically generated links (these links are generated after querying my backend for some categories). These links are stored inside a child component of the NavBar, called DrawerMenu.
The NavBar is a child of the main App.js component.
In my Category component, I have a "delete" function that deletes a category. Once I delete a category I want to remove the link to it in the NavBar. How would I go about doing this?
For further context, my components are given below:
DrawerMenu component
class DrawerMenu extends Component {
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
getData = (query) => {
// Query backend for category data and set it to this.state.menuItems
}
componentDidMount() {
this.getData(menuItemsQuery)
}
render() {
const { classes, handleDrawerClose, open } = this.props
const { menuItems } = this.state
const drawer = (classes, handleDrawerClose) => (
<div>
...
{
menuItems.map((menuItem, index) => (
<Link color="inherit" key={index} to={menuItem.link} className={classes.drawerLink} component={RouterLink}>
<ListItem button className={classes.drawerListItem} onClick={handleDrawerClose}>
<ListItemText primary={menuItem.name} />
</ListItem>
</Link>
))
}
...
</div>
)
...
return (
<div>
<Drawer
variant="temporary"
anchor='left'
open={open}
onClose={handleDrawerClose}
classes={{
paper: `${open ? classes.drawerOpen : null} ${!open ? classes.drawerClose : null}`,
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
{drawer(classes, handleDrawerClose)}
</Drawer>
</div>
)
}
}
NavBar component
function PrimarySearchAppBar(props) {
return (
<div className={classes.grow}>
...
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
/>
...
</div>
)
}
Category component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.history.push(`/`)
}
...
}
There are two common ways of doing this: You can either use a state management tool, like Redux or pass your state down the component tree as props.
Redux is often used when several components depend on the same state or when the component that depends on a state is several layers deep, so it would get cumbersome to pass it down as props.
I'll assume your component tree is not very large, so I will create a simple example passing props down the tree.
class DrawerMenu extends Component {
// We're gonna manage the state here, so the deletion
// will actually be handled by this component
state = {
menuItems: [] // Takes a series of objects of the shape { name: "", link: "" }
}
handleDelete = (id) => {
let updatedMenuItem = [...this.state.menuItems]; //Create a copy
updatedMenuItem = updatedMenuItem(item => item.id !== id) // Remove the
deleted item
this.setState({
menuItems: updatedMenuItem
})
}
...
// Then wherever you render the category component
<Category handleDelete = {handleDelete}/> //Pass a reference to the delete method
}
Category Component
class Category extends Component {
...
deleteCategory = async () => {
// Code to request backend to delete category
this.props.handleDelete(categoryId) //Pass the id of the category
this.props.history.push(`/`)
}
...
}
I would suggest reading about state management, it is a core concept in React and you will use it everywhere. Redux and Context API for example.
Not sure why Dennis Vash deleted their answer, they are correct, but perhaps not descriptive enough in the solution.
The way you delete the category is not to call the backend itself from inside the category component, because then the navbar doesn't know that you made a call, but to call a callback that is in an ancestor shared by both the category component and the navbar to delete a category, and then rerequest the categories list from the server. In the example below, this ancestor that is shared is MyCategoriesProvider
Because the category component is likely to be in a much different place (or multiple places) in the tree than the NavBar, it's best to use context.
Honestly, this is a great place for redux, but I'm not going to push redux on you and instead will just demo a Context solution.
// We're going to create a context that will manage your categories
// The only job of this context is to hold the current categories,
// and supply the updating functions. For brevity, I'll just give
// it a handleDelete function.
// Ideally, you'd also store the status of the request in this context
// as well so you could show loaders in the app, etc
import { createContext } from 'react';
// export this, we'll be using it later
export const CategoriesContext = createContext();
// export this, we'll render it high up in the app
// it will only accept children
export const MyCategoriesProvider = ({children}) => {
// here we can add a status flag in case we wanted to show a spinner
// somewhere down in your app
const [isRequestingCategories,setIsRequestingCategories] = useState(false);
// this is your list of categories that you got from the server
// we'll start with an empty array
const [categories,setCategories] = useState([]);
const fetch = async () => {
setIsRequestingCategories(true);
setCategories(await apiCallToFetchCategories());
setIsRequestingCategories(false);
}
const handleDelete = async category => {
await apiCallToDeleteCategory(category);
// we deleted a category, so we should re-request the list from the server
fetch();
}
useEffect(() => {
// when this component mounts, fetch the categories immediately
fetch();
// feel free to ignore any warnings if you're using a linter about rules of hooks here - this is 100% a "componentDidMount" hook and doesn't have any dependencies
},[]);
return <CategoriesContext.Provider value={{categories,isRequestingCategories,handleDelete}}>{children}</CategoriesContext.Provider>
}
// And you use it like this:
const App = () => {
return (
<MyCategoriesProvider>
<SomeOtherComponent>
<SomeOtherComponent> <- let's say your PrimarySearchBar is in here somewhere
<SomeOtherComponent>
</MyCategoriesProvider>
)
}
// in PrimarySearchBar you'd do this:
function PrimarySearchBar(props) => {
const {categories} = useContext(CategoriesContext); // you exported this above, remember?
// pass it as a prop to navbar, you could easily put the useContext hook inside of any component
return <NavBar categories={categories}/>
}
// in your category component you could do this:
class Category extends Component {
render() {
// Don't forget, categoriesContext is the thing you exported way up at the top
<CategoriesContext.Consumer>
{({handleDelete}) => {
return <button onClick={() => handleDelete(this.props.category)}>
}}
</CategoriesContext.Consumer>
}
}
EDIT:
I see you're mixing class and functional components, which is fine. You should check out this article on how to use the context api in either of them - in functional components you typically use a useContext hook, while in class components you'll use a consumer.
I would just refresh the list of categories that come from the server, after the delete request is done.
I'd do it as follows:
I would make the drawer component not so smart, making it receive the list of menuItems.
<DrawerMenu
classes={classes}
handleDrawerClose={handleDrawerClose}
open={open}
items={/* ... */}
/>
This is an important step, because now, to refresh the list of items rendered, you just pass another list. The server-side logic remains disconnected from this component in this way.
I'm not sure where you render the Category components, but supposing it is rendered outside the PrimarySearchAppBar it seems that this menuItems might need to be passed to the components from an upper level. I see 2 solutions:
I'd do the request for the menuItems from the same place where I do the request for the categories:
const App = props => {
const [categories, setCategories] = React.useState([])
const [menuItems, setMenuItems] = React.useState([])
const fetchCategories = useCallback(()=> {
yourApi.getCategories().then(categories => setCategories(categories))
})
const fetchMenuItems = useCallback(() => {
yourApi.getMenuItems().then(menuItems => setMenuItems(menuItems))
})
useEffect(() => {
fetchCategories()
}, [])
useEffect(() => {
fetchMenuItems()
}, [categories])
const handleDeleteCategory = useCallback(idToDelete => {
yourApi.deleteCategory(idToDelete).then(fetchCategories)
})
return (
<div>
<PrimarySearchAppBar menuItems={menuItems}/>
<Categories categories={categories} onDeleteClick={handleDeleteCategory} />
</div>
)
}
you can do the same thing but do it with a provider and using the content API if you do not want to have all the logic here. It is good to have smart/fetches/server-side logic in a top level component and then pass down props to dumb components.
PS.
There is also a nice hook to make fetches easier:
https://github.com/doasync/use-promise
I currently use a custom version of a usePromise hook I found because I added some interesting features. I can share it if you want but I don't want to add noise to the answer.
I have a situation like this :
I want to output an array of React components based on a firebase database request.
class component exends React.component {
constructor () {
...
firebase.database().ref( '/properties/' ).once( 'value' ).then(
snapshot =>
{
var obj = snapshoot.val();
arr = Object.getOwnPropertyNames( obj )
.map( key => { name: obj[key].name, ... } );
this.setState( { elements : arr } );
}
);
...
}
...
render () {
return(
<Container>
{this.state.elements.map(el => <Element {...el} />)};
</Container>
);
}
...
}
At runtime, I find that the <Container> component has an extra element. However, when I check the size of the array just before the render function, the size is correct.
I know that the problem comes from the firebase database request because when I replace the data by order data that do not come from the firebase database everything behaves correctly.
The most astonishing is that if build the array inside the firebase request function callback without using any data providing from the firebase database the same problem arise again.
Any idea?
I fact the extra element was added by a lower level SizeMe component. I don't know why the problem only occurs with the firebase objects, However, when I wrap the content of the SizeMe component with a <div> I got rid of the extra element at the start of the array.
I have a backend Drupal site and react-native app as my frontend. I am doing a graphQL query from the app and was able to display the content/s in console.log. However, my goal is to use a call that query inside render return method and display it in the app but no luck. Notice, I have another REST API call testName and is displaying in the app already. My main concern is how to display the graphQL query in the app.
Below is my actual implementation but removed some lines.
...
import gql from 'graphql-tag';
import ApolloClient from 'apollo-boost';
const client = new ApolloClient({
uri: 'http://192.168.254.105:8080/graphql'
});
client.query({
query: gql`
query {
paragraphQuery {
count
entities {
entityId
...on ParagraphTradingPlatform {
fieldName
fieldAddress
}
}
}
}
`,
})
.then(data => {
console.log('dataQuery', data.data.paragraphQuery.entities) // Successfully display query contents in web console log
})
.catch(error => console.error(error));
const testRow = ({
testName = '', dataQuery // dataQuery im trying to display in the app
}) => (
<View>
<View>
<Text>{testName}</Text> // This is another REST api call.
</View>
<View>
<Text>{dataQuery}</Text>
</View>
</View>
)
testRow.propTypes = {
testName: PropTypes.string
}
class _TestSubscription extends Component {
...
render () {
return (
<View>
<FlatList
data={this.props.testList}
...
renderItem={
({ item }) => (
<testRow
testName={item.field_testNameX[0].value}
dataQuery={this.props.data.data.paragraphQuery.entities.map((dataQuery) => <key={dataQuery.entityId}>{dataQuery})} // Here I want to call the query contents but not sure how to do it
/>
)}
/>
</View>
)
}
}
const mapStateToProps = (state, ownProps) => {
return ({
testList: state.test && state.test.items,
PreferredTest: state.test && state.test.PreferredTest
})
}
...
There are few different things that are wrong there.
Syntax error is because your <key> tag is not properly closed here:
(dataQuery) => <key={dataQuery.entityId}>{dataQuery})
And... there is no <key> element for React Native. You can check at docs Components section what components are supported. Btw there is no such an element for React also.
Requesting data is async. So when you send request in render() this method finishes execution much earlier before data is returned. You just cannot do that way. What can you do instead? You should request data(in this element or its parent or Redux reducer - it does not matter) and after getting results you need to set state with .setState(if it happens inside the component) or .dispatch(if you are using Redux). This will call render() and component will be updated with data retrieved. There is additional question about displaying spinner or using other approach to let user know data is still loading. But it's orthogonal question. Just to let you know.
Even if requesting data was sync somehow(for example reading data from LocalStorage) you must not ever do this in render().This method is called much more frequently that you can expect so making anything heavy here will lead to significant performance degradation.
So having #3 and #4 in mind you should run data loading/fetching in componentDidMount(), componentDidUpdate() or as a part of handling say button click.
I am currently working on a react app,
and I need to implement changes on the screen that occurs when an element is on a certain position on the screen (on certain offsetTopValue),
this should be done dynamically because every element has a unique id that is created dynamically.
(it also contains 3 purecompenets and 2 functional components down the hierarchy
and most props are passed down using context)
index.jsx
class Collection extends Component {
<CollectionContext.Provider
value={{state: this.state, cards: this.props.cards...}}>
.......
ideal: (
<CollectionLayout cards= {this.props.cards ..../>
.......
</CollectionContext.Provider>
}
layout.jsx
componentDidMount () {
this._bindScroll();
}
_handleScroll = () => {
var html = document.documentElement;
this.updateCardsPositions();
}
render(
...
return(
<CollectionContext.Consumer>
{context =>
<Loader
randomSpin= {this.props.randomSpin}
currentTopic = {this.props.currentTopic}
/>}
<CollectionContext.Consumer>
);
)
loader.jsx
export const Loader = ({ randomSpin, currentTopic}) => (
.....
{!context.state.spinning &&
context.cards.map((card, index) => <Card key={index} card=
{card}/>)}
cards.jsx
export const Card = ({ card } **id={card.id}**) => {
return (...);
}
what I do have: I have a unique card id that is passed to each card,
and I can access the cards id in the layout.
I tried using refs but am not yet sure about the proper way
to pass it through all those components and access it in ComponentDidMount()
in the layout.
I tried utilizing the context but it can be accessed only inside render and not on componentDidMount.
I am also not sure how should I use the refs callback:
({(el)=>{inputRef = el}}) with a dynamic value (every card has a unique id that should be refed.
any other suggestions on how to get the offset top values from the layout would help.
I will appreciate any help or a general solution direction with it.
Thank You!
This may be more a javascript question than a react-native/meteor question: I am adding Meteor connectivity to an existing React Native app, and have run into a snag with navigation. I previously had a ListView that provided an onPress function each row that would call the navigation. In keeping with Meteor's createContainer protocol, I've used (in my case) a "PuzzlesContainer" in place of the ListView that, in a separate file, refers to
const PuzzlesContainer = ({ puzzlesReady }) => {
return (
<Puzzles
puzzlesReady={puzzlesReady}
/>
);
};
export default createContainer(() => {
const handle = Meteor.subscribe('puzzles-list');
return {
puzzlesReady: handle.ready(),
};
}, PuzzlesContainer);
This file includes the "Puzzles" file, which is also a const function that contains the MeteorListView:
const Puzzles = ({ puzzlesReady }) => {
if (!puzzlesReady) {
return null;//<Loading />;
}else{
return (
<View style={launcherStyle.container}>
<MeteorListView
collection="puzzles"
renderRow={
(puzzle) =>
<View >
<TouchableHighlight style={launcherStyle.launcher} onPress={()=>onSelect(puzzle.text)}>
<Text style={launcherStyle.text}>{puzzle.text}</Text>
</TouchableHighlight>
</View>
. . .
My problem is that there is now no context for the original routing scheme, so when I call
this.props.navigator.push
it gives "undefined is not an object (evaluating 'this.props.navigator')". How can I handle this?
One way is to look at the new NavigationExperimental, which handles nagivator in a redux fashion.
Another method is, even though I do not know if this is recommended or not, to globalize the navigator component by assigning it to a module. It can be something like this
// nav.js
let nav = null
export function setNav = _nav => nav = _nav
export function getNav = () => {
if (nav) {
return nav
} else {
throw "Nav not initialized error"
}
}
Then when you first get hold of your navigator, do this
// component.js
import { Navigator } from 'react-native'
import { setNav } from './nav'
// ...
renderScene={ (route, navigator) => {
setNav(navigator)
// render scene below
// ...
}}
As much as I liked the suggestion of globalizing my navigation, a) I never managed to do it and b) it seemed like maybe not the best practice. For anyone else who might encounter this issue, I finally succeeded by passing the navigation props in each of the JSX tags--so:
<PuzzlesContainer
navigator={this.props.navigator}
id={'puzzle contents'}
/>
in the parent (react component) file, then
<Puzzles
puzzlesReady={puzzlesReady}
navigator={navigator}
id={'puzzle contents'}
/>
in the second 'const' (Meteor container) file, and using it
<TouchableHighlight onPress={()=>navigator.replace({id: 'puzzle launcher', ... })}>
in the third 'const' (MeteorListView) file. Hope it helps someone!