How to actually do SEO-friendly pagination the right way - reactjs

I want to build pagination the proper way that is SEO-friendly and liked by google. From doing my research, it seems that there is a lot of incorrect content out there that, while it works, is poor for SEO. I assume that, because of my SEO-friendly requirement, I cannot use any state to manage the next/prev page links and so on, and I want the page to use URL params to determine the page number (e.g. /something?page=2). It sounds like /something/page/2 might also be acceptable, but I prefer URL params, especially as Facebook seems to use it so it must be a good way of doing so.
The problem is that my solution seems to have to use <a/> tags, which obviously just inefficiently reload the entire page when clicked on. When I try to replace the a tags with Link tags, the pagination stops working, and the component does not re-render when clicking on the prev/next links (but strangely, according to the react dev tools, re-renders all of the other components not in this demo, so something key must be wrong).
Here is my paginator component:
interface PaginatorProps {
currentPage: number;
itemCount: number;
itemsPerPage?: number;
path: string;
}
export const Paginator: FC<PaginatorProps> = ({
currentPage,
itemCount,
itemsPerPage,
path,
}) => {
const totalPages: number = Math.ceil(itemCount / itemsPerPage);
const disablePrev: boolean = currentPage <= 1;
const disableNext: boolean = currentPage >= totalPages; // is last page (or 'above')
if (totalPages <= 1 || !totalPages || !itemsPerPage || currentPage > totalPages) {
return null;
}
let next: string = path,
prev: string = path;
if (currentPage + 1 <= itemCount) {
next = `${path}?page=${currentPage + 1}`;
}
if (currentPage - 1 >= 1) {
prev = `${path}?page=${currentPage - 1}`;
}
return (
<div>
<Link to={prev} className={`${disablePrev ? 'disabled' : ''}`}>
Prev
</Link>
<span className={'paginationStyles.currentPage'}>
Page {currentPage} of {totalPages}
</span>
<Link to={next} className={`${disableNext ? 'disabled' : ''}`}>
Next
</Link>
</div>
);
};
And an example component that uses it:
export const DataList = () => {
const itemsPerPage: number = 3;
const urlParams: URLSearchParams = new URLSearchParams(window.location.search);
const currentPage: number = Number(urlParams.get('page')) || 1;
const skip: number = (currentPage - 1) * itemsPerPage;
const { dataCount, data, loading, error } = useGetDataList({ limit: itemsPerPage, skip });
if (loading) return <Loading />;
if (error) return <Error msg={error} />;
if (!data?.length) return <p>No data found...</p>;
return (
<>
<ul>
{data.map(({ email }) => (
<li key={email}>{email}</li>
))}
</ul>
<Paginator
currentPage={currentPage}
itemCount={dataCount}
itemsPerPage={itemsPerPage}
path='/user/profile/affiliate/data-list'
/>
</>
);
};
Does anyone know how to make it re-render the component and paginate properly? Or is this the wrong approach per se?

Replace pagination's buttons to a link, so maybe it will work)

Related

Applying state change to specific index of an array in React

