Payment success message doesn’t make it to from Stripe to React app - reactjs

I have a React app which I am testing on Vercel.
The app was created using create-react-app.
I have a Stripe Account and a Django REST API, hosted on Heroku.
I’m using PaymentRequestButtonElement to generate a dynamic Apple / Google Pay button.
I’m testing Google Pay via chrome with my personal card details saved into the browser.
The price is fetched from my django app, each product is a ‘Card’, within which the payment button appears, with the price of that product passed in.
The payments go through just fine and the payment shows up in my Stripe dashboard as successful.
My /create-payment-intent/ seems functional as the payment goes through and that it generates a client secret when I test it in Postman.
The issue is simply that the React app doesn’t seem to be listening for the payment success or failure, as it doesn’t say so on the browser console or on the UI.
The google pay sheet times out and the following is printed to the console:
DEVELOPER_ERROR in loadPaymentData: An error occurred in call back, please try to avoid this by setting structured error in callback response H # pay.js:149
Here is my React component.
CheckoutForm.js
import React, { useEffect, useState } from 'react';
import {
PaymentRequestButtonElement,
useStripe,
} from '#stripe/react-stripe-js';
import axios from 'axios';
const CheckoutForm = (props) => {
const stripe = useStripe();
const [paymentRequest, setPaymentRequest] = useState(null);
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
// const price = props.price;
// const dollar_price = price * 100;
useEffect(() => {
if (stripe) {
const pr = stripe.paymentRequest({
country: 'US',
currency: 'usd',
total: {
label: 'Purchase',
amount: 100,
},
requestPayerName: true,
requestPayerEmail: true,
applePay: true,
googlePay: true,
});
pr.canMakePayment().then(result => {
if (result) {
setPaymentRequest(pr);
}
});
}
}, [stripe]);
useEffect(() => {
if (paymentRequest) {
paymentRequest.on('paymentmethod', async event => {
const paymentMethod = event.paymentMethod;
try {
const response = await axios.post(
'https://my-api.com/create-payment-intent/',
{
paymentMethodId: paymentMethod.id,
amount: 100,
automatic_payment_methods: {
'enabled': true,
},
currency: 'usd',
}
);
const pi = await stripe.confirmCardPayment(response.data.client_secret, {
payment_method: paymentMethod.id
});
if (pi.status === 'succeeded') {
event.complete();
console.log('Payment succeeded!');
setPaymentSuccess(true);
setErrorMessage(null);
setSuccessMessage("Payment succeeded!");
} else if (pi.status === 'requires_action' || pi.status === 'requires_confirmation') {
event.complete('success');
console.log('Additional steps required!');
setErrorMessage(null);
setSuccessMessage("Additional steps required, please check your email for further instructions.");
// Prompt user to complete additional steps
} else if (pi.status === 'requires_payment_method') {
event.complete('fail');
console.log('Payment method required!');
setErrorMessage("Payment method required. Please add a new payment method.");
// Prompt user to add a new payment method
} else if (pi.status === 'processing') {
event.complete('success');
console.log('Payment is being processed!');
setErrorMessage(null);
setSuccessMessage("Payment is being processed. Please wait.");
// Show a message to the user that the payment is being processed
} else if (pi.status === 'canceled') {
event.complete('fail');
console.log('Payment canceled!');
setErrorMessage("Payment canceled.");
// Show a message to the user that the payment was canceled
} else if (pi.status === 'failed') {
event.complete('fail');
console.log('Payment failed!');
setErrorMessage("Payment failed. Please check your information and try again.");
// Show a message to the user that the payment failed
}
} catch (error) {
event.complete('fail');
console.log('An error occurred:', error);
setErrorMessage("An error occurred. Please try again later.");
// Show an error message to the user
}
});
}
}, [paymentRequest, stripe]);
if (paymentRequest) {
return <>
{paymentSuccess && <p>Payment Successful!</p>}
<PaymentRequestButtonElement options={{ paymentRequest }} />
</>
}
return 'Insert your form or button component here.';
};
export default CheckoutForm;
Here is my index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {Elements} from '#stripe/react-stripe-js';
import {loadStripe} from '#stripe/stripe-js';
import CheckoutForm from './CheckoutForm';
// Make sure to call `loadStripe` outside of a component's render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe('pk_live_123');
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
</React.StrictMode>
);
reportWebVitals();
And here is my django view
class PaymentIntentView(APIView):
def post(self, request, *args, **kwargs):
amount = request.data.get('amount')
currency = request.data.get('currency')
automatic_payment_methods = request.data.get('automatic_payment_methods')
try:
intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
automatic_payment_methods={
'enabled': True,
},
# You can also add other options like capture_method, setup_future_usage, etc.
)
return Response({'client_secret': intent.client_secret, 'id': intent.id})
except Exception as e:
return Response({'error': str(e)})
I feel like I’m missing a step here.
Happy to share more info if necessary.
I'm expecting the console and the UI to produce a success or failure message, but it just times out. The payment appears as successful in my Stripe Dashboard.

