How to use "useRouter()" from next.js in a class component? - javascript

I was trying to get the queries from my url pattern like localhost:3000/post?loc=100 by using useRouter() from "next/router" and fetching some data using that id from my server. It worked when I used it in a Stateless Functional Component.
But the page showing "Invalid hook call" then. I tried calling getInitalProps() of a Stateless Functional Component, but it didn't work there either and showed the same error.
Is there any rule to use this method?
I was developing a front-end using React Library and Next.js Framework.
constructor(props) {
this.state = {
loc: useRouter().query.loc,
loaded: false
};
}

Hooks can be used only inside functional components, not inside classes. I would recommend to use withRouter HOC as per next.js documentation:
use the useRouter hook, or withRouter for class components.
Or see From Classes to Hooks if you want to switch to hooks.
In general, it's possible to create a wrapper functional component to pass custom hooks into class components via props (but not useful in this case):
const MyClassWithRouter = (props) => {
const router = useRouter()
return <MyClass {...props} router={router} />
}
class MyClass...
constructor(props) {
this.state = {
loc: props.router.query.loc,
loaded: false
};
}

withRouter example
https://stackoverflow.com/a/57029032/895245 mentioned it, but a newbie like me needed a bit more details. A more detailed/direct description would be:
Function component:
import { useRouter } from "next/router";
export default function Post() {
const router = useRouter();
return (
<div>{ router.query.id }</div>
)
}
Class component equivalent:
import { withRouter } from 'next/router'
import React from "react";
export default withRouter(class extends React.Component {
render() {
return (
<div>{ this.props.router.query.id }</div>
)
}
})
I tested this out more concretely as follows. First I took vercel/next-learn-starter/basics-final/pages/posts/[id].js and I hacked it to use the router:
diff --git a/basics-final/pages/posts/[id].js b/basics-final/pages/posts/[id].js
index 28faaad..52954d3 100644
--- a/basics-final/pages/posts/[id].js
+++ b/basics-final/pages/posts/[id].js
## -4,13 +4,17 ## import Head from 'next/head'
import Date from '../../components/date'
import utilStyles from '../../styles/utils.module.css'
+import { useRouter } from "next/router"
+
export default function Post({ postData }) {
+ const router = useRouter();
return (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
<article>
+ <div>router.query.id = {router.query.id}</div>
<h1 className={utilStyles.headingXl}>{postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={postData.date} />
Then, I ran it as:
git clone https://github.com/vercel/next-learn-starter
cd next-learn-starter
git checkout 5c2f8513a3dac5ba5b6c7621d8ea0dda881235ea
cd next-learn-starter
npm install
npm run dev
Now when I visit: http://localhost:3000/posts/ssg-ssr I see:
router.query.id = ssg-ssr
Then I converted it to the class equivalent:
import Layout from '../../components/layout'
import { getAllPostIds, getPostData } from '../../lib/posts'
import Head from 'next/head'
import Date from '../../components/date'
import utilStyles from '../../styles/utils.module.css'
import { withRouter } from 'next/router'
import React from "react"
export default withRouter(class extends React.Component {
render() {
return (
<Layout>
<Head>
<title>{this.props.postData.title}</title>
</Head>
<article>
<div>router.query.id = {this.props.router.query.id}</div>
<h1 className={utilStyles.headingXl}>{this.props.postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={this.props.postData.date} />
</div>
<div dangerouslySetInnerHTML={{ __html: this.props.postData.contentHtml }} />
</article>
</Layout>
)
}
})
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id)
return {
props: {
postData
}
}
}
and everything seemed to be unchanged.
Tested on Next.js 10.2.2.

Related

React nextJS function references it self and pulls data without an import

I bought this template and am trying to understand which page getLayout in this code"{getLayout(<Component {...pageProps} />)}" goes to on initial load. I'm guessing it's a global variable somewhere, but I can't find it using the definitions. I'm trying to under the next.js documentation, but I'm having issues. If anyone has a good tutorial for this I'll happily take it.
import type { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';
import { SessionProvider } from 'next-auth/react';
import '#/assets/css/main.css';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
import { ModalProvider } from '#/components/ui/modal/modal.context';
import ManagedModal from '#/components/ui/modal/managed-modal';
import ManagedDrawer from '#/components/ui/drawer/managed-drawer';
import DefaultSeo from '#/components/seo/default-seo';
import { SearchProvider } from '#/components/ui/search/search.context';
import PrivateRoute from '#/lib/private-route';
import { CartProvider } from '#/store/quick-cart/cart.context';
import SocialLogin from '#/components/auth/social-login';
import { NextPageWithLayout } from '#/types';
import QueryProvider from '#/framework/client/query-provider';
import { getDirection } from '#/lib/constants';
import { useRouter } from 'next/router';
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function CustomApp({
Component,
pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
const authenticationRequired = Component.authenticationRequired ?? false;
const { locale } = useRouter();
const dir = getDirection(locale);
return (
<div dir={dir}>
<SessionProvider session={session}>
<QueryProvider pageProps={pageProps}>
<SearchProvider>
<ModalProvider>
<CartProvider>
<>
<DefaultSeo />
{authenticationRequired ? (
<PrivateRoute>
{getLayout(<Component {...pageProps} />)}
</PrivateRoute>
) : (
getLayout(<Component {...pageProps} />)
)}
<ManagedModal />
<ManagedDrawer />
<ToastContainer autoClose={2000} theme="colored" />
<SocialLogin />
</>
</CartProvider>
</ModalProvider>
</SearchProvider>
</QueryProvider>
</SessionProvider>
</div>
);
}
export default appWithTranslation(CustomApp);
Basically, you can define a per component layout. In order to do so, when you are defining a component, you add a property named getLayout. Here's an example, for a better understanding.
// ...
const Component: React.FC = () => <div>
I have a custom layout
</div>
// Here we define a layout, let's imagine it's a component
// we have inside /layouts and we have previously imported it
Component.getLayout = (page: React.ReactElement) =>
<LayoutComponent>{page}</LayoutComponent>
Now, when a page is rendered (note that in a Next JS app, all pages are rendered as a children of what is inside _app.{js,jsx,ts,tsx}) your code checks if the prop getLayout has been defined or not. Then, it is basically calling such function, if exists, otherwise it renders the base component.

Storybook 5.2 (CSF) - How to add react-router-dom Link - "You should not use <Link> outside a <Router>"

I am getting an error in my story, because my component is using <Link to="/" />. As far as I can understand the link will have no context in a story so I need to add a decorator, however most articles that describe this are not using the Component Story Format (CSF).
Any ideas how to do this globally or straight into the story?
Error: Invariant failed: You should not use <Link> outside a <Router>
This is my stories file.
// ./stories.tsx.tsx
import React from 'react'
import { LinkComponent } from '.'
import { StoryRouter } from 'storybook-react-router'
export default {
title: 'LinkComponent'
}
export const base = () => {
return <LinkComponent/>
}
This is my component:
// ./LinkComponent.tsx
import React from 'react'
import { Link } from 'react-router-dom'
const LinkComponent = () => {
return (
<section className="links">
<Link to="/">Home</Link>
</section>
)
}
I have tried addDecorator(StoryRouter()); globally in the storybook config as many people suggest, but it breaks the code.
// /.storybook/config.ts
import StoryRouter from 'storybook-react-router';
addDecorator(StoryRouter());
configure(require.context('../src', true, /stories\.tsx$/), module)
Total face-palm moment!!
There is no reason to add a decorator. You can simply import and wrap your component in <MemoryRouter/> to simulate the context of the link.
// ./stories.tsx
import React from 'react'
import { LinkComponent } from '.'
import { MemoryRouter } from 'react-router-dom'
export default {
title: 'LinkComponent'
}
export const base = () => {
return (
<MemoryRouter>
<LinkComponent/>
</MemoryRouter>
}
And... Bob's your uncle(I dont have an uncle called Bob!?!)!

this.context returning an empty object

I'm setting up ContextApi for the first time in a production app, hoping to replace our current handling of our app configs with it. I've followed the official docs and consulted with similar issues other people are experiencing with the API, and gotten it to a point where I am able to correctly the config when I do Config.Consumer and a callback in render functions. However, I cannot get this.context to return anything other than an empty object.
Ideally, I would use this.context in lifecycle methods and to avoid callback hell, so help would be appreciated. I've double checked my React version and that I'm setting the contextType. Below is a representation of the code
config.js
import { createContext } from "react";
export default createContext();
index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { Router, browserHistory } from "react-router";
import { syncHistoryWithStore } from "react-router-redux";
import Config from "../somePath/config";
// more imports
function init() {
const config = getConfig();
const routes = getRoutes(config);
const history = syncHistoryWithStore(browserHistory, appStore);
ReactDOM.render(
<Provider store={appStore}>
<Config.Provider value={config}>
<Router history={history} routes={routes} />
</Config.Provider>
</Provider>,
document.getElementById("app")
);
}
init();
someNestedComponent.js
import React, { Component } from "react";
import { connect } from "react-redux";
import Config from "../somePath/config";
#connect(
state => ({
someState: state.someState,
})
)
class someNestedComponent extends Component {
componentDidMount() {
console.log(this.context);
}
render() {
return (...someJSX);
}
}
someNestedComponent.contextType = Config;
export default someNestedComponent;
Currently running on:
React 16.8.6 (hopi to see error messages about circuitous code but
didn't get any warnings)
React-DOM 16.7.0
React-Redux 6.0.1
The problem is that someNestedComponent doesn't refer to the class where this.context is used:
someNestedComponent.contextType = Config;
It refers to functional component that wraps original class because it was decorated with #connect decorator, it is syntactic sugar for:
const someNestedComponent = connect(...)(class someNestedComponent extends Component {
...
});
someNestedComponent.contextType = Config;
Instead, it should be:
#connect(...)
class someNestedComponent extends Component {
static contextType = Config;
componentDidMount() {
console.log(this.context);
}
...
}
There are no callback hell problems with context API; this is conveniently solved with same higher-order component pattern as used in React Redux and can also benefit from decorator syntax:
const withConfig = Comp => props => (
<Config.Consumer>{config => <Comp config={config} {...props} />}</Config.Consumer>
);
#connect(...)
#withConfig
class someNestedComponent extends Component {
componentDidMount() {
console.log(this.props.config);
}
...
}
You didn't use a consumer to get the values
ref: https://reactjs.org/docs/context.html#contextconsumer

React global component

I am coming from a vue.js background and I have just recently started looking into react.
I have a component: PageContent.jsx and I wish to use it without constantly having to import it to be able to use it inside the render function (JSX).
In vue it is possible to globalise a component using:
Vue.component(componentName, componentObject)
Is there anything similar in react?
Hmm, there isn't any kind of "global" component in React. Each component has to be imported or passed as a prop. You have a few options if you want to avoid adding an import to each file though:
1) Create a Higher Order Component that renders the PageContent and the wrapped component.
import PageContent from './PageContent';
const withPageContent = WrappedComponent => {
return class extends React.Component {
render () {
return (
<PageContent>
<WrappedComponent />
</PageContent>
)
}
}
};
export default withPageContent;
// Usage
import withPageContent from './withPageContent';
class MyComponent extends React.Component {
render () {
return (
<div>
I'm wrapped in PageContent!
</div>
)
}
}
export default withPageContent(MyComponent);
2) Pass PageContent as a prop to a component:
import PageContent from './PageContent';
export default class App extends React.Component {
render() {
return (
<React.Fragment>
<Child1 content={PageContent} />
<Child2 content={PageContent} />
</React.Fragment>
)
}
}
// Usage
export default class Child1 extends React.Component {
render () {
const PageContent = this.props.content;
return (
<PageContent>
I'm wrapped in PageContent!
</PageContent>
)
}
}
export default class Child2 extends React.Component {
render () {
const PageContent = this.props.content;
return (
<PageContent>
I'm wrapped in PageContent!
</PageContent>
)
}
}
I very much agree with Chase's answer.
Still if you need another approach you can use the context api. You can declare in the App root, or another nested components tree, a collection of components that you want to easily access.
Here is an example with the useContext hook, but hooks is not a must. The structure is the standard create-react-app structure.
The component we would like to access globally - src/deep/Header.js:
function Header() {
return (
<h1>
I am a global component
</h1>
);
}
export default Header;
The context creation - src/global-components-context.js:
import React from 'react';
const MyContext = React.createContext(null);
export default MyContext;
The grouping of the global-components - src/global-components.js:
import Header from './deep/Header';
const contextValue = {
Header,
};
export default contextValue;
The app init file - src/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import MyContext from './global-components-context';
import contextValue from './global-component';
ReactDOM.render(
<MyContext.Provider value={contextValue}>
<App />
</MyContext.Provider>,
document.getElementById('root')
);
Using the component without importing it - src/App.js:
import { useContext } from 'react';
import globalComponent from './global-components-context';
function App() {
const Context = useContext(globalComponent);
return (
<div className="App">
<Context.Header />
</div>
);
}
export default App;
I think this is the most global components you can have in react. Note that you still need to import the context wherever you would like to use a global component.
Also one more disclaimer, global components are very hard to test and often to reason about. I believe that is why there is no standard solution for it in react.
Hope I could help

What is wrong with my render method here?

I'm writing an example app over here to get my head around React but one of my simpler components is throwing an error that I can't seem to track.
Error: Element type is invalid: expected a string (for built-in
components) or a class/function (for composite components) but got:
undefined. You likely forgot to export your component from the file
it's defined in. Check the render method of ContactListItem.
Here's the code for the ContactListItem component.
import React, { Component } from 'react';
import { ListGroupItem } from 'react-bootstrap';
import { Link } from 'react-router';
class ContactListItem extends Component {
render() {
const { contact } = this.props;
return (
<ListGroupItem>
<Link to={'/contact/${contact.id'}>
<h4>{contact.name}</h4>
</Link>
</ListGroupItem>
);
}
}
export default ContactListItem;
It's a pretty simple class. Nothing sticks out to me as problematic. For completion's sake, here's the parent component.
import React, { Component } from 'react';
import { ListGroup } from 'react-bootstrap';
import ContactActions from '../actions/ContactActions';
import ContactStore from '../stores/ContactStore';
import ContactListItem from './ContactListItem';
function getContactListItem(contact) {
return (
<ContactListItem key={contact.id} contact={contact} />
);
}
class ContactsComponent extends Component {
constructor() {
super();
this.state = {
contacts: []
}
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
ContactStore.addChangeListener(this.onChange);
}
componentDidMount() {
ContactActions.receiveContacts()
}
componentWillUnmount() {
ContactStore.removeChangeListener(this.onChange);
}
onChange() {
this.setState({
contacts: ContactStore.getContacts()
});
}
render() {
let contactListItems;
if ( this.state.contacts ) {
contactListItems = this.state.contacts.map(contact => getContactListItem(contact));
}
return (
<div>
<ListGroup>
{contactListItems}
</ListGroup>
</div>
);
}
}
export default ContactsComponent;
You get the error in ContactListItem#render() because Link is undefined. As of React Router v4, <Link /> is no longer a part of the react-router, but a part of the react-router-dom package. Changing this should fix your problem:
import { Link } from 'react-router-dom';
As per documentation,
use
import { Link } from 'react-router-dom';`
unlike react-router which used to work earlier.
But react-router-dom comes with:
HashRouter
BrowserRouter
MemoryRouter
Prompt
NavLink
StaticRouter, etc.
You might benefit from tree-shaking a lot, on using
import Link from 'react-router-dom/Link';
if all the above stuffs are not needed in your app.

Categories

Resources