Nextjs how to not unmount previous page when going to next page (to keep state) - node.js

we are using Nextjs in our web app.
We want to keep stack of pages where users visit to keep state of component on back navigation.
How should we do that?
I have tried https://github.com/exogen/next-modal-pages, but it calls getInitialProps of previous pages again on back.

Here's my solution with a custom _app.js
import React, { useRef, useEffect, memo } from 'react'
import { useRouter } from 'next/router'
const ROUTES_TO_RETAIN = ['/dashboard', '/top', '/recent', 'my-posts']
const App = ({ Component, pageProps }) => {
const router = useRouter()
const retainedComponents = useRef({})
const isRetainableRoute = ROUTES_TO_RETAIN.includes(router.asPath)
// Add Component to retainedComponents if we haven't got it already
if (isRetainableRoute && !retainedComponents.current[router.asPath]) {
const MemoComponent = memo(Component)
retainedComponents.current[router.asPath] = {
component: <MemoComponent {...pageProps} />,
scrollPos: 0
}
}
// Save the scroll position of current page before leaving
const handleRouteChangeStart = url => {
if (isRetainableRoute) {
retainedComponents.current[router.asPath].scrollPos = window.scrollY
}
}
// Save scroll position - requires an up-to-date router.asPath
useEffect(() => {
router.events.on('routeChangeStart', handleRouteChangeStart)
return () => {
router.events.off('routeChangeStart', handleRouteChangeStart)
}
}, [router.asPath])
// Scroll to the saved position when we load a retained component
useEffect(() => {
if (isRetainableRoute) {
window.scrollTo(0, retainedComponents.current[router.asPath].scrollPos)
}
}, [Component, pageProps])
return (
<div>
<div style={{ display: isRetainableRoute ? 'block' : 'none' }}>
{Object.entries(retainedComponents.current).map(([path, c]) => (
<div
key={path}
style={{ display: router.asPath === path ? 'block' : 'none' }}
>
{c.component}
</div>
))}
</div>
{!isRetainableRoute && <Component {...pageProps} />}
</div>
)
}
export default App
Gist - https://gist.github.com/GusRuss89/df05ea25310043fc38a5e2ba3cb0c016

You can't "save the state of the page by not un-mounting it" but you can save the state of your app in _app.js file, and the rebuild the previous page from it.
Check the redux example from next's repo.

Related

NextJS per page layout not working with typescript

I am developing new application in NextJS 12 using typescript. I have defined two pages register and home page and i want to apply different layout to this pages, i have followed official next js documentation for this, i can see the "Registration Page" text in browser but layout not applying on page output, am i missing something in code? below is my code.
register.tsx
const UserRegistration: NextPageWithLayout = () => {
return <h1>Registration Page</h1>
}
UserRegistration.getLayout = (page: ReactElement) => {
return (
<DefaultLayout>{page}</DefaultLayout>
)
}
export default UserRegistration;
_app.tsx
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}
export default MyApp
type.ts
export type NextPageWithLayout = NextPage & { getLayout: (page: ReactElement) => ReactNode };
export type AppPropsWithLayout = AppProps & { Component: NextPageWithLayout }
export type DefaultLayoutType = { children: ReactNode }
layout.tsx
const DefaultLayout = ({ children }: DefaultLayoutType) => {
return(
<div id="main">
<nav>
<li>
Home
</li>
</nav>
{children}
</div>
)
}
export default DefaultLayout;

How to display an svg image in reactjs

I am working on a web application with Nodejs and Reactjs and currently i'm retrieving data from the mongo database and displaying it with react.
Here is some code :
import React, {Component} from 'react';
import axios from '../../node_modules/axios';
import {Col,Card} from 'react-bootstrap';
import {BrowserRouter as Router,Link, useRouteMatch} from 'react-router-dom';
const Brand = props => (
<Col lg="4" className="d-inline-block">
<Link to="/admin/Marques/MarqueDetails/1">
<Card className="marque-card" style={{ width: '100%' }}>
<p>{props.brand.name}</p>
<Card.Img className="marque-card" variant="top" src={`../../public/` +
props.brand.imgUrl} />
</Card>
</Link>
</Col>
)
class cardBrand extends Component {
constructor(props) {
super(props);
this.state = {brands: []};
}
componentDidMount() {
axios.get('http://localhost:5000/brand/')
.then(response => {
this.setState({ brands: response.data })
})
.catch((error) => {
console.log(error);
})
}
brandList() {
return this.state.brands.map(currentBrand => {
return <Brand brand={currentBrand} key={currentBrand._id}/>;
})
}
render() {
return(
<Col lg="12">
{ this.brandList() }
</Col>
)
}
}
export default cardBrand;
As you can see i have the img/brand folders inside the public folder and the props.brand.imgUrl contains the path and the image name, but unfortunately it's not working on the browser, here is an image :
P.S : i already tried react-svg but nothing happened, maybe because i didn't know how to use it.
Thank you in advance.
From the screenshot, looks like your svg path is img/brand/svg-name.svg. to display the image don't include public in your source. for example, to display images in public/img/brand you use
// without specifying public directory
<img alt="test" src={'/img/brand/svg-name.svg'}/>
Change your card src to
<Card.Img className="marque-card" variant="top" src={`/${props.brand.imgUrl}`} />