Inside the function paymentRequest.on('paymentmethod'... you have the call to your endpoint "https://my-api.com/create-payment-intent/" then a subsequent JS call to const pi = await stripe.confirmCardPayment.
From what you described, it looks like some bits are off around this area. You can put breakpoints on the endpoint call and the confirmCardPayment to see if they really go through. You can also open your browser console to see whether the real ajax call succeeded (instead of testing in Postman).
If they are succeeded, what is the pi.status on your console.log?

Related

How to fix invalid hook call error in my airtable extension?

I'm getting this error in my Airtable blocks extension:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm unsure as to why. My program uses airtable blocks api to create a dropdown that sets a useState value, before useEffect updates a database live. Both hooks are being used inside the function body, and the function should be a react component, so I don't understand where the error lies. I've seen it can be due to react version conflicts and such as well, but I'm not sure how to confirm whether or not that is the underlying issue.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import {
Select,
initializeBlock,
SelectSynced,
useBase,
useRecords,
BaseProvider,
useGlobalConfig,
expandRecord,
TablePickerSynced,
ViewPickerSynced,
FieldPickerSynced,
FormField,
Input,
Button,
Box,
Icon,
} from '#airtable/blocks/ui';
import { FieldType } from '#airtable/blocks/models';
const base = useBase();
const table = base.getTable("National Works In Progress");
export default function FilterApp() {
// YOUR CODE GOES HERE
let records = useRecords(table);
var aunumbers_array = [];
const [value, setValue] = useState("");
const queryResult = table.selectRecords({ fields: ["AU Number"] });
records.forEach(function (x) {
if (aunumbers_array.indexOf(x.getCellValueAsString("AU Number"), -1)) {
aunumbers_array.push({ value: x.getCellValueAsString("AU Number"), label: x.getCellValueAsString("AU Number") })
}
});
queryResult.unloadData();
let updates = [];
useEffect(() => {
const fetchData = async () => {
records.forEach(function (x) {
if (x.getCellValueAsString('AU Number') == value) {
updates.push({ id: x.id, fields: { 'Matches Filter': true } });
console.log(value);
}
else if (x.getCellValueAsString('AU Number') !== value && x.getCellValueAsString('Matches Filter') == 'checked') {
updates.push({ id: x.id, fields: { 'Matches Filter': false } });
}
});
while (updates.length) {
await table.updateRecordsAsync(updates.splice(0, 50));
}
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [value])
return (
<div>
<FormField label="Text field">
<Select
options={aunumbers_array}
value={value}
onChange={newValue => setValue(newValue.toString())}
width="320px"
/>
</FormField>
</div>
);
}
The error is because you are calling useBase, a custom hook in an invalid way.
const base = useBase();
This is wrong way of calling a hook as hooks can only be called inside of the body of a function component.
Move the hook call inside FilterApp() functional component.

Invalid value for stripe.confirmPayment(): elements should have a mounted Payment Element

I'm trying to integrate a Stripe payments page using the React "Elements". I'm following the tutorial from https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements#web-submit-payment and I've gotten to step 5, "Submit the payment to Stripe". My code doesn't look much different from the example, but whenever I try to submit a payment this error:
Invalid value for stripe.confirmPayment(): elements should have a mounted Payment Element.
Thrown from stripe.confirmPayment. I've included the <Elements/> and <PaymentElement/> on the page, and passed the return value from useElements() so I'm really not sure what I'm missing.
Here's my checkout form:
function StripeCheckoutForm({paymentIntent,booking}: StripeCheckoutFormProps) {
const stripe = useStripe();
const elements = useElements();
const [confirming,setConfirming] = useState(false)
const handleSubmit = async (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!stripe || !elements) {
notifyWarning("Still loading. Please wait a few seconds and then try again.");
return;
}
setConfirming(true)
try {
const {error} = await stripe.confirmPayment({
//`Elements` instance that was used to create the Payment Element
elements,
confirmParams: {
return_url: resolveRoute('customerPaymentReceived',{key:booking.key}),
},
})
if(error) {
notifyError(error.message)
setConfirming(false)
}
} catch(error) {
setConfirming(false)
if(error?.message) {
notifyError(error.message) // <-- error shown here
} else {
notifyError("Something went wrong");
}
}
}
return (
<form onSubmit={handleSubmit}>
<PaymentElement/>
<BlockSpacer height="1rem"/>
<ActionButton disabled={!stripe || !elements || confirming} type="submit" className="btn-phone btn-fullwidth">Pay {paymentIntent.amountFormatted}</ActionButton>
</form>
)
}
And that form is inside this component, similar to the example shown in step 4:
import {Elements as StripeElements} from '#stripe/react-stripe-js';
import {useStripe, useElements, PaymentElement} from '#stripe/react-stripe-js';
function StripePaymentForm({stripeAccountId,paymentIntent,booking}: StripePaymentFormProps) {
const options = {
clientSecret: paymentIntent.clientSecret,
loader: 'always',
}
return (
<StripeElements stripe={getStripeConnect(stripeAccountId)} options={options}>
<StripeCheckoutForm paymentIntent={paymentIntent} booking={booking}/>
</StripeElements>
)
}
The only thing I can see that's different is that I'm using a Connect account, so I'm passing in account ID in when I load Stripe. getStripeConnect is basically
loadStripe(STRIPE_PUBLIC_KEY, {stripeAccount: CONNECTED_ACCOUNT_ID})
You can see the React component tree here if it helps:
What am I missing?
I'm guessing this stems from useElements() is not finding any elements:
But I still don't know why.
I believe it's a recent bug in react-stripe-js:
https://github.com/stripe/react-stripe-js/issues/296
I believe your loadStripe() promise isn't resolved at the time it is passed to the Elements provider, thus useElements returns null. Either load it earlier or await it and it should resolve your issue.

How to keep MetaMask connection to the UI persistent with Web3-react?

I am working with web3-react and I cannot figure out how to keep the connection to the MetaMask wallet persistent upon browser refreshes.
This is the code:
// define the injectedConnectors
const injectedConnector = new InjectedConnector({
supportedChainIds: [
1, // Mainet
3, // Ropsten
4, // Rinkeby
5, // Goerli
42, // Kovan
],
})
const { chainId, account, activate, active } = useWeb3React()
// activate the wallet
activate(injectedConnector)
console.log(account)
// all good.
Up to here all is working and I activate my MetaMask wallet as well as I get the account correctly logged, and the active variable is a boolean that changes to true.
The problem is that when I refresh the page the active turns to false and I lose the connection between the UI to the MetaMask wallet. Of course saving active into the browser does not change anything because the connection relies on the active boolean value.
The docs are lacking such information.
Finally found a solution!
I was trying to use the example in the official library using ... but for some reason it wasn't working though no error came out.
Then I stumbled upon some guy who had the same issue and posted on reddit and got a good answer that works for me.
This is the link to the post: https://www.reddit.com/r/ethdev/comments/nw7iyv/displaying_connected_wallet_after_browser_refresh/h5uxl88/?context=3
and this is the code from that post:
First create a file that holds the injectedConnector called connectors.js:
import { InjectedConnector } from '#web3-react/injected-connector'
export const Injected = new InjectedConnector({ supportedNetworks: [1, 3, 4, 5, 42] })
Then create a component that checks if the user already activated the wallet:
import React, { useEffect, useState } from 'react'
import { injected } from '../connectors'
import { useWeb3React } from '#web3-react/core'
function MetamaskProvider({ children }) {
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React()
const [loaded, setLoaded] = useState(false)
useEffect(() => {
injected
.isAuthorized()
.then((isAuthorized) => {
setLoaded(true)
if (isAuthorized && !networkActive && !networkError) {
activateNetwork(injected)
}
})
.catch(() => {
setLoaded(true)
})
}, [activateNetwork, networkActive, networkError])
if (loaded) {
return children
}
return <>Loading</>
}
export default MetamaskProvider
And wrap MetamaskProvider around the components you want the wallet to be activated upon refresh:
return (
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
<StylesProvider injectFirst>
<Paper>
<Router>
<Web3ReactProvider getLibrary={getLibrary}>
<MetamaskProvider>
{...children components}
</MetamaskProvider>
</Web3ReactProvider>
</Router>
</Paper>
</StylesProvider>
</ThemeProvider>
);
Its actually really simple. You can just store the connect address in local storage and when the user clicks the disconnect button then remove the address from local storage. basically we use the condition that if there is an acccount in local storage then we connect on load and if not then we have to manually click the connect button. Consider the code below. Note that ideally you should write the logic as a hook and use the hook in the main app then pass in the props the "active" status which is returned from useWeb3React(). but for the purpose of this example i just keep the connect logic in one file to make it read easier
import React, { useState, useEffect } from 'react';
import Web3 from 'web3';
import detectEthereumProvider from '#metamask/detect-provider';
import { useWeb3React } from "#web3-react/core"
import { InjectedConnector } from '#web3-react/injected-connector'
//declare supportated chains
export const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42, 1337, 43114],
})
export default function connButton() {
var web3;
var accounts;
var connected
const [loading, setLoading] = useState(false)
//here we can destructure out various things from web3React such as
//active (which is true if the user is connected and false otherwise)
//activate and deactiveate which we use to instansiate and break the users
//connection
const { active, account, library, connector, activate, deactivate } = useWeb3React()
//set up an elemnt in local storage that we use to hold the connected account
var acc = localStorage.getItem("account")
//function that initialises web3.js
const connectWalletHandler = () => {
if (window.ethereum && window.ethereum.isMetaMask) {
console.log('MetaMask Here!');
web3 = new Web3(window.ethereum);
window.ethereum.request({ method: 'eth_requestAccounts'})
} else {
console.log('Need to install MetaMask');
// setErrorMessage('Please install MetaMask browser extension to interact');
}
console.log(web3.eth.currentProvider)
}
//function that is called on page load if and only if their exists and
//item for the user accoun tin local storage
async function connectOnLoad() {
try {
//here we use activate to create the connection
await activate(injected)
connected = true
} catch (ex) {
console.log(ex)
}
//we use web3.eth to get the accounts to store it in local storage
var accounts1 = await web3.eth.getAccounts();
acc = localStorage.setItem("account", accounts1);
}
//here we use a useEffect so that on page load we can check if there is
//an account in local storage. if there is we call the connect onLoad func
//above which allows us to presist the connection and i also call connectWalletHandler
which sets up web3.js so we can call web3.eth.getAccounts()
useEffect(() => {
if (acc != null) {
connectOnLoad()
}
connectWalletHandler()
}, [])
//however in the case where there is no item in local storage we use this
//function to connect which is called when we click the connect button. its
//essentially the same but we check if local storage is null if it is we activate
//if its not then we disconnect. And when we disconnect we remove the acccount from local storage
async function connectOnClick() {
if (localStorage.getItem("account") == null) {
setLoading(true);
try {
await activate(injected)
connected = true
} catch (ex) {
console.log(ex)
}
// window.location.reload();
var accounts1 = await web3.eth.getAccounts();
console.log(accounts1)
acc = localStorage.setItem("account", accounts1);
console.log(acc)
setTimeout(function(){
setLoading(false)
}, 1600);//wait 2 seconds
} else {
disconnect();
connected = false
}
}
async function disconnect() {
try {
deactivate()
localStorage.removeItem("account");
} catch (ex) {
console.log(ex)
}
}
return (
//remember the active boolean from useReactWeb3() stores a bool
//depending on if the user is or is not connected there for we can
//use this as a conditon to render the button saying "Connect Wallet"
or displaying their address as the text.
<div>
{active ? <button onClick={connectOnClick}>{account.substring(0, 6)}...{account.substring(account.length - 4)}</button> : <button onClick={connectOnClick}>Connect Wallet</button>}
</div>
);
}
then in your app.js remember to wrap your entire app in the tag. remember this means you need to import web3React into your app.js also