Yo there! Back at it again with a noob question!
So I'm fetching data from an API to render a quizz app and I'm struggling with a simple(I think) function :
I have an array containing 4 answers. This array renders 4 divs (so my answers can each have an individual div). I'd like to change the color of the clicked div so I can verify if the clicked div is the good answer later on.
Problem is when I click, the whole array of answers (the 4 divs) are all changing color.
How can I achieve that?
I've done something like that to the divs I'm rendering :
const [on, setOn] = React.useState(false);
function toggle() {
setOn((prevOn) => !prevOn);
}
const styles = {
backgroundColor: on ? "#D6DBF5" : "transparent",
};
I'll provide the whole code of the component and the API link I'm using at the end of the post so if needed you can see how I render the whole thing.
Maybe it's cause the API lacks an "on" value for its objects? I've tried to assign a boolean value to each of the items but I couldn't seem to make it work.
Thanks in advance for your help!
The whole component :
import React from "react";
import { useRef } from "react";
export default function Quizz(props) {
const [on, setOn] = React.useState(false);
function toggle() {
setOn((prevOn) => !prevOn);
}
const styles = {
backgroundColor: on ? "#D6DBF5" : "transparent",
};
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
let answers = props.incorrect_answers;
const ref = useRef(false);
if (!ref.current) {
answers.push(props.correct_answer);
shuffleArray(answers);
ref.current = true;
}
const cards = answers.map((answer, key) => (
<div key={key} className="individuals" onClick={toggle} style={styles}>
{answer}
</div>
));
console.log(answers);
console.log(props.correct_answer);
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{cards}</div>
<hr />
</div>
);
}
The API link I'm using : "https://opentdb.com/api.php?amount=5&category=27&type=multiple"
Since your answers are unique in every quizz, you can use them as id, and instead of keeping a boolean value in the state, you can keep the selected answer in the state, and when you want render your JSX you can check the state is the same as current answer or not, if yes then you can change it's background like this:
function Quizz(props) {
const [activeAnswer, setActiveAnswer] = React.useState('');
function toggle(answer) {
setActiveAnswer(answer);
}
...
const cards = answers.map((answer, key) => (
<div key={key}
className="individuals"
onClick={()=> toggle(answer)}
style={{background: answer == activeAnswer ? "#D6DBF5" : "transparent" }}>
{answer}
</div>
));
...
}

How to properly update the screen based on state variables

I'm new to react and I'm learning how to fetch data from an api once the user clicks on a button. Somehow, I've gotten everything to work, but I don't think I'm using the library properly.
What I've come up with:
class App extends React.Component {
state = {
recipe: null,
ingredients: null
}
processIngredients(data) {
const prerequisites = [];
const randomMeal = data.meals[0];
for(var i = 1; i <= 20; i++){
if(randomMeal['strIngredient' + i]){
prerequisites.push({
name: randomMeal['strIngredient' + i],
amount: randomMeal['strMeasure' + i]
})
}
}
this.setState({
recipe: data,
ingredients: prerequisites,
})
console.log(prerequisites[0].name)
}
getRecipes = () => {
axios.get("https://www.themealdb.com/api/json/v1/1/random.php").then(
(response) => {
this.processIngredients(response.data);
}
)
}
render() {
return (
<div className="App">
<h1>Feeling hungry?</h1>
<h2>Get a meal by clicking below</h2>
<button className="button" onClick={this.getRecipes}>Click me!</button>
{this.state.recipe ? <Recipe food={this.state.recipe}
materials={this.state.ingredients} /> : <div/>}
</div>
);
}
}
The way I'm checking the value of state.recipe in render() and invoking the Recipe component, is it correct? Or does it seem like hacked together code? If I'm doing it wrong, what is the proper way of doing it?
It's really a minor nit, but in this case you can use an inline && logical operator since there's nothing to render for the "false" case:
{this.state.recipe && <Recipe food={this.state.recipe} materials={this.state.ingredients} />}
Checkout https://reactjs.org/docs/conditional-rendering.html for more info.

There's not a page yet at /second%20post

