Persist data between two pages with Next.js - javascript

I would like to refactor my Next.js webapp to have different pages handle different screens. Currently, I have this component holding several states to know in which screen I'm in. In the jsx section, I'm using {value && ... } to render the right component.
But I feel this is not good design, and won't be maintainable when adding more and more screens.
I would also like to avoid Redux as it is overkill for my project.
I was thinking about persisting data in cookies so I can retrieve them with getInitialProps in every component when rendering a new page, but is there a more elegant way?
I've read about tweaking the _app.js but I'm not sure to understand the consequences of doing so, and how it could help me..
Any suggestion?

When multiple of your pages need to make use of same data, you can make use of Context to store the result. It a good way to make a centralized storage without using complex and more self sufficient libraries like redux
You can implement context inside of _app.js file which must reside inside your root folder. This way next.js treats it as a root wrapper and you would just need to use 1 instance of Context
contexts/appContext
import React from 'react';
const AppContext = React.createContext();
export const AppProvider = AppContext.Provider;
export const AppConsumer = AppContext.Consumer;
export default AppContext;
_app.js
import React from 'react'
import App from 'next/app'
import AppProvider from '../contexts/appContext';
class MyApp extends App {
state={
data:[]
}
render() {
const { Component, pageProps } = this.props;
// You can implement logic in this component to fetch data and update state
return (
<div>
<AppProvider value={this.state.data}> // pass on value to context
<Component {...pageProps} />
</AppProvider>
</div>
)
}
}
export default MyApp
Now further each component can make use of context value by using AppConsumer or using useContext if you use hooks
Please read more about how to use Context here

Related

Retrieve Route Parameters in any Component

Suppose my URL looks something like this:
/blog/[post_id]/something
What is the recommended way to pass $post_id down to any component anywhere in the tree?
I know how to retrieve route parameters using getInitialProps but passing the values down is always giving me a hard time.
For pages I could technically use React Contexts although this seems a bit oversized for such a trivial use case.
For layouts I am honestly completely lost because pages are children of layouts and the return value of getInitialProps is passed to the page and not the layout.
My components could make use of useRouter but this requires useEffect and would also make my component depend on the route itself...
Any advice would be welcome (:
My components could make use of useRouter but this requires useEffect and would also make my component depend on the route itself...
useRouter seems like the obvious solution here. I'm not exactly understanding your concerns regarding the component depending on the route. I guess it does make the Layout less flexible since it needs to know that the post id is stored in the post_id query variable. But I would do it anyways :) It gives you a nice and simple way to access the query variables which can be used in a Layout that's outside of your BlogPost or in a deeply-nested component that you use inside the BlogPost.
Using the per-page layouts approach:
/components/Layout
import { useRouter } from "next/router";
import { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
const router = useRouter();
return (
<div>
<h3>You are viewing post id #{router.query.post_id}</h3>
{children}
</div>
);
}
/pages/blog/[post_id].jsx
import Layout from '../../components/Layout';
export default function BlogPost() {
return <div>Hello World</div>
}
BlogPost.getLayout = function getLayout(page) {
return (
<Layout>
{page}
</Layout>
)
}
/pages/_app.tsx (to support per-page layouts, copied from docs)
export default function MyApp({ Component, pageProps }) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}
I think the easiest and the cleanest way is to use window.location.pathname. this will give you the part after the domain name. for example for
http://localhost:3001/blog/[post_id]/something
you will get /blog/[post_id]/something
const pathname=window.location.pathname
const splittedPathname=pathname.split("/") // ['', 'blog', '[post_id]', 'something']
const dynamicId=splittedPathname[2]
you can run above code in useEffect and set a state. or you could write a hook and use it in the components that under dynamicId components
import React, { useState, useEffect } from "react";
const usePathname = () => {
const [postId, setPostId] = useState("");
useEffect(() => {
const pathname = window.location.pathname;
const splittedPathname = pathname.split("/");
const dynamicId = splittedPathname[2];
setPostId(dynamicId);
}, []);
return { postId };
};
export default usePathname;
If you are looking for client side rendering, useRouter is the best way to go. If you are looking for SSR or SSG, you should rather use getStaticProps or getServerSideProps.

