Linking or Routing dynamicaly with Next.js - reactjs

I am trying to find the best way to link to a details page from a list of objects that are mapped. The list fetches items from an API and works fine. The problem is that I cant pass the id to my details page and get this error when I click on the objects.
:3000/_next/static/development/pages/_app.js?ts=1592696161086:1299 GET http://localhost:3000/flowers/[object%20Object] 404 (Not Found)
and in the url http://localhost:3000/flowers/[object%20Object]
This is what I have in my /pages/flowers.js
import React, { useState, useEffect } from 'react'
import Head from 'next/head'
import Layout from '../components/layout'
import Link from 'next/link'
import utilStyles from '../styles/utils.module.css'
export default function Flowers() {
const LISTURL = 'https://flowers-mock-data.firebaseio.com/flowers.json'
const TESTURL = 'https://flowers-mock-data.firebaseio.com/flowers/9.json'
const [items, setItems] = useState([])
useEffect(() => {
fetch(LISTURL)
.then((res) => res.json())
.then((json) => setItems(json))
}, [] )
return (
<Layout>
<Head>
<title>🌸</title>
</Head>
<section className={utilStyles.centerSection}>
<button>Shade loving plants</button>
<button>Sun loving plants</button>
</section>
{items.map((item) => (
<ul key={item._id.oid}>
<li className={utilStyles.listItem}>
<Link href='/flowers/[flowerid]' as={`/flowers/${item._id}`}>
<a className={utilStyles.list}>
<h2 className={utilStyles.headingTop}>{item.common_name}</h2>
<img className={utilStyles.imgList} src={`${item.cover_image}`} alt={item.common_name} />
</a>
</Link>
</li>
</ul>
))
}
</Layout>
)
}
This is pages/[flowerid].js
import React, { useEffect, useState } from 'react'
import Head from 'next/head'
import Router from 'next/router'
import axios from 'axios'
import Layout from '../components/layout'
import utilStyles from '../styles/utils.module.css'
const FlowerDetail = () => {
const [flower, setFlower] = useState(null)
useEffect(() => {
const fetchData = async () => {
const { flowerId } = Router.query
try {
const { data } = await axios.get(
`https://flowers-mock-data.firebaseio.com/flowers/${flowerId}.json`
)
console.log(`blomma`, data)
setFlower(data)
} catch (error) {
console.log(`Can not find id`, error)
}
}
fetchData()
}, [])
if (!flower) return <div>Loading...</div>
console.log('no flowers to see')
return (
<Layout>
<Head>
<title>🌸</title>
</Head>
<div>
<p>Blooming season {flower.blooming_season}</p>
<img className={utilStyles.imgList} src={`${flower.cover_image}`} alt={flower.common_name} />
<p>Common name {flower.common_name}</p>
<h5>{flower.notes}</h5>
</div>
</Layout>
)
}
export default FlowerDetail;