I have an issue regarding my gatsby site. I am fetching content from contentful and according to the code in my gatsby-node.js it has to generate two pages and it does but only one of them is working when I click on it for the second one it show that
There's not a page yet at /second%20post
I am so confused cause most of the issues asked here telling that they are not able to generate the pages and I don't know if the page is created or not and if it is created then why it shows me the error message and also when got to the error page the
second post
link is given but it is non clickable. all the other code is in my git repository here at Github code
Pleas refer to image for clear understanding at
Image here
here is my gatsby-node.js code file
const path = require(`path`)
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;
// Define a template for blog post
const blogPost = path.resolve(`./src/templates/blog-post-contentful.js`)
// Get all markdown blog posts sorted by date
const result = await graphql(
`
{
allContentfulBlockchainlearning{
edges{
node{
slug
title
subtitle
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your blog posts`,
result.errors
)
return
}
const posts = result.data.allContentfulBlockchainlearning.edges
// Create blog posts pages
// But only if there's at least one markdown file found at "content/blog" (defined in gatsby-
config.js)
// `context` is available in the template as a prop and as a variable in GraphQL
if (posts.length > 0) {
posts.forEach((post, index) => {
const previousPostSlug = index === 0 ? null : posts[index - 1].id
const $nextPostSlug = index === posts.length - 1 ? null : posts[index + 1].id
createPage({
path: post.node.slug,
component: blogPost,
context: {
slug: post.node.slug,
previousPostSlug,
$nextPostSlug,
},
})
})
}
}
and here is my blog-post template I want to creat
import React from "react"
import { Link, graphql } from "gatsby"
import Bio from "../components/bio"
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPostTemplate = ({ data, location }) => {
const post = data.contentfulBlockchainlearning
const siteTitle = data.site.siteMetadata?.title || `Title`
const { previous, next } = data
return (
<Layout location={location} title={siteTitle}>
<SEO
title={post.title}
description={post.subtitle}
/>
<article
className="blog-post"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h1 itemProp="headline">{post.title}</h1>
<p>{post.date}</p>
</header>
<section
dangerouslySetInnerHTML={{ __html: post.content.raw }}
itemProp="articleBody"
/>
<hr />
<footer>
<Bio />
</footer>
</article>
<nav className="blog-post-nav">
<ul
style={{
display: `flex`,
flexWrap: `wrap`,
justifyContent: `space-between`,
listStyle: `none`,
padding: 0,
}}
>
<li>
{previous && (
<Link to={previous.slug} rel="prev">Hey There
← {previous.title}
</Link>
)}
</li>
<li>
{next && (
<Link to={next.slug} rel="next">
{next.title} →
</Link>
)}
</li>
</ul>
</nav>
</Layout>
)
}
export default BlogPostTemplate
export const pageQuery = graphql`
query BlogPostBySlug(
$slug: String!
$previousPostSlug: String
$nextPostSlug: String
) {
site {
siteMetadata {
title
}
}
contentfulBlockchainlearning(slug: {eq: $slug}){
title
subtitle
content{
raw
}
}
previous: contentfulBlockchainlearning(slug: { eq: $previousPostSlug}) {
title
}
next: contentfulBlockchainlearning(slug: { eq: $nextPostSlug }) {
title
}
}
`
The issue is simple, you can't create a URL with whitespace like the one you are trying to create. second page should be parsed as second-page since the whitespace between second and page potentially will cause a lot of issues.
Gatsby is creating properly the pages since they appear on the 404 page (under gatsby develop, the 404 page lists all you created pages). However, it doesn't have a valid route because your slugs must be slugified. Ideally, the slug should be fetched with the correct format from the CMS already, however, you can add some controls to avoid this behaviour:
if (posts) {
posts.forEach((post, index) => {
let slugifiedPath= post.node.slug.toLowerCase().replace(/\s/g, '-');
const previousPostSlug = index === 0 ? null : posts[index - 1].id
const $nextPostSlug = index === posts.length - 1 ? null : posts[index + 1].id
createPage({
path: slugifiedPath,
component: blogPost,
context: {
slug: post.node.slug,
previousPostSlug,
$nextPostSlug,
},
})
})
}
It's quite self-explanatory but, since your paths are being fetched with a wrong format, you need to refactor them by:
let slugifiedPath= post.node.slug.toLowerCase().replace(/\s/g, '-');
It transforms it to lower case and it replaces all-white spaces globally (/\s/g) using a regular expression for hyphens (-), creating a valid slug.

onClick to go to next page not working, what's missing in my function?