Stripe payment button for ReactJs with a simple Snippet Code

I have found a lot of repositories and examples about installing an Stripe complete checkout, but I cannot just use the simple Snippet code that Stripe offers for a product. How can I use that code on a page in my React Project? Here is the code. I just want to redirect the user to the Stripe checkout page for that product, I don't want to use my own formulary and I don't want either to collect data in my app. Thanks a lot.
<!-- Load Stripe.js on your website. -->
<script src="https://js.stripe.com/v3"></script>
<!-- Create a button that your customers click to complete their purchase. Customize the styling to suit your branding. -->
<button
style="background-color:#6772E5;color:#FFF;padding:8px 12px;border:0;border-radius:4px;font-size:1em"
id="checkout-button-price_1Heree568gerg54rtretrt"
role="link"
type="button"
>
Checkout
</button>
<div id="error-message"></div>
<script>
(function() {
var stripe = Stripe('pk_live_t5tyutrytutruytyutyufake....');
var checkoutButton = document.getElementById('checkout-button-price_1Heree568gerg54rtretrt');
checkoutButton.addEventListener('click', function () {
// When the customer clicks on the button, redirect
// them to Checkout.
stripe.redirectToCheckout({
lineItems: [{price: 'price_1Heree568gerg54rtretrt', quantity: 1}],
mode: 'subscription',
// Do not rely on the redirect to the successUrl for fulfilling
// purchases, customers may not always reach the success_url after
// a successful payment.
// Instead use one of the strategies described in
// https://stripe.com/docs/payments/checkout/fulfill-orders
successUrl: 'https://myweb.com/success',
cancelUrl: 'https://myweb.com/canceled',
})
.then(function (result) {
if (result.error) {
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer.
var displayError = document.getElementById('error-message');
displayError.textContent = result.error.message;
}
});
});
})();
</script>
You can create a dedicated component for that. As stated in the documentation, I am using StripeJS to import it as a module.
// npm install #stripe/stripe-js
import React from 'react';
import {loadStripe} from '#stripe/stripe-js';
const StripeButton = (props) => {
const [stripeError, setStripeError] = React.useState(null);
const [stripe, setStripe] = React.useState(null);
useEffect( async () => {
if (!stripe) {
// Here, you can use some `props` instead of hardcoding the API key
const stripeTmp = await loadStripe('pk_live_t5tyutrytutruytyutyufake....');
setStripe(stripeTmp);
}
});
const handleClick = () => {
// Reset error holder
setStripeError(null);
// When the customer clicks on the button, redirect
// them to Checkout.
stripe.redirectToCheckout({
// Here you can use another `prop` instead of hard coding it
lineItems: [{price: 'price_1Heree568gerg54rtretrt', quantity: 1}],
mode: 'subscription',
// Do not rely on the redirect to the successUrl for fulfilling
// purchases, customers may not always reach the success_url after
// a successful payment.
// Instead use one of the strategies described in
// https://stripe.com/docs/payments/checkout/fulfill-orders
successUrl: 'https://myweb.com/success',
cancelUrl: 'https://myweb.com/canceled',
})
.then(function (result) {
if (result.error) {
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer.
setStripeError(result.error.message);
}
});
}
return (
<>
{ stripe ? (
<button
style="background-color:#6772E5;color:#FFF;padding:8px 12px ;border:0;border-radius:4px;font-size:1em"
id="checkout-button-price_1Heree568gerg54rtretrt"
role="link"
type="button"
onClick={ handleClick }
>
Checkout
</button>
) : "Loading..."
}
{ stripeError ? <div id="error-message">{ stripeError }</div> : null }
</>
)
}
export default StripeButton;