Cannot read property 'allContentfulBlogPost' of undefined" after moving query from index.js to component in GatsbyJS (with Contenful and GraphQL)

Moving a query from index.js to midsection.js (a component) gives Cannot read property of undefined.
I made a website with GatsbyJS which gets it's content from Contentful. I accomplished this by following the Build a blazing fast website with GatsbyJS and Contentful tutorial: https://www.youtube.com/watch?v=wlIdop5Yv_Y
In the tutorial you learn the basics of making a query which shows your content from Contentful on the homepage.
Because I like to use Bulma and I'm pretty new to GatsbyJS (new to React as well) I decided to download the Gatsby-Bulma-Quickstart (https://www.gatsbyjs.org/starters/amandeepmittal/gatsby-bulma-quickstart) and compare it to my own website and use what I need.
I decided to use the component structure used in the Quickstart and wanted to move the query for getting my content from the index.js to the midsection.js.
I got everything working until I moved the query.
My index.js looked like this:
import React from 'react'
import { Link } from 'gatsby'
// import Layout from '../components/layout';
const BlogPost = ({node}) => {
return (
<li>
<Link to={node.slug}><h3>{node.title}</h3></Link>
<img src={node.heroImage.resize.src} />
<div>{node.description.childMarkdownRemark.excerpt}</div>
</li>
)
}
const IndexPage = ({data}) => (
<ul className='blog-post'>
{data.allContentfulBlogPost.edges.map((edge) => <BlogPost node={edge.node} />)}
</ul>
)
// const IndexPage = () => <Layout />;
export default IndexPage
export const pageQuery = graphql`
query pageQuery {
allContentfulBlogPost (filter: {
node_locale: {eq: "en-US"}
},
sort:{ fields: [publishDate], order: DESC }
) {
edges {
node {
title
slug
description {
childMarkdownRemark {
excerpt
}
}
heroImage {
resize(width: 300, height: 300) {
src
}
}
}
}
}
}
`
Note: This works, this shows my content. (But as you can see the components etc from the Quickstart are not included (yet))
This is what my index.js looks like right now:
import React from 'react'
import Layout from '../components/layout';
const IndexPage = () => <Layout />;
export default IndexPage
And this is what my midsection.js looks like right now:
import React from 'react'
import { Link } from 'gatsby'
import './style.scss'
const BlogPost = ({node}) => {
return (
<li>
<Link to={node.slug}><h3>{node.title}</h3></Link>
<img src={node.heroImage.resize.src} />
<div>{node.description.childMarkdownRemark.excerpt}</div>
</li>
)
}
const Midsection = ({data}) => (
<ul className="blog-post">
{data.allContentfulBlogPost.edges.map((edge) => <BlogPost node={edge.node} />)}
</ul>
)
export default Midsection
export const pageQuery = graphql`
query pageQuery {
allContentfulBlogPost (filter: {
node_locale: {eq: "en-US"}
},
sort:{ fields: [publishDate], order: DESC }
) {
edges {
node {
title
slug
description {
childMarkdownRemark {
excerpt
}
}
heroImage {
resize(width: 300, height: 300) {
src
}
}
}
}
}
}
`
Using this way of moving the query to a component gives this error in the browser:
TypeError: Cannot read property 'allContentfulBlogPost' of undefined
I'd expected to use the midsection.js component for columns to show available "blog posts" from Contentful. Instead this only works straight from index.js.
Is there some way the query is not working because I moved it from the root folder to the components folder? And if so, what do I need to do to get the result I want?
With an colleague helping me, we found an solution by following these steps:
Change layout.js to:
import './style.scss'
const Layout = ({ children }) => children
export default Layout
Change index.js to:
import React from 'react'
import Layout from '../components/layout';
import Helmet from '../components/helmet';
import Header from '../components/header';
import Midsection from '../components/midsection';
import Footer from '../components/footer';
const IndexPage = ({data}) => (
<Layout>
<Helmet />
<Header />
<Midsection posts={data.allContentfulBlogPost.edges}/>
<Footer />
</Layout>
)
export default IndexPage
export const pageQuery = graphql`
query pageQuery {
allContentfulBlogPost (filter: {
node_locale: {eq: "en-US"}
},
sort:{ fields: [publishDate], order: DESC }
) {
edges {
node {
title
slug
description {
childMarkdownRemark {
excerpt
}
}
heroImage {
resize(width: 300, height: 300) {
src
}
}
}
}
}
}
`
Change midsection.js to:
import React from 'react'
import Link from 'gatsby-link'
import './style.scss'
const BlogPost = ({node}) => {
return (
<li>
<Link to={node.slug}><h3>{node.title}</h3></Link>
<img src={node.heroImage.resize.src} />
<div>{node.description.childMarkdownRemark.excerpt}</div>
</li>
)
}
const Midsection = ({ posts }) => (
<ul className="blog-post">
{posts.map(post => (
<BlogPost key={post.node.slug} node={post.node} />
))}
</ul>
)
export default Midsection
So what was the problem and what solved it?
The query used in this situation is a pageQuery which means that it only works from pages found in the pages folder. If you want to use the data in a component you have to pass it through :)