Can someone help me with this, please? I can't get my head around why my showNextPage function doesn't take me to the next page. When showNextPage is clicked, it should update the state and return a new array of items, and display the next project in the array. I am getting a new array, but the page is not displaying the next project in the line. I know there is something missing with my function, but I couldn't get past the mental block. Many thanks for the help in advance.
import React, {useState} from 'react'
import {BrowserRouter as Router, Route, useParams} from "react-router-dom";
import SingleProject from '../atom/SingleProject'
import ProjectNav from '../atom/ProjectNav'
function PortfolioPage(props) {
const {projectDb} = props
const [projects, setProject] = useState(projectDb)
console.log(projects)
function showNextPage (item){
const idx = projects.findIndex(i => i.id === item.id)
if (idx === -1) {
return
}
const newItems = [...projects]
newItems[idx] = {
...item,
id: item.id,
title: item.title
}
setProject(newItems)
}
function showPrevPage (){
console.log('go to prev page')
}
return(
<Router>
{projects.map((project) => {
return <Route path={`/projects/${project.id}`} render={(i) => (
<SingleProject
project={project}
key={i}
showNextPage={() => showNextPage(project)}
showPrevPage={() => showPrevPage(project)}
/>
)}/>
})}
</div>
</Router>
)
}
export default PortfolioPage;
Sometimes it helps me to breakdown the logic of a function to see where I might be missing something. Here's how I break yours down:
function showNextPage (item){
// Go through all of your projects to find the index of the CURRENT project
const idx = projects.findIndex(i => i.id === item.id)
// If I don't find it in the projects, EJECT!!
if (idx === -1) {
return
}
// Spread my projects in to a new variables so I can manipulate it without touching the original
const newItems = [...projects]
// Update the new array variable index (where index is equal to the current project) to equal everything it already has but also overwrite/create the current project id and title keys.
newItems[idx] = {
...item,
id: item.id,
title: item.title
}
// Set the project state to the new value (which seems to also be the same as the old)
setProject(newItems)
}
I'm not sure what the purpose of setting these values is, unless they might be changing on the page and you're saving them? Otherwise, your next page function could probably be simplified to something like:
const showNextPage = (item) => {
const idx = projects.findIndex(i => i.id === item.id)
if (idx === -1) {
return
}
setProject(projects[idx === projects.length? idx : idx + 1])
};
Edit: The reason I added the ternary in the setState function is to make sure you're not trying to call an index value that doesn't exist. In this case, if you're at the end (and didn't have a check somewhere else for it already) then it would just stay on the same page.
import {Switch} from 'react-router-dom'
<Router>
<Switch>
{projects.map((project) => {
return <Route exact path={`/projects/${project.id}`} render={(i) => (
<SingleProject
project={project}
key={i}
showNextPage={() => showNextPage(project)}
showPrevPage={() => showPrevPage(project)}
/>
)} />
})}
</Switch>
</Router >
i think mistake is in routings,make this changes in routes and check

React Subcomponent (Typescript) doesn't preserve state until rendered for a third time