How to link to a show view from an index using react hooks with firestore data

I am trying to figure out how to define a link to reference that can use a firebase document id to link to a show view for that document. I can render an index. I cannot find a way to define a link to the document.
I've followed this tutorial - which is good to get the CRUD steps other than the show view. I can find other tutorials that do this with class components and the closest I've been able to find using hooks is this incomplete project repo.
I want to try and add a link in the index to show the document in a new view.
I have an index with:
const useBlogs = () => {
const [blogs, setBlogs] = useState([]); //useState() hook, sets initial state to an empty array
useEffect(() => {
const unsubscribe = Firebase
.firestore //access firestore
.collection("blog") //access "blogs" collection
.where("status", "==", true)
.orderBy("createdAt")
.get()
.then(function(querySnapshot) {
// .onSnapshot(snapshot => {
//You can "listen" to a document with the onSnapshot() method.
const listBlogs = querySnapshot.docs.map(doc => ({
//map each document into snapshot
id: doc.id, //id and data pushed into blogs array
...doc.data() //spread operator merges data to id.
}));
setBlogs(listBlogs); //blogs is equal to listBlogs
});
return
// () => unsubscribe();
}, []);
return blogs;
};
const BlogList = ({ editBlog }) => {
const listBlog = useBlogs();
return (
<div>
{listBlog.map(blog => (
<Card key={blog.id} hoverable={true} style={{marginTop: "20px", marginBottom: "20px"}}>
<Title level={4} >{blog.title} </Title>
<Tag color="geekblue" style={{ float: "right"}}>{blog.category} </Tag>
<Paragraph><Text>{blog.caption}
</Text></Paragraph>
<Link to={`/readblog/${blog.id}`}>Read</Link>
<Link to={`/blog/${blog.id}`}>Read</Link>
</Card>
))}
</div>
);
};
export default BlogList;
Then I have a route defined with:
export const BLOGINDEX = '/blog';
export const BLOGPOST = '/blog/:id';
export const NEWBLOG = '/newblog';
export const EDITBLOG = '/editblog';
export const VIEWBLOG = '/viewblog';
export const READBLOG = '/readblog/:id';
I can't find a tutorial that does this with hooks. Can anyone see how to link from an index to a document that I can show in a different page?
I did find this code sandbox. It looks like it is rendering a clean page in the updateCustomer page and using data from the index to do it - but the example is too clever for me to unpick without an explanation of what's happening (in particular, the updateCustomer file defines a setCustomer variable, by reference to useForm - but there is nothing in useForm with that definition. That variable is used in the key part of the file that tries to identify the data) - so I can't mimic the steps.
NEXT ATTEMPT
I found this blog post which suggests some changes for locating the relevant document.
I implemented these changes and while I can print the correct document.id on the read page, I cannot find a way to access the document properties (eg: blog.title).
import React, { useHook } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
return (
<div>{slug}
</div>
)
};
export default ReadBlogPost;
NEXT ATTEMPT:
I tried to use the slug as the doc.id to get the post document as follows:
import React, { useHook, useEffect } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
console.log("Document data:", doc.data());
doc.data();
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
});
return (
<div>{blog.title}
</div>
)
};
export default ReadBlogPost;
It returns an error saying blog is not defined. I also tried to return {doc.title} but I get the same error. I can see all the data in the console.
I really can't make sense of coding documentation - I can't figure out the starting point to decipher the instructions so most things I learn are by trial and error but I've run out of places to look for inspiration to try something new.
NEXT ATTEMPT
My next attempt is to try and follow the lead in this tutorial.
function ReadBlogPost(blog) {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
doc.data()
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
},
[blog]
);
return (
<div><Title level={4} > {blog.title}
</Title>
<p>{console.log(blog)}</p>
</div>
)
};
export default ReadBlogPost;
When I try this, the only odd thing is that the console.log inside the useEffect method gives all the data accurately, but when I log it form inside the return method, I get a load of gibberish (shown in the picture below).
NEXT ATTEMPT
I found this tutorial, which uses realtime database instead of firestore, but I tried to copy the logic.
My read post page now has:
import React, { useHook, useEffect, useState } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
import { Card, Divider, Form, Icon, Input, Switch, Layout, Tabs, Typography, Tag, Button } from 'antd';
const { Paragraph, Text, Title } = Typography;
const ReadBlogPost = () => {
const [loading, setLoading] = useState(true);
const [currentPost, setCurrentPost] = useState();
let { slug } = useParams()
if (loading && !currentPost) {
Firebase
.firestore
.collection("blog")
.doc(slug)
.get()
.then(function(doc) {
if (doc.exists) {
setCurrentPost(...doc.data());
console.log("Document data:", doc.data());
}
}),
setLoading(false)
}
if (loading) {
return <h1>Loading...</h1>;
}
return (
<div><Title level={4} >
{currentPost.caption}
{console.log({currentPost})}
</Title>
</div>
)
};
export default ReadBlogPost;
Maybe this blog post is old, or maybe it's to do with it using .js where I have .jsx - which I think means I can't use if statements, but I can't get this to work either. The error says:
Line 21:9: Expected an assignment or function call and instead saw
an expression no-unused-expressions
It points to the line starting with Firebase.
I got rid of all the loading bits to try and make the data render. That gets rid of the above error message for now. However, I still can't return the values from currentPost.
It's really odd to me that inside the return statement, I cannot output {currentPost.title} - I get an error saying title is undefined, but when I try to output {currentPost} the error message says:
Error: Objects are not valid as a React child (found: object with keys
{caption, category, createdAt, post, status, title}). If you meant to
render a collection of children, use an array instead.
That makes no sense! I'd love to understand why I can log these values before the return statement, and inside the return statement, I can log them on the object but I cannot find how to log them as attributes.
First of all: is your useBlog() hook returning the expected data? If so, all you need to do is define your <Link/> components correctly.
<Link
// This will look like /readblog/3. Curly braces mean
// that this prop contains javascript that needs to be
// evaluated, thus allowing you to create dynamic urls.
to={`/readblog/${blog.id}`}
// Make sure to open in a new window
target="_blank"
>
Read
</Link>
Edit: If you want to pass the data to the new component you need to set up a store in order to avoid fetching the same resource twice (once when mounting the list and once when mounting the BlogPost itself)
// Define a context
const BlogListContext = React.createContext()
// In a top level component (eg. App.js) define a provider
const App = () => {
const [blogList, setBlogList] = useState([])
return (
<BlogListContext.Provider value={{blogList, setBlogList}}>
<SomeOtherComponent/>
</BlogListContext.Provider>
)
}
// In your BlogList component
const BlogList = ({ editBlog }) => {
const { setBlogList } = useContext(BlogListContext)
const listBlog = useBlogs()
// Update the blog list from the context each time the
// listBlog changes
useEffect(() => {
setBlogList(listBlog)
}, [listBlog])
return (
// your components and links here
)
}
// In your ReadBlog component
const ReadBlogComponent = ({ match }) => {
const { blogList } = useContext(BlogListContext)
// Find the blog by the id from params.
const blog = blogList.find(blog => blog.id === match.params.id) || {}
return (
// Your JSX
)
}
There are other options for passing data as well:
Through url params (not recommended).
Just pass the ID and let the component fetch its own data on mount.
I found an answer that works for each attribute other than the timestamp.
const [currentPost, setCurrentPost] = useState([]);
There is an empty array in the useState() initialised state.
In relation to the timestamps - I've been through this hell so many times with firestore timestamps - most recently here. The solution that worked in December 2019 no longer works. Back to tearing my hair out over that one...

Resources