Gatsby Replace Static Query Data at Runtime - reactjs

I'm new to Gatsby & React and I'm trying to figure out how to get the best of both worlds of prerendering and dynamic data.
The query alone works great for getting the data at build time and passing it as a prop to the Menu component where each menu item is rendered. However, at run time, I would like to pull the data again from the DB and have it update the data, for example, if there was a price change, etc.
I know I could just rebuild the whole project but I would like to have this as a fallback.
How can I make the query send the data to the Menu component and then also [send the data again?] when the DB call is done.
Code that is currently not working as expected:
index.jsx
import React, { useEffect } from "react"
import Layout from "../components/layout"
import SEO from "../components/seo"
import Menu from '../components/menu'
import { graphql } from "gatsby"
import firebase from "gatsby-plugin-firebase"
const IndexPage = (props) => {
useEffect(() => {
// use this hook to make db call and re-render menu component with most up to date data
var db = firebase.firestore();
let docs = []
db.collection(`public/menu/${process.env.restaurantId}`).get().then(val => {
val.forEach(doc => {
docs.push({ node: doc.data() })
});
console.log('docs', docs)
props.data.allMenuItem.edges = docs; // i have no idea what i'm doing
})
}, [])
return (
<Layout>
<SEO title="Home" />
<Menu menuItems={props.data.allMenuItem.edges}></Menu>
</Layout>
)
}
// use this query for prerendering menu items
export const query = graphql`
query MyQuery {
allMenuItem {
edges {
node {
available
name
group
}
}
}
}
`;
export default IndexPage

You aren't supposed to modify React properties; any value that can change should be part of the state of the component. See Can I update a component's props in React.js?
However, the following code ought to do it. Create a state and give it the property value as default value. Then update it after the data loads on the client side.
const IndexPage = props => {
const [menuItems, setMenuItems] = useState(props.data.allMenuItem.edges.map(({node}) => node))
useEffect(() => {
// use this hook to make db call and re-render menu component with most up to date data
let db = firebase.firestore()
db.collection(`public/menu/${process.env.restaurantId}`)
.get()
.then(setMenuItems)
}, [])
return (
<Layout>
<SEO title="Home" />
<Menu menuItems={menuItems}></Menu>
</Layout>
)
}
Note that I've switched to using the data format you get from firestore (without node) rather than the one from Gatsby, so you'd need to modify your Menu component to not expect an extra level of nesting (with node) if you use this code.

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.

Why does react-query query not work when parameter from router.query returns undefined on refresh?

When page is refreshed query is lost, disappears from react-query-devtools.
Before Next.js, I was using a react and react-router where I would pull a parameter from the router like this:
const { id } = useParams();
It worked then. With the help of the, Next.js Routing documentation
I have replaced useParams with:
import { usePZDetailData } from "../../hooks/usePZData";
import { useRouter } from "next/router";
const PZDetail = () => {
const router = useRouter();
const { id } = router.query;
const { } = usePZDetailData(id);
return <></>;
};
export default PZDetail;
Does not work on refresh. I found a similar topic, but manually using 'refetch' from react-query in useEffects doesn't seem like a good solution. How to do it then?
Edit
Referring to the comment, I am enclosing the rest of the code, the react-query hook. Together with the one already placed above, it forms a whole.
const fetchPZDetailData = (id) => {
return axiosInstance.get(`documents/pzs/${id}`);
};
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {});
};
Edit 2
I attach PZList page code with <Link> implementation
import Link from "next/link";
import React from "react";
import TableModel from "../../components/TableModel";
import { usePZSData } from "../../hooks/usePZData";
import { createColumnHelper } from "#tanstack/react-table";
type PZProps = {
id: number;
title: string;
entry_into_storage_date: string;
};
const index = () => {
const { data: PZS, isLoading } = usePZSData();
const columnHelper = createColumnHelper<PZProps>();
const columns = [
columnHelper.accessor("title", {
cell: (info) => (
<span>
<Link
href={`/pzs/${info.row.original.id}`}
>{`Dokument ${info.row.original.id}`}</Link>
</span>
),
header: "Tytuł",
}),
columnHelper.accessor("entry_into_storage_date", {
header: "Data wprowadzenia na stan ",
}),
];
return (
<div>
{isLoading ? (
"loading "
) : (
<TableModel data={PZS?.data} columns={columns} />
)}
</div>
);
};
export default index;
What you're experiencing is due to the Next.js' Automatic Static Optimization.
If getServerSideProps or getInitialProps is present in a page, Next.js
will switch to render the page on-demand, per-request (meaning
Server-Side Rendering).
If the above is not the case, Next.js will statically optimize your
page automatically by prerendering the page to static HTML.
During prerendering, the router's query object will be empty since we
do not have query information to provide during this phase. After
hydration, Next.js will trigger an update to your application to
provide the route parameters in the query object.
Since your page doesn't have getServerSideProps or getInitialProps, Next.js statically optimizes it automatically by prerendering it to static HTML. During this process the query string is an empty object, meaning in the first render router.query.id will be undefined. The query string value is only updated after hydration, triggering another render.
In your case, you can work around this by disabling the query if id is undefined. You can do so by passing the enabled option to the useQuery call.
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {
enabled: id
});
};
This will prevent making the request to the API if id is not defined during first render, and will make the request once its value is known after hydration.