I have an external component with a render method like this:
render()
: JSX.Element
{
return (
<React.Fragment>
<QueryGetCustomersPaginated
query={Q_GET_CUSTOMERS}
variables={{ limit: this.props.limit, offset: this.state.offset }}
pollInterval={1000}
onCompleted={()=>console.log('Data Loading Completed!')}
>
{({ loading, error, data, startPolling, stopPolling }) =>
{
if (loading)
{
return "Loading..."
}
if (error)
{
return `Error: ${error.message}`
}
if (data)
{
// Pass Data to Private Properties
this.customers = data.getCustomers.customers
this.totalRecords = data.getCustomers.metadata.totalRecords
// Build UI
return (
<React.Fragment>
{/* PAGE TITLE */}
<h2 className="text-center mb-3">Lista de Clientes</h2>
{/* Customer List */}
<ul className="list-group">
{
this.customers.map(customer => (
<CustomerItem customer={(customer as Customer)} key={customer.id} />
))
}
</ul>
{/* Pagination */}
<Paginator
maxRangeSize={3}
pageSize={this.props.limit}
totalRecords={this.totalRecords}
currentPage={this.state.currentPage}
onPageChange={(newOffset: number, newPage: number) => this.setPageFor(newOffset, newPage)}
/>
</React.Fragment>
)
}
}}
</QueryGetCustomersPaginated>
</React.Fragment>
)
}
I also have an eventhandler for the subcomponent (Paginator) onPageChanged like this:
private setPageFor(offset: number, page: number)
{
this.setState({
offset : offset,
currentPage : page
})
}
and finally the subcomponent looks like this:
import React, { SyntheticEvent } from 'react'
//---------------------------------------------------------------------------------
// Component Class
//---------------------------------------------------------------------------------
export class Paginator extends
React.PureComponent<IPaginatorProps, IPaginatorState>
{
//-------------------------------------------------------------------------
constructor(props: IPaginatorProps)
{
// Calls Super
super(props)
// Initialize State
this.state = {
initialPageInRange : 1,
currentPage : 1
}
}
//-------------------------------------------------------------------------
public render(): JSX.Element
{
return (
<div className="row justify-content-center mt-3">
<nav>
<ul className="pagination">
{this.renderPageItems()}
</ul>
</nav>
</div>
)
}
//-------------------------------------------------------------------------
private renderPageItems(): JSX.Element[]
{
// Return Value
const items: JSX.Element[] = []
for (let iCounter: number = 0; iCounter < this.getMaxPossibleRange(); iCounter++)
{
items.push(
// className won't be set until third time a link is clicked..
<li key={iCounter} className={(**this.state.currentPage** === (this.state.initialPageInRange + iCounter)) ?
'page-item active' : 'page-item'}>
<a className="page-link"
href="#"
onClick={(e: SyntheticEvent) => this.goToPage(this.state.initialPageInRange + iCounter, e)}
>
{this.state.initialPageInRange + iCounter}
</a>
</li>
)
}
return items
}
//-------------------------------------------------------------------------
private goToPage(page: number, e: SyntheticEvent)
{
e.preventDefault()
const newOffset: number = this.getOffset(page)
const newPage: number = this.getCurrentPage(newOffset)
this.setState({
currentPage: newPage
})
this.props.onPageChange(newOffset, newPage)
}
//-------------------------------------------------------------------------
private getOffset(currentPage: number): number
{
return ((currentPage - 1) * this.props.pageSize)
}
//-------------------------------------------------------------------------
private getCurrentPage(offset: number): number
{
return ((Math.ceil(offset / this.props.pageSize)) + 1)
}
//-------------------------------------------------------------------------
private getTotalPages(): number
{
return (Math.ceil(this.props.totalRecords / this.props.pageSize))
}
//-------------------------------------------------------------------------
private getMaxPossibleRange(): number
{
return (this.props.maxRangeSize <= this.getTotalPages()) ?
this.props.maxRangeSize : this.getTotalPages()
}
}
//---------------------------------------------------------------------------------
// Component Interfaces
//---------------------------------------------------------------------------------
interface IPaginatorProps
{
maxRangeSize : number // 3
pageSize : number // 3
totalRecords : number // 19
currentPage : number // 1
onPageChange : (newOffset: number, newPage: number) => void
}
//---------------------------------------------------------------------------------
interface IPaginatorState
{
initialPageInRange : number
currentPage : number
}
As you can see, we have a main component that issues an Apollo Query with an offset parameter (set in state) that is updated every time setPageFor(offset) is called as it updates the state and re-renders the component.
Now, in the subcomponent I have two ways of getting the currentPage value, one is by using the props passed from the parent component (customers in this case), and the other is to use the component's state to set an initial value of 1 and update the state on every click in any page link.
The page link when clicked calls goToPage() which sets the local state and fires the event of page changed to the caller (customers component).
The current behavior of this component is not to change the <li> tag className to 'active' until the third time any link is pressed.
I have tried using shouldComponentUpdate()and extending React.PureComponent, I have even tried setting timeouts (I never wrote this...) to test if this worked but somehow the result is always the same.
Now, if I comment the line that triggers the onPageChanged() event in the goToPage() method, the subcomponent for pagination renders perfectly, but if I don't and the subcomponent is allowed to send the event to the parent, this one re-renders all the tree and it is like the subcomponent has been remounted causing its state to be deleted.
The weird part is that the third time I click on any link everything works as expected, it fails two times and works the third one.
I am totally puzzled at this point after hours of my weekend analyzing this code with the debugger.
I apologize for copying and pasting all this code but I believe it was necessary to get the picture of what I am trying to achieve here.
P.S. At this point the problem can be solved using props for passing the currentPageproperty from the customers component which is the subcomponent's (for pagination) caller.
I have been working with React for a few months now and this is the first time I get and error this weird, therefore I would really like to know why is this behavior happening and why the state is being lost the first two times the component renders. Also for the purpose of reusing the component, I believe it is better to keep these two variables in the state. Any thoughts from anyone who knows React better will be greatly appreciated, or is it the Apollo library the problem?
Thanks.

Resources