React update component after loading data

So I have a component that shows categories from firestore, the component shows nothing the first time but when I click navbar button again it does show the data stored in firestore.
Here is the component file :
import * as React from "react";
import Category from "./Category";
import connect from "react-redux/es/connect/connect";
import {getCategories} from "../reducers/actions/categoryAction";
class CategoriesList extends React.Component{
constructor(props) {
super(props);
this.state = ({
categoriesList: [{}]
})
}
componentWillMount() {
this.props.getCategories();
this.setState({categoriesList: this.props.categories});
this.forceUpdate();
}
render() {
return (
<div className={'container categories'}>
<div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
<div className={'col s24 m12'}>
<p>Create New Category</p>
</div>
</div>
<div className={'row'}>
<div className={'col s24 m12'}>
{/*{() => this.renderCategories()}*/}
{this.state.categoriesList && this.state.categoriesList.map(category => {
return <Category category={category} key={category.id}/>
})}
</div>
</div>
</div>
);
}
}
const mapDisptachToProps = (dispatch) => {
return {
getCategories: () => dispatch(getCategories()),
}
};
const mapStateToProps = (state) => {
return {
categories: state.category.categories
}
};
export default connect(mapStateToProps, mapDisptachToProps)(CategoriesList)
And here is the reducer file:
import db from '../firebaseConfig'
const initState = {
categories: []
};
const categoryReducer = (state=initState, action) => {
switch (action.type) {
case 'CREATE_CATEGORY':
db.collection("Categories").add({
category: action.category.name
})
.then(function(docRef) {
db.collection("Categories").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// console.log(`${doc.id} => ${doc.data().category}`);
if(doc.id === docRef.id) {
state.categories.push({id: doc.id, name: doc.data().category});
console.log(state.categories)
}
});
});
})
.catch(function(error) {
console.error("Error adding document: ", error);
});
break;
case 'GET_CATEGORIES':
console.log('Getting data from firestore');
db.collection("Categories").get().then((querySnapshot) => {
if(state.categories.length !== querySnapshot.size) {
querySnapshot.forEach((doc) => {
state.categories.push({id: doc.id, name: doc.data().category});
});
}
});
break;
}
return state;
};
export default categoryReducer
Is there any way to update the component after fully loading the data? or a way to load all the data in the initalState?
There are few things one needs to understand. First, this.props.getCategories() performs an action that is asynchronous in nature and hence in the very next line this.setState({categoriesList: this.props.categories});, we wont get the required data.
Second, Storing props to state without any modification is un-necessary and leads to complications. So try to use the props directly without storing it. In case you are modifying the obtained props, make sure you override getDerivedStateFromProps apropiately.
Third, Try to use componentDidMount to perform such async operations than componentWillMount. Refer when to use componentWillMount instead of componentDidMount.
Fourth(important in your case), Reducer should not contain async operations. Reducer should be a synchronous operation which will return a new state. In your case, you need to fetch the data elsewhere and then dispatch within your db.collection(..).then callback. You can also use redux-thunk, if you are using too many async operations to get your redux updated.
So #Mis94 answer should work if you follow the fourth point of returning the new state in the reducer rather than mutating the redux directly in the db().then callback
First, you don't need to store the component's props in the state object. This is actually considered an anti-pattern in react. Instead of doing this, just use your props directly in your render method:
render() {
return (
<div className={'container categories'}>
<div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
<div className={'col s24 m12'}>
<p>Create New Category</p>
</div>
</div>
<div className={'row'}>
<div className={'col s24 m12'}>
{/*{() => this.renderCategories()}*/}
{this.props.categories && this.props.categories.map(category => {
return <Category category={category} key={category.id}/>
})}
</div>
</div>
</div>
);
}
Hence in your componentWillMount you only need to initiate your request:
componentWillMount() {
this.props.getCategories();
}
You can also do it in componentDidMount() lifecycle method.
Now when your request resolves and your categories update in the store (Redux) they will be passed again to your component causing it to update. This will also happen with every update in the categories stored in the store.
Also you don't have to call forceUpdate like this unless you have components implementing shouldComponentUpdate lifecycle method and you want them to ignore it and do a force update. You can Read about all these lifecycle methods (and you have to if you are using React) here.