Designing persistent layouts in Next.js

I'm going through this article and I'm trying to figure out how the persistence is supposed to occur in Option 4. From what I can tell, you'd need to redefine the .getLayout for every page. I'm not sure how the logic for nesting is incorporated into further urls.
Here's the code from the article
// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () => <div>{/* ... */}</div>
AccountSettingsBasicInformation.getLayout = page => (
<SiteLayout>
<AccountSettingsLayout>{page}</AccountSettingsLayout>
</SiteLayout>
)
export default AccountSettingsBasicInformation
// /pages/_app.js
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps, router } = this.props
const getLayout = Component.getLayout || (page => page)
return getLayout(<Component {...pageProps}></Component>)
}
}
export default MyApp
For example, say AccountSettingsBasicInformation.getLayout is /settings/, how would I use this template to produce something at /settings/username
P.S. If someone has done something in the past they'd recommend over this, I'm open to ideas.
Yes, you have to redefine the getLayout function to every page. As long as the SiteLayout component stays “unchanged” (eg.no props change) the rendered content in that layout component (not the page content itself) stays persistent. This is because React wont rerender that component.
I used Adam’s article when I was building next.js lib for handlin modal routes. You can check the example folder where you can see I am defining the getLayout property on every page which should be rendered with layout.
Example: https://github.com/svobik7/next-bodies/tree/master/example

having access to a parent state from component's props.children:

I am making a container for a d3 line graph that I'm going to create, my format so far is this:
import React from "react";
import AnalyticPads from "../AnalyticPad/AnalyticPad";
import Pad from "../AnalyticPad/Pad";
import MainContent from "../AnalyticPad/MainContent";
import Extention from "../AnalyticPad/Extention";
const GraphPad = () => {
return (
<AnalyticPads properties={{height: "200px"}}>
<Pad>
<MainContent>
</MainContent>
<Extention>
</Extention>
</Pad>
</AnalyticPads>
)
}
export default GraphPad;
And my "AnalyticsPad" looks like this:
import React from "react";
const AnalyticPads = (props) => {
return (
<div className="analytic-pad-container">
{props.children}
</div>
)
}
export default AnalyticPads;
What I want is that there will be a grid of "Pads" and I want this "AnalyticsPad" to provide default styles for each pad, for example if I want each pad to have a height of 200px I set it in this wrapper and then for any individual pad that I want to differ from the default I can overide it.
The "MainContent" component is where the line graph will be and any extra information will be put inside the "Extention" which will render if a button is pressed.
Throughout my react app I keep using the context api to provide the data to the children components, but I know ( or think ) it is bad practice to do this, from what I understand context should only be used for providing data to global components.
What is best practice?
please don't answer with a solution for class components as I have never used them before as I am new to react.

React Context API not working from custom NPM component library