React.js Router url is resolving from browser but not from Link within App

I have a component setup in react router that has the following url
http://localhost:3000/editing/5f0d7484706b6715447829a2
The component is called from a parent component using the <Link.. syntax and when I click the button in the parent component the url above appears in the browser but my app crashes.
The error applies to the first manipulation the component does with the data - which indicates to me that the app is running before the data is available. This does not happen elsewhere in the app.
×
TypeError: Cannot read property 'reduce' of undefined
If I then refresh the url in the browser it works as intended. The component also loads fine if I just paste the above url into the browser.
The code is below
const Button = (props) => {
//main props
const { buttonFunction, buttonIconName, buttonStyle, visible } = props;
return (
<Link
to='./editing/'+props.documentId}
</Link>
);
};
export default Button;
The called component is below
mport React, { useContext, useEffect } from 'react';
import DocumentEditor from '../documentEditor/documentEditor';
import DocumentContext from '../../context/Document/DocumentContext';
import Spinner from '../layout/Spinner';
//For testing this Id works
// 5f0d7484706b6715447829a2
//Wrappers
const Editing = ({ match }) => {
//styling
const documentContext = useContext(DocumentContext);
const { Documents, myFiltered, getDocuments, loading, getDocument } = documentContext;
useEffect(() => {
getDocument(match.params.documentid);
// eslint-disable-next-line
}, []);
return (
<div>
{Documents !== null && !loading ? (
<DocumentEditor
Document={Documents}
DocumentContext={documentContext}
documentMode='edit'
/>
) : (
<Spinner />
)}
</div>
);
};
export default Editing;
My Thoughts
I suspect it may be due to the form loading prior to the data has been fetched but I don't see how as the code
{Documents !== null && !loading ? (
along with my functions that have been pulling data have been working fine for ages and use async / await should prevent this. Maybe I shouldn't be using a link in this way?
As #Yousaf says in the comments, your specific error is regarding a reduce() call, and we can't see that in the code you posted.
I do see what appears to be a typo, and that may be your issue.
At the top of your file, you're importing DocumentContext with a capital "D"
import DocumentContext from '../../context/Document/DocumentContext';
And inside your Editing component, you're destructuring documentContext with a lowercase "d":
const { Documents, myFiltered, getDocuments, loading, getDocument } = documentContext;
This would lead to { Documents, myFiltered, getDocuments, loading, getDocument } all being undefined. You're then passing Documents and documentContext to the DocumentEditor, which presumably is trying to call reduce() on one of those things that are passed, which are undefined.

Best practice to show Apollo Client Query loading outside of Query Component?

I want to show loading indicator outside of my query component.
Right now I'm using redux and when I start loading data from api, I set redux state attribute which I then use to show loader component.
How to achieve same thing with Apollo Client Query/Mutations?
Every example shows loading prop inside Query but how to show loader outside of this Query?
Lets say I have something like this:
<Header><Loader/></Header>
<Content><Query><List /></Query></Content>
But I do not want to wrap everything with Query and do something like this:
<Query>
<Header><Loader/></Header>
<Content><List ></Content>
</Query>
Is it even possible ?
Should i mutate #client cache inside Query when loading ?
Edit: I want to replace redux with apollo-link-state, so solution should not use redux.
I would suggest to use react-apollo-hooks, it make Apollo usage much more easy and fun.
Have a look at sample code
import React from 'react';
import { useQuery } from 'react-apollo-hooks';
import get from 'lodash.get';
import SUBSCRIPTIONS_QUERY from './Subscriptions.graphql';
import s from './SubscriptionTable.scss';
const SubscriptionsTable = () => {
const { data, loading, error } = useQuery(SUBSCRIPTIONS_QUERY);
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<div className={s.SubscriptionTable}>
<div className={s.SubscriptionTable__Header}>Email</div>
{get(data, 'subscriptions', []).map(subscription => (
<div key={get(subscription, 'id')} className={s.SubscriptionTable__Row}>
{get(subscription, 'email')}
</div>
))}
</div>
);
};
Firstly, there's no reason to put your loading state in Redux, but if you really want to for some reason you should set your redux state to loading: true when the component mounts, and set your redux state to loading: false when loading is no longer true in the component. Here's what I mean:
import React from 'react';
import {
useQuery,
useEffect
} from 'react-apollo-hooks';
import get from 'lodash.get';
import SUBSCRIPTIONS_QUERY from './Subscriptions.graphql';
import s from './SubscriptionTable.scss';
const SubscriptionsTable = () => {
const {
data,
loading,
error
} = useQuery(SUBSCRIPTIONS_QUERY);
useEffect(() => {
// set loading: true in redux state
// as soon as component mounts
// via action/reducer pattern
}, [])
if (loading) {
return "Loading..."
} else {
// set loading: false in redux state
// when query no longer loading
// via action/reducer pattern
}
if (error) return `Error! ${error.message}`;
return (<div>{JSON.stringify(data)}</div>);
};

How to customize data provider request call?

I'm new to react and react-admin.
I'm, using jsonServerProvider (in my App.js I have the following):
import jsonServerProvider from 'ra-data-json-server';
I'd like to create a custom bulk action. In a list, select many items and click a button to "connect" them. I tried to use UPDATE_MANY, but this calls my endpoint multiple times, so it's not suitable. Ideally I need the request to call my endpoint like so: url.to.myendpoint?ids=1,2,3 or even better pass an array of IDs in the body and use a PUT request.
Just to understand how things work and debug network calls, I tried also the GET_MANY, in the dataproviders page, the request seems to get the IDs like so: { ids: {mixed[]}, data: {Object} }
But the request is sent to the server like so: url.to.myendpoint?id=1&id=2&id=3 which in my python/flask backend is not nice to parse.
I've spent a bunch of time reading the docs, e.g.:
https://github.com/marmelab/react-admin/blob/master/docs/Actions.md
https://react-admin.com/docs/en/actions.html
https://marmelab.com/react-admin/Actions.html
I tried different approaches and I could not achieve what I want. So again please help me to make my custom bulk button work.
My bulk button is called ConnectItemsButton and the code looks like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Button, crudUpdateMany } from 'react-admin';
import { showNotification, GET_MANY } from 'react-admin';
import dataProvider from './dataProvider';
class ConnectItemsButton extends Component {
handleClick = () => {
const { selectedIds } = this.props;
dataProvider(GET_MANY, 'items/connect', { ids: selectedIds })
.then(() => {
showNotification('Connected!');
})
.catch((e) => {
showNotification('Error.', 'warning')
});
};
render() {
return (
<Button label="Associate" onClick={this.handleClick} />
);
}
}
export default connect(undefined, { crudUpdateMany })(ConnectItemsButton);
Note that the contents of ./dataProvider (it's the same provider used in the App.js file and passed to the <Admin> in the props):
import jsonServerProvider from 'ra-data-json-server';
export default jsonServerProvider('http://127.0.0.1:5000/api');
In my list I created it, the button is displayed properly, so here I share the code snippet:
const PostBulkActionButtons = props => (
<Fragment>
<ConnectItemsButton {...props} />
</Fragment>
);
...
export const ItemsList = props => (
<List {...props} bulkActionButtons={<PostBulkActionButtons />}>
...
In my backend endpoint items/connect I simply need to get a comma separated list of IDs to parse, that's it.
A simple working solution would be awesome, or at least point me in the right direction. Thanks for your help.
The way I would do this is by using react-admin's dataActions. Your action would be something like this:
crudCreate('items/connect', { selectedIds: selectedIds }, basePath , redirectUrl)
I recommend using a custom dataProvider (e.g. if you use jsonDataProvider, in your App.js import where you see ra-data-json-server: if you use WebStorm Ctrl + click on it and copy the code e.g. to customJsonDataProvider.js and fix eventual warnings, e.g. import lines should be moved at the top) and pass it as props to your Admin component. In your customJsonDataProvider you will have a convertDataRequestToHTTP, or something similar, which manages the CRUD actions and returns the url and HTTP method that you want to use.
An example of what you want to do would be:
const convertDataRequestToHTTP = (type, resource, params) => {
let url = '';
const options = {};
switch (type) {
...
case CREATE: {
...
if (type === 'items/connect') {
const { data: { selectedIds } } = params;
url = `${apiUrl}?${selectedIds.reduce((acc, id) => `${acc};${id}`)}`
options.method = 'GET';
}
...
break;
}
...
}
return { url, options };
}
In your dataProvider.js, modify the import to use the custom provider you created, e.g.:
import jsonServerProvider from './customJsonServer';
The code in your AssociateTradesButton.js should work fine as is.
You can find the documentation for creating you own dataProvider here.
I think using Promise.all will solve the issue, see this link for reference https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/src/index.js#L140

Resources