actions/reducers are not causing a rerender as expected

I am building a web client (react,redux) & API (mongo, express, node) that will show a list of deals to a user and allow them to "favorite/like" them. I am new to react/redux, as you will be able to tell. I am using axios to make my requests and have successfully rendered a list of deals. I have a "favorite" button that successfully makes the post request, and the request just sends back the deal that was favorited.. However, the "number of likes" is not updating and does not show the increased number until I manually refresh the page.
Here is my component that successfully produces a list of deals (2)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchDeals, favoriteDeal } from '../actions';
import DealCard from './DealCard';
class DealList extends Component {
componentDidMount(){
this.props.fetchDeals();
this.favoriteDeal = this.favoriteDeal.bind(this);
}
favoriteDeal = (dealId) => {
this.props.favoriteDeal(dealId)
}
renderDeals(){
return this.props.deals.map(deal => {
return(
<DealCard
onFavorite = {this.favoriteDeal}
key={deal._id}
{...deal}
/>
)
});
}
render(){
return(
<div>
{this.renderDeals()}
</div>
);
}
}
function mapStateToProps(state){
return {
deals: state.deals,
favoriteDeal: state.favoritedDeal
}
}
export default connect(mapStateToProps, {fetchDeals, favoriteDeal})(DealList)
Below is my individual deal card:
import React, { Component } from 'react';
class DealCard extends Component {
render() {
return (
<div key={this.props._id} className="card" style={{width: "18rem", marginTop: 10}}>
<img className="card-img-top" src={this.props.dealImage} style={{maxHeight: 200}} alt="${this.props.dealHeadline}" />
<div className="card-body">
<h4>{this.props.dealHeadline}</h4>
<p className="card-text">{this.props.dealDescription}</p>
<div>
<button onClick={() => this.props.onFavorite(this.props._id)}>Favorite</button>
<span>{this.props.dealId}</span>
<i className="fa fa-heart" aria-hidden="true"></i>
<p className="card-text">#of Likes: {this.props.dealNumberOfLikes}</p>
</div>
</div>
</div>
);
}
}
export default DealCard;
Below are my action creators:
export const fetchDeals = () => async dispatch => {
const res = await axios.get('/api/deals')
dispatch({type: FETCH_DEALS, payload: res.data})
};
export const favoriteDeal = (dealId) => async dispatch => {
const res = await axios.post(`/api/deals/${dealId}/favorites`)
dispatch({type: FAVORITE_DEAL, payload: res.data})
};
and finally my reducers:
// deals reducer
import { FETCH_DEALS } from '../actions/types';
export default function (state = [], action){
switch(action.type){
case FETCH_DEALS:
return action.payload;
default:
return state;
}
};
// favorite deals Reducer
import { FAVORITE_DEAL } from '../actions/types';
export default function (state = {}, action){
switch(action.type){
case FAVORITE_DEAL:
return action.payload;
default:
return state;
}
};
To summarize: I have a list of deals, and each deal has a button that when clicked, "favorites" a deal via an HTTP post request and increases the NumberOfDealLikes by 1. When the button is clicked, the request is successfully executed and the database shows that the NumberOfDealLikes is increased by one. However, on the screen, the update is not shown until I manually rerender. As twitter works, I would like to show that the increase happens simultaneously.
Thank you all for your help!
I think the problems lies in your favorite_deal reducer. As you said, the post request sends back the updated deal. It should then replace the old one in the deals array. Your deals reducer should look like:
import { FETCH_DEALS, FAVORITE_DEAL } from '../actions/types';
export default function (state = [], action){
switch(action.type){
case FETCH_DEALS:
return action.payload;
case FAVORITE_DEAL:
return state.map((d) => d._id === action.payload._id ? action.payload : d);
default:
return state;
}
};
As the deals array is updated, your component will be re-rendered. And you do not need another reducer.
By the way, as you defined the favoriteDeal function as a class property with an arrow function, you do not need to bind it to this.

Resources