Next.js + SSR not updating the Context API - reactjs

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);
// ...
}

Related

How do I make Next.js 13 server-side components in the app directory that depend on useEffect for props?

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.

React High Order Components: best idiomatic way to avoid re-rendering the wrapper?

I have a little app where page components can have a layout like so:
// publicPage.js
export default function PublicPage() {...}
PublicPage.layout = NarrowLayout;
// narrowLayout.js
export default function NarrowLayout({children}) {
return <Container maxWidth="xs">{children}</Container>
}
and the rendering pipeline of the application wraps the page with its layout (similar to Next.js <= 12).
I'd like to have a Higher Order Component (HOC) that allows nesting layouts, like so:
// withAuth.js
export function withAuth(LayoutComponent) {
return (props) => <Authenticated><LayoutComponent {...props} /></Authenticated>
}
export function Authenticated({children}) {
const [userId, setUserId] = useState(null);
useEffect(() => {
// Some code to set up authentication
}, [])
return userId ? children : <LoadingSpinner />
}
// pageRequiringLogin1.js
export default function PageRequiringLogin1() {...}
PageRequiringLogin1.layout = withAuth(NarrowLayout);
// pageRequiringLogin2.js
export default function PageRequiringLogin2() {...}
PageRequiringLogin2.layout = withAuth(NarrowLayout);
Problem is, the code in the useEffect above runs every time I navigate between PageRequiringLogin1 and PageRequiringLogin2.
I understand this happens because withAuth generates a different function instance on every invocation, so React views it as a different component and unmounts the entire tree.
My solution is to cache the results of the HOC:
// withAuth.js
const withAuthCache = {};
export function withAuth(LayoutComponent) {
return withAuthCache[LayoutComponent] ??= (props) => <Authenticated><LayoutComponent {...props} /></Authenticated>
}
This works, but it feels unidiomatic. Is there a better way to do this with React?

Next.js Fetching Json to Display in Components

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.

React-testing-library with connected react router together

I am trying to test a workflow with a React app. When all fields are filled withing a workflow step, user is able to click to the "next" button. This action registers a state in a reducer and changes the URL to go to the next workflow step.
According to the RTL documentation, I wrap my component under test in a store provider and a connected router using this function:
export const renderWithRedux = (ui: JSX.Element, initialState: any = {}, route: string = "/") => {
// #ts-ignore
const root = reducer({}, { type: "##INIT" })
const state = mergeDeepRight(root, initialState)
const store = createStoreWithMiddleWare(reducer, state)
const history = createMemoryHistory({ initialEntries: [route]})
const Wrapper = ({ children }: any) => (
<Provider store={store}>
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</Provider>
)
return {
...render(ui, { wrapper: Wrapper }),
// adding `store` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
store
}
}
Unlike in the documentation, I amm using connected-react-router, not react-router-dom, but I've seen some people using connected-react-router with RTL on the web so I don't think the problem come from here.
The component under test is wrapped in a withRouter function, and I refresh the URL via the connected react router push function, dispatching via the redux connectfunction:
export default withRouter(
connect<any, any, any>(mapStateToProps, mapDispatchToProps, mergeProps)(View)
)
Everything work well in production, but the page doesn't refresh when I fire a click event on the "next" button. Here is the code of my test (to make it easier to read for you, I have filled all field and enable the "next" button):
const {
container,
queryByText,
getAllByPlaceholderText,
getByText,
debug,
getAllByText
} = renderWithRedux(<Wrapper />, getInitialState(), "/workflow/EXAC")
await waitForElement(
() => [getByText("supplierColumnHeader"), getByText("nextButton")],
{ container }
)
fireEvent.click(getByText("nextButton"))
await waitForElement(
() => [getByText("INTERNAL PARENT"), getByText("EXTERNAL PARENT")],
{ container }
)
Any clue of what is going wrong here?

Update React Context using a REST Api call in a functional component

