I'm trying to take the function MainMenu and getStaticProps from being in the same page (index.js) and break it up into components. Here is the index.js page below that is working good.
#index.js
import Link from 'next/link';
function MainMenu({ menuLists }) {
return (
<div>
{menuLists.map(menuItem => (
<div>
<Link href={menuItem.absolute}><a>{menuItem.title}</a></Link>
{menuItem.below && menuItem.below.map(childItem => (
<div>
<Link href={childItem.absolute}><a>{childItem.title}</a></Link>
</div>
))}
</div>
))}
</div>
)
}
export async function getStaticProps() {
const response = await fetch('http://localhost:8888/api/menu_items/main');
const menuLists = await response.json();
return {
props: {
menuLists: menuLists,
},
}
}
export default MainMenu
I have created fetch-mainmenu.js in a lib directory with the following code.
#fetch-mainmenu.js
export async function loadMainMenu() {
const response = await fetch('http://localhost:8888/api/menu_items/main')
const menuLists = await response.json()
return {
props: {
menuLists: menuLists,
},
}
}
I then created sidebar.js to show the menu system from the json file. The sidebar.js file is working because the hard coded menus are showing.
# sidebar.js
import Link from 'next/link'
import styles from './sidebar.module.css'
import { loadMainMenu } from '../lib/fetch-mainmenu'
export default function Sidebar( { menuLists } ) {
const menus = loadMainMenu()
return (
<nav className={styles.nav}>
<input className={styles.input} placeholder="Search..." />
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
<Link href="/contact">
<a>Contact</a>
</Link>
</nav>
)
}
Getting the following error "TypeError: Failed to fetch".
What is the best way of getting this done using components.
Solution
1. Prop Drilling
Easy. Just send down all the data from getStaticProps(). This is the safest bet at current stage but it may create some redundant props.
// I've omitted fetch().json() to ease the reading. just assume it's a proper code.
const MainMenuComponent = ({menuLists}) => {
return <div>{menuLists}</div>
}
const MainPage = ({menuLists}) => {
return <MainMenuComponent menuLists={menuLists} />
}
export async function getStaticProps() {
const req = await fetch('...');
return {
props: {
menuLists: req,
},
}
}
export default MainPage
2. React.useEffect
A React component can't have asynchronous code inside render code. It is pretty obvious in a class component but it's hard to tell in a functional component
// I've omitted fetch().json() to ease the reading. just assume it's a proper code.
// class component
class SampleComponent extends React.Component {
constructor(props) {
super(props);
this.state = { data: {} };
}
async getData() {
// ✅ this works
const data = await fetch('...');
// data has to be put in state because it's asynchronous.
this.setState({ ...this.state, data });
}
componentDidMount() {
this.getData();
}
render() {
// ❌ this can't happen here because render is synchronous
await fetch('...');
// use state to display asynchronous data.
return <h1>Hello, {JSON.stringify(this.state.data)}</h1>;
}
}
// functional component
function SampleComponent = () => {
// everything outside `useEffect, useLayoutEffect` is mostly assumed as render function.
// ❌ thus, this does not work here
await fetch('...');
const [data, setData] = useState({});
useEffect(async () => {
// everything inside here treated as componentDidMount()
// not the exact same thing though.
// ✅ this works!
setData(await fetch('...'))
}, []);
return <h1>Hello, {JSON.stringify(data)}</h1>
}
WARNING if there's getStaticProps inside your page, it means the component also has to be synchronous. If the rendered component changes its content in a very short time, in a fraction of second, then it may get rehydration error. It needs to be wrapped with dynamic() so that the Next.js can ignore the component when rendering server-side & rehydrating the component. Please refer to Next.js official document on Dynamic Import.
It does work but the code seems long.
3. TanStack Query(or React-Query) or useSWR
There are nice 3rd party libraries that help writing asynchronous data fetching code inside a react component; TanStack Query and SWR are the most well known. These libraries also implement caching and revalidation. It can help handling complex issues invoked due to asynchronous requests.
// example code from useSWR
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
4. State Management with Context
Most cases are easily dealt with the Query-SWR solution but if the app gets big enough, there could be a need to synchronize the data.
In that case, fetch the data in server code and share the data with a central state management library(a.k.a store libs). A good example is this github repo of Zustand + Next.js. A bare React.Context can be used as well.
However, this method can get very complicated later, maybe not suitable for an inexperienced team; it's basically similar to building another complex layer as big as backend. That's why the trend has moved to Query-SWR solution these days. Still, this comes handy in certain cases.
import { useStore } from "../lib/store";
const SampleComponent = () => {
const { data } = useStore();
return <div>{JSON.stringify(data)}</div>
}
const MainPage() {
return <SampleComponent />
}
export async function getStaticProps() {
// refer to the github repo for this store structure
const zustandStore = initializeStore();
// this is a POC. the actual code could be different.
// store data is updated, and can be used globally in other components in a synchronized state.
const data = await useStore.setData(await fetch('...'));
return {
props: {
initialZustandState: JSON.parse(JSON.stringify({ ...zustandStore.getState(), data })),
},
};
}
5. Server-side Component
With the emergence of React 18 server side component, Next.js is also working on Next.js Server Components.
This implementation is probably the closest implementation to code from the question. Nevertheless, the work is still in progress and highly unstable.
I've kept my eyes on this method for about a year but the implementation has been constantly changing. Until we get the stable release, this can wait.
Related
I'm trying to write a Next.js 13 newsletter page in the app directory that uses server-side components that depend on useEffect for props. The useEffect fetches data from a REST API to get newsletters which will render the content of the page. The code I'm using is below. I'm having trouble figuring out how to configure the server-side components to work when I need to "use client" for interactivity. How can I make sure that the server-side components are rendered before it is sent to the client?
Code:
import Navbar from '#/components/navbar'
import Footer from '#/components/footer'
import Pagination from './pagination'
import IssueCards from './issueCards';
import { useState, useEffect } from 'react';
import axios from 'axios';
const Newsletters = () => {
const [issues, setIssues] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [issuesPerPage, setIssuesPerPage] = useState(5);
useEffect(() => {
const fetchIssue = async () => {
const res = await axios.get(`${process.env.NEXT_PUBLIC_BACKEND_API}/newsletters`)
setIssues(res.data)
}
fetchIssue()
}, [])
// Change page
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const indexOfLastIssue = currentPage * issuesPerPage;
const indexOfFirstIssue = indexOfLastIssue - issuesPerPage;
const currentIssues = issues.slice(indexOfFirstIssue, indexOfLastIssue)
return (
<>
<Navbar />
<div className="newsletter-container" id='newsletter-container'>
<h1>Newsletters</h1>
<hr></hr>
<div className="newsletter-wrapper">
<IssueCards issues={currentIssues} />
<Pagination
issuesPerPage={issuesPerPage}
totalIssues={issues.length}
paginate={paginate}
/>
</div>
</div>
<Footer />
</>
);
}
export default Newsletters;
How do I configure Next.js 13 server-side components that depend on useEffect for props and ensure that the content is rendered before it is sent to the client?
I tried following the Nextjs docs on Server and Client components but I am unsure of how I can pass down the props information onto the server.
Unfortunately, server components don't allow for hooks such as useEffect, see documentation here.
You have two main options:
New way of fetching data
Server components allow for a new way of fetching data in a component, described here.
This approach would look something this:
async function getData() {
const res = await fetch('https://api.example.com/...');
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <main></main>;
}
Revert to client components
Your other option is to use the use client directive at the top of your file and leaving Newsletter as a client component. Of course, this way, you wouldn't get the benefits of server components, but this would prevent you from having to change your code substantially. Also, keep in mind that server components are still in beta.
We have a simple setup involving a Context API provider wrapping the _app.tsx page. The flow we're trying to implement is:
On a page we collect some data from an API by using getServerSideProps
We send the data to the page through the props
We update the context from the page (the context provider is wrapping the _app.tsx as mentioned above)
The context is updated and all the children components can access the data in it
So we have a pretty standard setup that works correctly on the client side. The problem is that the updated context values are not being used to SSR the pages.
Context API
const ContextValue = createContext();
const ContextUpdate = createContext();
export const ContextProvider = ({children}) => {
const [context, setContext] = useState({});
return <ContextValue.Provider value={context}>
<ContextUpdate.Provider value={setContext}>
{children}
</ContextValue.Provider>
</ContextUpdate.Provider>
}
export const useContextValue = () => {
const context = useContext(ContextValue);
return context;
}
export const useContextUpdate = () => {
const update = useContext(ContextUpdate);
return update;
}
Then we have on _app.jsx:
...
return <ContextProvider>
<ContextApiConsumingComponent />
<Component {...pageProps} />
</Context>
And in any page, if we can update the context by using the hook provided above. For example:
export function Index({customData, isSSR}) {
const update = useContextUpdate();
if(isSSR) {
update({customData})
}
return (
<div>...</div>
);
}
export async function getServerSideProps(context) {
...
return {
props: { customData , isSSR}
};
};
And we can consume the context in our ContextApiConsumingComponent:
export function ContextApiConsumingComponent() {
const context = useContextValue()
return <pre>{JSON.stringify(context?.customData ?? {})}</pre>
}
The (very simplified) code above works fine on the client. But during the SSR, if we inspect the HTML sent to the browser by the server, we'll notice that the <pre></pre> tags are empty even though the context values are correctly sent to the browser in the __NEXT_DATA__ script section (they are precisely filled with the data on the browser after the app is loaded).
I've put together a GitHub repo with a minimum reproduction too.
Am I missing something?
As far as I understand, React doesn't wait for asynchronous actions to be performed during the SSR. Whereas setState is an asynchronous operation.
If you want it to be rendered during SSR, you need to provide an initialValue for useState hook.
export const ContextProvider = ({children, customData}) => {
const [context, setContext] = useState(customData);
// ...
}
I'm using ketting for my React REST client.
The library provides hooks to access some data, In my case, this is this hook :
import { useResource } from 'react-ketting';
...
const { loading, error, data } = useResource('https://api.example/article/5');
I have multiples resources and I want to loop on them:
items.map(item => {
const { loading, error, data, resourceState } = useResource(item);
myItems.push({
title: data.name,
onClick: () => go(resourceState.links.get('self').href),
});
});
But React doesn't accept looping the useResource hook like this.
So I found a dirty solution that I'm not proud of...
import React from 'react';
import { useCollection, useResource } from 'react-ketting';
let myItems = [];
const Collection = ({ resource, go }) => {
const { items } = useCollection(resource);
myItems = [];
return (
<div>
{items.map(item => (
<CollectionItem go={go} resource={item} />
))}
<ElementsUser elements={myItems} />
</div>
);
};
const CollectionItem = ({ resource, go }) => {
const { data, resourceState } = useResource(resource);
myItems.push({
title: data.name,
onClick: () => go(resourceState.links.get('self').href),
});
return null;
};
Do you have any tips to deal with that problem?
And is it possible that it causes a :
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Using 1 component per Resource is exactly the pattern that's recommended. I'd even say it's a best practice. Note that a call to useResource will not result in an API call if there was a cached resource state.
If you find that you are seeing 1 request per iteration, you might want to make sure that your collection returns every member as an embedded resource. If you use the useCollection hook, the request will include a Prefer: transclude=item header to give a hint to the server that embedding might be desired.
Also, react-ketting will do cleanups on unmount.
The question is simple. How to fetch data in your React blog and stay DRY? Let's say that you have just two components in your blog - PostsList and SinglePost, in both components you must fetch data, activate isLoading state, etc. There will be chunks of the same code in both components.
I investigated the situation a little bit, checking React-blog demo apps of big headless CMS providers, like Prismic or Sanity.io, and they all just repeat fetch functions in both PostsList and SinglePost.
Does anybody have any idea? You can point me to some good resources?
You can achieve this by using High Order Components. You can use them for reusing component logic. Let me show you an example of how to handle the isLoading with a HOC:
HOC:
import React, { useState } from 'react'
const hocLoading = (WrappedComponent, loadingMessage) => {
return props => {
const [ loading, setLoading ] = useState(true)
const setLoadingState = isComponentLoading => {
setLoading(isComponentLoading)
}
return(
<>
{loading && <p>{loadingMessage}</p>} //message showed when loading
<WrappedComponent {...props} setLoading={setLoadingState} />
</>
)
}
}
export default hocLoading
As you can see this HOC is receiving the WrappedComponent and a message that you can set depending on your component. Then you will have to wrap every component where you want to show the loading feedback with the HOC and you can use the setLoading prop to stop showing the loading feedback:
const Component = props => {
const { setLoading } = props
useEffect(() => {
const loadUsers = async () => {
await fetchData() // fetching data
setLoading(false) // this function comes from the HOC to set loading false
}
loadUsers()
},[ ])
return (
<div className="App">
{usuarios.data.map(x => <p key={x.id}>{x.title}</p>)}
</div>
);
}
export default hocLoading(Component, "Data is loading") //component wrapped
// with the HOC and setting feedback message
This way you avoid repeating this process for every component. Regarding the data fetching you can create a Hook or a function that receives dynamic params so you can just call something like fetchData(url). Here is an example of a dynamic function for making request using axios:
const baseUrl = "" //your BASE URL
async function request(url,method,data){
try {
const response = await axios({
method,
url: `${baseUrl}${url}`,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
data: data ? data : undefined
})
return response
} catch (e) {
// handle error
}
}
I have one fetch and one display .js file. However I am trying to figure out how to read the state. Of course as it's done now it's returned from the other .js file. But I would like to use the state that was set instead. How would I refactor to do this?
I would like to use the stateURL prop in the DataLoader.js
DataLoader.js
import React, { useState, useEffect } from 'react';
import useFetch from "./useFetch";
export default function DataLoader({stateURL}) {
const data = useFetch("/api");
// Should not be used
console.log("data", data);
const data2 = Object.keys(data).map(data => data);
console.log("data2", data2);
const data3 = data2.map(key => {
console.log("inside data3", key );
return data[key];
});
//This is empty
console.log("state", stateURL);
return (
<div>
<h1>Testing</h1>
<ul>
{Object.keys(data3).map(key => {
return <li>{data3[key].href}</li>;
})}
</ul>
</div>
);
}
useFetch.js
import { useState, useEffect } from "react";
export default function useFetch(url) {
const [stateURL, setStateURL] = useState([]);
console.log("url", url);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setStateURL(data._links));
}, []);
console.log("stateURL", stateURL);
return stateURL;
}
That is not possible. The hooks can only be referred from the original creating component.
Why do you just use the fetch hook within the display file?
If you want to keep these two components separated:
To access the data, you have to share the data somehow to be accessible to your other components. There are several ways to do it:
Pass the data up into the parent component via a callback and pass that into the other child component.
Using a state management library like Redux or Mobx.
Using the context API to share data between components but that might not be the best way for this kind of data.
It depends on your setup and your needs. If only these two components ever need that data, pushing it into the parent works fine.
If there are other components, which maybe need that data, you might want to use a state management lib.