The problems holding you back...
The _id coming from your API is an object which contains an oid property:
"_id": {
"oid": "5e57880c9fa50a095f475c18"
},
Therefore you'll want to use the items._id.oid property as a link source. As is, you're passing the id object to the link source, which is why your links are invalid: http://localhost:3000/flowers/[object%20Object]
Unfortunately, that's not the only problem. The other problem is that the current data structure doesn't contain any reference to the URL parameter id (where 1.json is supposed to reference oid: 5e579cdfe99bdd0ca1cf64a3, 2.json references oid: 5e579dfcb664120cd14b4331, and so on), so you'll need to reconfigure your API data to include a property with this URL id within each data document (id: 1, id: 2, ...etc) and then use that id as your link source OR you'll have to reconfigure your API to utilize this oid property as a URL parameter to match against (instead of 1.json it'll use the oid as a URL parameter to match against and be 5e579cdfe99bdd0ca1cf64a3.json).
For example, this API endpoint (which comes from http://jsonplaceholder.typicode.com) contains an id property that is used as a URL parameter to retrieve the specific JSON file by its id (notice how both the URL and the JSON data contain an id of 9).
On a side note, if you're using Chrome, you can install this extension to prettify/format JSON, so it looks like this.
OID doesn't match the requested API endpoint
For reference, here's how your response is currently structured when you query this endpoint (notice that the 9 in https://flowers-mock-data.firebaseio.com/flowers/9.json isn't referenced/used anywhere within the JSON data, so using the oid (https://flowers-mock-data.firebaseio.com/flowers/5e57a0e1e5f2470d789335c2.json) as a parameter won't be valid):
{
"__v": {
"numberInt": "0"
},
"_id": {
"oid": "5e57a0e1e5f2470d789335c2"
},
"blooming_season": "Late Spring",
"common_name": "Ornamental onion",
"cover_image": "https://images.unsplash.com/photo-1565685225009-fc85d9109c80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80",
"depth": {
"numberInt": "3"
},
"height": [
{
"numberInt": "6"
},
{
"numberInt": "60"
}
],
"latin_name": "Allium",
"notes": "Usually pest-free; a great cut flower",
"soil": [
"well drained",
"moist",
"fertile"
],
"spacing": {
"numberInt": "12"
},
"sun": true
}
Example fetch data repo
I've put together an example with some comments that incorporates the API endpoint (jsonplaceholder) mentioned in the paragraph above, as well as implementing some of nextjs's data fetching methods (excluded getStaticPaths for simplicity; however, technically, it can replace getServerSideProps in the dynamic pages/users/[id].js page since the data will never be changed/updated during runtime):
fetch data example repo

Related

Confusion about handling graphQL data in a Nextjs project

I'm building a webpage with Next.js and graphQL, using Apollo as a client.
On on the main page (index.js) I want a "capsules" and "past launches" link. When the user clicks on either of these links, it directs them to a page detailing either the capsules or the past launches, where mission names are displayed. This details page is controlled by [parameters].js
The problem is this: I want render either the name of the capsule or the launch, depending on what the user has clicked on. However, in the data returned from the API call, the name properties are described using different keys: for capsules the key is name and past launches is mission_name. API data as follows:
{
"data": {
"capsules": [
{
"landings": 1,
"missions": [
{
"flight": 14,
"name": "CRS-3"
}
]
}
],
"launchesPast": [
{
"mission_name": "Starlink-15 (v1.0)",
"id": "109"
}
]
}
}
This presents an issue, because when I'm mapping through the returned data in [parameters].js I can only use either name or mission_name to select and render the name property - which isn't useful since I want to dynamically render the name of whatever element the user clicks on.
What's the best way to handle my API data/structure my project so I can render the relevant data, despite different key names?
Here's my index page - please ignore the other links:
index.js
import { useContext } from 'react'
import { LaunchContext } from '../spacexContext'
import Link from 'next/link'
import Card from '../components/Card'
export const Home = () => {
const data= useContext(LaunchContext)
return (
<div>
<Link href = {`/launchesPast`}>
<div>{data.launchesPast.length} launches last year</div>
</Link>
<Link href = {`/launchesUpcoming`}>
<div> Upcoming launches</div>
</Link>
<Link href = {`/missions`}>
<div>Missions</div>
</Link>
<Link href = {`/payloads`}>
<div> Payloads</div>
</Link>
<Link href = {`/users`}>
<div>Users</div>
</Link>
<Link href = {`/coresPast`}>
<div>Used cores to date</div>
</Link>
</div>
)
}
export default Home
[parameters].js
import { useRouter } from 'next/router'
import { useContext,useState,useEffect } from 'react'
import { LaunchContext } from '../spacexContext'
const Items = () => {
const [path, setPath] = useState("");
const data = useContext(LaunchContext);
const router = useRouter();
useEffect(() => {
if (router.isReady) {
setPath(router.query.parameter);
}
}, [router.isReady]);
return (
<div>
{data &&
path &&
data[path].map((item, i) => {
return (
<>
<p>{item.mission_name}</p>
<p>{item.launch_date_local}</p>
</>
)
})}
</div>
);
};
export default Items;
It occured to me to have some sort of conditional in [paramter].js as follows:
let name
if(item.mission_name){
name = mission_name
} else if (item.name) {
name = item.name
}
...however, this feels like a problematic solutution.

Passing props to an axios delete request

The objective is to, by pressing a button, delete the object from the database.
To do that, I have to pass the ID of the object I want to delete from the database to the axios query. But I'm stuck trying to do it.
In my opinion the problem is I am not passing the ID to erase to the query, since the query seems right to me.
File: persons.js
import axios from 'axios'
const baseUrl = 'http://localhost:3001/persons'
const deleteContact = (id) =>{
const request = axios.delete('{$baseUrl}/${id}')
request.then(response =>response.data)
}
export default {
deleteContact: deleteContact,
}
The button that should call the function to delete:
File: person.js
import React from 'react'
const Person = ({ person, deleteContact }) => {
return (
<li>
{person.name} {person.number}
<button onClick={deleteContact(person.id)}>Delete {person.id} </button>
</li>
)
}
export default Person
So, by pressing the button I execute the deleteContact funtion and I pass to that function the person.id so it sends the id to delete.
Here is waht's wrong. I don't know how to make the function deleteContact.
I have tried this, but of course I am not sending any props. It's wrong and does nothing. I get the error TypeError: deleteContact is not a function.
const deleteContact = (id) => {
}
The deleteContact funtion I try to implement is on the file App.js
It is something obvious I am missing here. But I can't figure out what is.
Likely something basic, but I have been stuck here for a while, as silly this may seem to be.
File: App.js
import React, { useState, useEffect } from 'react'
import Person from './components/Person'
import Form from './components/Form'
import Filter from './components/Filter'
import FilterResults from './components/FilterResults'
import contactService from './services/persons'
//npm run server
const App = () => {
//Reminder: current state, function that updates it, initial state.
const [ newName, setNewName ] = useState('')
const [ newNumber, setNewNumber ] = useState('')
const [contacts, setContacts] = useState([])
//Filter
const [ filter, setFilter ] = useState('')
//contactService is importer from /services/persons.
//.getAll is like typing: axios.get('http://localhost:3001/persons')
//Effect hooks used to fetch data from the server. The data fetched is saved
//into the contacts state variable
useEffect(() => {
contactService
.getAll()
.then(response => {
setContacts(response.data)
console.log(contacts)
})
}, [])
/*
second parameter of useEffect is used to specify how often the effect
is run. If the second parameter is an empty array [],
then the effect is only run along with the first render of
the component. */
console.log('render', contacts.length, 'contacts')
//adding new persons
const addPerson = (event) => {
event.preventDefault()
/* complete the addPerson function for creating new persons */
const personObject = {
name: newName,
number: newNumber,
//The server will create the id
//id: persons.length + 1,
}
//Adding the data to the server
/*
using separate server comunication module from persons.js
"create" instead of previous code:
axios
.post('http://localhost:3001/persons', personObject)
replaced by:
contactService
.create(personObject)
*/
contactService
//Passing personObject to create
.create(personObject)
.then(response => {
console.log(response)
//After concat, the fiel is set to blank again ('').
//Updating state after creating, to display created contact.
setContacts(contacts.concat(personObject))
setNewName('')
setNewNumber('')
})
}
//Delete contacts
const deleteContact = (personObject) => {
}
const handlePersonChange = (event) => {
console.log(event.target.value)
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
console.log(event.target.value)
setNewNumber(event.target.value)
}
const handleFilterChange = (event) => {
setFilter(event.target.value)
}
const personsToShow = filter === ''
? contacts
: contacts.filter(person =>
person.name.toLowerCase().includes(filter.toLowerCase()))
const row_names = () => personsToShow.map(person =>
<p key={person.name}>{person.name} {person.number} </p>
)
return (
<div>
<Filter value={filter} onChange={handleFilterChange} />
<Form
onSubmit={addPerson}
name={{value: newName, onChange: handlePersonChange}}
number={{value: newNumber, onChange: handleNumberChange}}
deleteContacts={() => deleteContact()}
/>
<h2>Numbers from database</h2>
{/* The contents of the database are stored on the variable contacts.
I map through the array. Person.js component used. */}
<ul>
{contacts.map(person =>
//Pass all the props from person to Person.js
<Person
key={person.id}
person={person}
/>
)}
</ul>
<h2>Filter results</h2>
<FilterResults persons={row_names()} />
</div>
)
}
export default App
The dabatbase is hardcoded json.
file db.json
"persons": [
{
"name": "ss",
"number": "ssssd",
"id": 17
},
{
"name": "ddd",
"number": "6tyhhth",
"id": 18
},
{
"name": "almejas",
"number": "1234",
"id": 19
},
{
"name": "pailo",
"number": "244",
"id": 20
}
]
}
OK. Once again. To handle API requests you have to build API server first. When you send ajax reqests they must go somewhere, and there must be a program listening requests and doing something what depends on request params and body. The repository you show contains only frontend logic and it is OK because server side logic and front-end can be kept separately and run indepentendly. In the same git account you may find some other reps like this one https://github.com/inci-august/fullstackopen/tree/d6680a40d03536e20ee9537cc64e1cb57dd6b74a/part3/phonebook-backend containing back-end implementation. So you build API server, accepting requests, doing something (create/delete posts, users, auth etc) and sending something back, and AFTER you find it working you can send API requests from frontend. Before front part is created you may use apps like Postman to test your API server.
UPDATE:
You mentioned the following link https://fullstackopen.com/en/part2/altering_data_in_server containing the same I have already said - the server side logic does not suppose to be implemented on this step.
In the next part of the course we will learn to implement our own
logic in the backend. We will then take a closer look at tools like
Postman that helps us to debug our server applications
As for you question - the "props" in requests to server can be sent by params in address string like:
axios.delete("api/person/2")
In the example above we say to server that we want the person with id=2 to be deleted. On the server side it will be catched with instruction like:
router.delete("api/person/:id", (req, res) => {
const id = req.params.id
// here you delete the person information and send response back
})
ID here will be handled by server as parameter and you will have an access to its value for further actions.
The exercise expected me to remove objects from the json database just using axios. Perhaps I did not express myself clarly enough, but I finally solved it.
I just wanted to pass the id to delete of each object to the axios request.
On the return sttement of App.js, I pass the person object as props to deleteContactOf
<ul>
{contacts.map(person =>
//Pass all the props from person to Person.js
//Here you pass the props
<Person
key={person.id}
person={person}
deleteContact={() => deleteContctOf(person)}
/>
)}
</ul>
This is how the function deleteContactOf looks like
//Delete contacts
const deleteContctOf = (person) => {
console.log(person)
console.log('delete contact ' + person.id + ' ????')
if (window.confirm("Do you really want to delete this person")) {
contactService
.remove(person.id)
} else {
return
}
}
deleteContactOf passes the id (person.id) to the axios request on file perons.js
Now the id is passed. That's were I was failing.
const remove = (id) => {
const req = axios.delete(`${baseUrl}/${id}`)
return req.then((res) => res.data)
}
Now by clicking the delete button, the contacts that belong to that id are deleted
<button onClick={deleteContact}>Delete</button>
Thanks for your time effort. Of course this is implemented with APIs in real life. This was just an specific exercise I had to solve.
Thanks

How do I link two items from a mongo database?

I'm building a simple MERN stack app, where users submit reviews about cafes.
I'd like functionality whereby a user can click on the name of a cafe, resulting in a redirect to a view with all the reviews about that particular cafe - any suggestions of how I can accomplish this?
Mongo Atlas database structure
Database
|
+--cafes (collection)
+-- _id:5ffb7a6bf32d1b27ac8474d9
cafeName:"Customs Coffee
photoURL:"https://*******
+--reviews (collection)
+--_id:5ffb95b75624dd13d825ea5e
userName:"Josh"
stars:"4"
title:"Second review of customs coffee"
photo:"photoURL.com"
blurb: "this is the blurb for the second review of customs"
cafe:"Customs Coffee"
createdAt:2021-01-11T00:03:03.842+00:00
Components rendering list of cafes
CafeList.jsx
import React, {useState, useEffect} from 'react'
import axios from 'axios'
import Cafe from './Cafe'
const CafeList = () => {
const [cafes, setCafe] = useState([])
useEffect(() => {
axios.get('/api/all-cafes')
.then(cafe => {
setCafe(cafe.data)
})
.catch(err => {
console.log(err)
})
},[])
return(
<div className = 'cafe-container-container'>
<h2>Cafes</h2>
<Cafe cafes = {cafes}/>
</div>
)
}
export default CafeList
Cafe.jsx
import React from 'react'
import {Link} from 'react-router-dom'
const Cafe = (props) => {
const {cafes} = props
return(
<div>
{
cafes.map(cafe =>{
const {cafeName,photoURL} = cafe
return (
<Link to = '/cafe-reviews/' style={{ textDecoration: 'none' }} >
<div className = 'cafe-container'>
<h2>{cafeName}</h2>
<img src = {photoURL}></img>
</div>
</Link>
)
})
}
</div>
)
}
export default Cafe
..and here's the empty component where I eventually want to render reviews specific to a particular cafe:
import react from 'react'
const CafeReviews = () => {
return(
<div>
This is the cafe review list
</div>
)
}
export default CafeReviews
As it stands, when you click on any cafe, it redirects to the CafeReviews component - but as I mentioned, I'd like to instead render the reviews specific to the cafe being clicked. The common element between the two collections is the cafe name (cafeName and ```cafe``, respectively), so I'm thinking I'm going to have to do some sort of join using this property.
Lastly, here's the express routes I've written:
server.js
app.get('/api/all-reviews', (req,res) => {
Review.find()
.then((result) => {
res.send(result)
})
.catch(err => {
console.log(err)
})
})
app.get('/api/all-cafes', (req,res) => {
Cafe.find()
.then((result) => {
res.send(result)
})
.catch(err => {
console.log(err)
})
})
app.post('/api/add-review',(req,res) => {
const review = new Review(req.body)
review.save()
.then(() => {
console.log('review successfully posted')
res.status(200)
})
.catch(err => {
console.log(err)
})
})
I, personally, would add a Cafe reference to your Review collection instead of just having common field such as cafe name. If you are using mongoose to define your mongo schema, this would be an example of your cafe reference field in your Reviews collection
_id: mongoose.Schema.Types.ObjectId,
userName: {Type: String, required: true},
cafeReference: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Cafe',
required: true,
},
... etc (your other collection fields)
Then, create an API endpoint on your back-end that gets cafe reviews by cafe Id. And then when rendering CafeReviews component you will be sending GET request with that cafe Id in your request payload. Then, finally, Mongo "find" method takes in parameters by which it can look for requested documents. Linking docs - https://docs.mongodb.com/manual/reference/method/db.collection.find/
In your case, example would be as follows:
Review.find({cafeReference: cafeId})
Also mongoose has great method to combine fields from 2 collections (sql join equivalent), called "populate" - https://mongoosejs.com/docs/populate.html.
Also fire off your "get reviews by cafe id" request inside the componentDidMount react lifecycle method. Here is how to implement it in hooks - https://reactjs.org/docs/hooks-effect.html
Hope this will help,

cannot access value passed from context api in firestore fetch data function

so I am trying to fetch some data from my firestore through the following bit of code
import React, { useContext, useState, useEffect } from 'react';
import { CocktailContext } from '../../context/CocktailContext';
import fire, { db } from '../../Config/firebase';
import RecipeCard from '../RecipeCard/RecipeCard';
import NavBar from '../NavBar/NavBar';
export default function SavedItemPage() {
const [ content, setContent ] = useState(null);
const { uid } = useContext(CocktailContext);
useEffect(() => {
listenForMessages(uid)
}, []);
const listenForMessages = (id) => {
db.collection('users').doc(id).onSnapshot(function(doc) {
const allMessages = [];
allMessages.push(doc.data());
setContent(allMessages);
console.log(content);
});
};
return (
<div>
<NavBar />
</div>
);
}
what should basically happen here is that the context API which looks like below, takes the uid and feeds it into listenForMessages() which then does the job of fetching the content
{
"name": "State",
"value": null,
"subHooks": []
},
{
"name": "Context",
"value": {
"recipes": "[{…}, {…}, {…}, {…}, {…}, {…}]",
"searchTerm": "Æ’ searchTerm() {}",
"updateUid": "Æ’ updateUid() {}",
"uid": "MfLXrE5czaYK7fYiTFuqFv9SZV02"
},
"subHooks": []
},
{
"name": "Effect",
"value": "Æ’ () {}",
"subHooks": []
}
]
trouble is that when the page loads i am met with this errors- FirebaseError: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: ""
so the trouble is that listenForMessages() is not able to access the uid value in the context API.
Can anyone tell me how to solve this issue?
I don't know how Firebase works, but the useContext maybe is incorrect ?
usually you use it this way :
[state, dispatch] = useContext(CocktailContext)
Which will give you the whole state. In the state you provided there is 3 objects in your state so i guess you will access the uid by doing something like:
useEffect(() => listenForMessages(state[1].value.uid));
I see there is a ' ] ' closing your provided state but nothing at the beginning, just adapt in case i miss a node level.

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