I am trying to update the context of a React App using data resulted from an API call to a REST API in the back end. The problem is that I can't synchronize the function.
I've tried this solution suggested in this blog post https://medium.com/#__davidflanagan/react-hooks-context-state-and-effects-aa899d8c8014 but it doesn't work for my case.
Here is the code for the textContext.js
import React, {useEffect, useState} from "react";
import axios from "axios";
var text = "Test";
fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
text = json;
})
const TextContext = React.createContext(text);
export const TextProvider = TextContext.Provider;
export const TextConsumer = TextContext.Consumer;
export default TextContext
And this is the functional component where I try to access the data from the context
import TextProvider, {callTextApi} from "../../../../services/textService/textContext";
function Profile()
{
const text = useContext(TextProvider);
console.log(text);
const useStyles = makeStyles(theme => ({
margin: {
margin: theme.spacing(1)
}
}));
I can see the fetch request getting the data in the network section of the browser console but the context is not getting updated.
I've tried doing this in the textContext.js.
export async function callTextApi () {
await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
And I was trying to get the data in the Profile.js using the useEffect function as so
const [text, setText] = useState(null);
useEffect(()=> {
setText (callTextApi())
},[])
It's my first time using React.context and it is pretty confusing. What am I doing wrong or missing?
You have a lot of problems here. fetching and changing should happen inside Provider by modifying the value property. useContext receives an entire Context object not only the Provider. Check the following
//Context.js
export const context = React.createContext()
Now inside your Provider
import { context } from './Context'
const MyProvider = ({children}) =>{
const [data, setData] = useState(null)
useEffect(() =>{
fetchData().then(res => setData(res.data))
},[])
const { Provider } = context
return(
<Provider value={data}>
{children}
</Provider>
)
}
Now you have a Provider that fetches some data and pass it down inside value prop. To consume it from inside a functional component use useContext like this
import { context } from './Context'
const Component = () =>{
const data = useContext(context)
return <SomeJSX />
}
Remember that Component must be under MyProvider
UPDATE
What is { children }?
Everything that goes inside a Component declaration is mapped to props.children.
const App = () =>{
return(
<Button>
Title
</Button>
)
}
const Button = props =>{
const { children } = props
return(
<button className='fancy-button'>
{ children /* Title */}
</button>
)
}
Declaring it like ({ children }) it's just a shortcut to const { children } = props. I'm using children so that you can use your Provider like this
<MyProvider>
<RestOfMyApp />
</MyProvider>
Here children is RestOfMyApp
How do I access the value of the Provider inside the Profile.js?
Using createContext. Let's assume the value property of your Provider is {foo: 'bar'}
const Component = () =>{
const content = useContext(context)
console.log(content) //{ foo : 'bar' }
}
How can you double declare a constant as you've done in the Provider?
That was a typo, I've changed to MyProvider
To access it from inside a class based component
class Component extends React.Component{
render(){
const { Consumer } = context
return(
<Consumer>
{
context => console.log(contxt) // { foo: 'bar' }
}
</Consumer>
)
}
}
First thing that I am seeing is that you are not returning the promise within your function which will lead to setting the state to undefined.
I added the return statement below:
export async function callTextApi () {
return await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
Also your last then-chain could be cleaned up a bit and I am quite sure you can remove the await statement in an async function when returning a promise. It will automatically be awaited:
export async function callTextApi () {
return fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => json)
}
Second step would be to have a look at your useEffect hook. You want to setText after the promise from the api call has been resolved. So you have to make the callback function of useEffect asynchronous as well.
useEffect(async ()=> {
const newText = await callTextApi();
setText (newText);
},[])
Third step, would be to look at how to properly use the context api and the useContext hook. The useContext hook takes a context as a parameter but you passed the ContextProvider as the argument.
const text = useContext(TextContext);
The context and the context-provider are two different entities in the React world. Think of the context as state and functionality that you want to share across your application (like a global state), and think about the provider as a react component that manages one context and offers this context state to it's child components.
return(
<TextContext.Provider value={/* some value */}>
{children}
</TextContext.Provider>);
This is how a return statement of a provider component would look like and I think this code is currently missing in your application.

Resources