I've built a ReactJS component library that I use for multiple projects installed via an NPM package using a sim link. I want to use the context API to pass data from a parent component served from the component library to my base project to be consumed by multiple consumer components also served from the component library. When I try the context is always undefined in my child components.
If I place my consumer component in my provider component within my library it works like a champ but this defeats what I'm trying to achieve. If I export both the provider and the consumer to my base project the consumer doesn't see the provider.
This is from my base project
import { Screen, COD, GenericSocketServer } from 'component-library'
export default class View extends React.PureComponent {
render() {
return (
<Screen className="screen odmb1">
<GenericSocketServer>
<COD />
</GenericSocketServer>
</Screen>
)
}
}
This is my provider code exported from my 'component-library'
import React from 'react';
import MyContext from "./context";
import COD from './../cod';
export default class GenericSocketServer extends React.Component {
render() {
return (
<MyContext.Provider value={{ foo: 'bar' }}>
<COD />
{this.props.children}
</MyContext.Provider>
);
}
}
This is my content code used in 'component-library'
import React from 'react'
const MyContext = React.createContext()
export default MyContext
This is my consumer component exported from 'component-library'
import MyContext from "../GenericSocketServer/context"
class COD extends React.Component {
render() {
return (
<React.Fragment>
<MyContext.Consumer>
{(context) => {
/*
context comes back undefined
I expect { foo: 'bar' }
*/
console.log('context :', context)
return (
<p>This should work</p>
)}}
</MyContext.Consumer>
</React.Fragment>
)
}
}
Context always comes back undefined as if it doesn't see the parent provider. I think I'm ether doing something wrong initializing the context myself or for some reason the two components I'm importing just don't share the same context. Please help!! Not sure if I should give up on this and just use redux.
Maybe you are making multiple instances of the component providing the context. Let's say you have a component Sound, which starts by:
const { Provider, Consumer } = React.createContext();
If you import this library from your main project, the context will be created at the global space. You then use it to render your document tree. But in another component you also imported this library, which had to be resolved during webpack transpilation. It thus has its own copy of the above lines and a context object created in its own space. The problem occurs when you try to use the Consumer, because the Provider was only made by the main project for the first context object, and the second context's provider instance was never instantiated, thus returns undefined.
A solution to the problem is to enforce a single context object, which you can achieve by telling the second component's webpack that the provider-owning library is an external, so when webpack reaches e.g. the "import sound" line, it will not go further and will assume this dependency is resolved at runtime. When runtime comes, it will take it from the same place where the main project is taking it. To do this in webpack, e.g. for above "sound" library, add this to your other component (not main project):
{
...
externals: {
...
'sound': 'sound'
}
...
}
Also in your component package.json:
{
...
peerDependencies: {
"sound": "^1.2.3"
}
}
Apart from Darko's answer, esm and cjs export is also a possible reason for context to fail in a package. If you use the hook in esm and the provider in cjs, you will not get the value for that context.
I recently had a similar issue where I was trying to consume the value of a context inside my library components but using the provider (imported from the package) in the host app.
I managed to solve the issue just by making react and react-dom external and peerDependencies when bundling in rollup.
should your code of consumer be
<React.Fragment>
<MyContext.Consumer>
{value => /* render something based on the context value */}
</MyContext.Consumer>
</React.Fragment>
as stated from the official react doc : https://zh-hant.reactjs.org/docs/context.html
when you define
you can use it like

Why HOC are applied during exporting of component in place of importing it

My basic understading is that HOC like connect (for connecting with redux store) and other HOC's are applied to a component while exporting it.
Like this
import React, { Component } from 'react';
import './App.css';
import myHoc from './myHoc/index';
class App extends Component {
render() {
return (
<div className="App">
</div>);
}
}
export default myHoc({})(App);
Where as a better thing would be to apply HOC during import as it would make it easier to make reusable component. The same component can pick up props from store or from props and that would be the responsibility of the parent component to check what to give which HOC to apply on the component.
I know we can use container components which takes the component and render children but that just adds code in the JSX (wont look good if there are many container components)
though we can do it like this
import React, { Component } from 'react';
import './App.css';
import myHoc from './myHoc/index';
import AppChild from './AppChild';
const NewAppChild = myHoc({}, ()=> {
})(AppChild);
class App extends Component {
state = {
count: 1,
};
reRender = () => {
this.setState({count: this.state.count + 1});
};
render() {
return (
<div className="App">
<NewAppChild handleClick={this.reRender} count={this.state.count}/>
</div>
);
}
}
export default App;
What my question is that, is there something better that can handle this kind of situations where I want to apply my HOC on import that is each many container components can import it and they can apply different HOCs depending on the needs.
There is no single concrete reason for this design choice - as you have already seen you can invoke your HOC wherever you use the component - but I see at least 1 advantage: configuration & component reuse.
In your example, myHoc takes no parameters or configuration so this doesn't necessarily apply, but imagine instead that you are invoking connect from redux.
In most use cases, connect accepts 2 configuration functions -
mapStateToProps & mapDispatchToProps - that define the behaviour. If you define those within MyComponent then any consuming component can import MyComponent from 'MyComponent' and start using it.
If you instead rely on the parent component to call connect() then you are forcing every consumer to re-implement the configuration of connect as well. That may mean many instances of duplicated configuration and adds to the complexity for consuming components.
That being said, there are certainly cases where you might want this behaviour - for example, if you wanted to connect the same component to different state definitions. Ultimately you need to pick the best pattern to support what you need from the component.

Categories

Resources