I have a parent component GoalList which maps to a child component:
{data.goals.map((item, index) => {
return (
<Link
href={{ pathname: "/goal", query: { id: item.id } }}
key={`goal-item-${index}`}
>
<a>
<li>
<div>{item.title}</div>
</li>
</a>
</Link>
);
})}
next/router's page:
import SingleGoal from "../components/SingleGoal";
const Single = () => {
return <SingleGoal />;
};
export default Single;
Child Component:
const SingleGoal = () => {
const [id, setId] = useState("");
const router = useRouter();
useEffect(() => {
if (router.query.id !== "") setId(router.query.id);
}, [router]);
const { loading, error, data } = useQuery(SINGLE_GOAL_QUERY, {
variables: { id: id },
});
if (loading) return <p>Loading...</p>;
if (error) return `Error! ${error.message}`;
return (
<div>
<h1>{data.goal.title}</h1>
<p>{data.goal.endDate}</p>
</div>
);
};
When I click on Link in the parent component, the item.id is properly transferred and the SINGLE_GOAL_QUERY executes correctly.
BUT, when I refresh the SingleGoal component, the router object takes a split second to populate, and I get a GraphQL warning:
[GraphQL error]: Message: Variable "$id" of required type "ID!" was not provided., Location: [object Object], Path: undefined
On a similar project I had previously given props to next/router's page component, but this no longer seems to work:
const Single = (props) => {
return <SingleGoal id={props.query.id} />;
};
How do I account for the delay in the router object? Is this a situation in which to use getInitialProps?
Thank you for any direction.
You can set the initial state inside your component with the router query id by reordering your hooks
const SingleGoal = () => {
const router = useRouter();
const [id, setId] = useState(router.query.id);
useEffect(() => {
if (router.query.id !== "") setId(router.query.id);
}, [router]);
const { loading, error, data } = useQuery(SINGLE_GOAL_QUERY, {
variables: { id: id },
});
if (loading) return <p>Loading...</p>;
if (error) return `Error! ${error.message}`;
return (
<div>
<h1>{data.goal.title}</h1>
<p>{data.goal.endDate}</p>
</div>
);
};
In this case, the secret to props being transferred through via the page was to enable getInitialProps via a custom _app.
Before:
const MyApp = ({ Component, apollo, pageProps }) => {
return (
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
);
};
After:
const MyApp = ({ Component, apollo, pageProps }) => {
return (
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} />
</Page>
</ApolloProvider>
);
};
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {};
if (Component.getInitialProps) {
// calls page's `getInitialProps` and fills `appProps.pageProps`
pageProps = await Component.getInitialProps(ctx);
}
// exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
};
The only downfall now is that there is no more static page generation, and server-side-rendering is used on each request.
Related
How to pass data inside single product page ??
const store = configureStore({
reducer: {
cart: CartReducer,
product: ProductReducer,
},
});
productSlice
import { createSlice } from "#reduxjs/toolkit";
import { STATUS } from "./StatusSlice";
const ProductSlice = createSlice({
name: "product",
initialState: {
data: [],
status: STATUS.IDLE,
},
reducers: {
setProducts(state, action) {
state.data = action.payload;
},
setStatus(state, action) {
state.status = action.payload;
},
},
});
export const { setProducts, setStatus } = ProductSlice.actions;
export default ProductSlice.reducer;
export function fetchProducts() {
return async function fetchProduct(dispatch) {
dispatch(setStatus(STATUS.LOADING));
try {
const res = await fetch("https://fakestoreapi.com/products#");
const data = await res.json();
dispatch(setProducts(data));
dispatch(setStatus(STATUS.IDLE));
} catch (error) {
console.log(error);
dispatch(setStatus(STATUS.ERROR));
}
};
}
here is product page
const Products = () => {
const dispatch = useDispatch();
const { data: products, status } = useSelector((state) => state.product);
useEffect(() => {
dispatch(fetchProducts());
}, []);
if (status === STATUS.LOADING) {
return <h1>Loading......!!</h1>;
}
if (status === STATUS.ERROR) {
return <h1>Something went wrong</h1>;
}
return (
<>
<div className={style.wrapper}>
{products.map((product) => {
return (
<Link to={`/products/${product.id}`} key={product.id}>
// rest of the codes
</Link>
);
})}
</div>
</>
);
};
i'm not able to understand how get data in single product page
const SingleProduct = () => {
return (
// details
);
};
i had two option,
i can fetch api data in products page, single product page and wherever i want to or
i can fetch api data in one page (productSlice) and use everywhere that's why redux has been made.
Need Solution :
You are passing the product id as part of the URL path:
{products.map((product) => {
return (
<Link to={`/products/${product.id}`} key={product.id}>
// rest of the codes
</Link>
);
})}
Assuming you have a router and a route for path="/products/:productId" rendering the SingleProduct component:
<Routes>
...
<Route path="/products/:productId" element={<SingleProduct />} />
...
</Routes>
Then the SingleProduct component can use the useParams hook to access the productId route path parameter value and use this to filter the products array selected from the redux store.
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
const SingleProduct = () => {
const products = useSelector(state => state.product.data);
const { productId } = useParams();
const product = products.find(product => String(product.id) === productId);
if (!product) {
return "Sorry, no matching product found.";
}
return (
// render product details
);
};
I have a layout component that is loaded once on App.jsx and stays the same throughout the session, but since the page is SSRed it loads first, then the layout displays after a second, is there a way to get the layout data along with the page without having to add it to every page?
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const abortController = new AbortController();
const signal = abortController.signal;
const [content, setContent] = useState();
const router = useRouter();
console.log("url", router);
useEffect(() => {
const fetchUIContent = async () => {
let data;
let responseStatus;
try {
data = await axios.get("/api/layout", {
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
signal: signal,
});
responseStatus = data.status;
} catch (error) {
if (error.name === "AbortError") return;
console.log("Error message:", error.message);
} finally {
if (responseStatus == 200) {
setContent(await data.data);
} else {
console.log("Oops error", responseStatus, "occurred");
}
}
};
fetchUIContent();
return () => {
abortController.abort();
};
}, []);
return (
<Provider store={store}>
<SessionProvider session={session} /*option={{clientMaxAge: 10}}*/>
<ConfigProvider direction="ltr">
<HeadersComponent>
<Layout content={content}>
<Component {...pageProps} />
</Layout>
</HeadersComponent>
</ConfigProvider>
</SessionProvider>
</Provider>
);
}
I thought of using redux to get the layout data, but that would still need to make changes on each page and it is a pretty large project.
Fixed by https://github.com/vercel/next.js/discussions/39414
For Future readers, this is how to add extra props for use in MyApp
//to consume foo
function MyApp({ Component, pageProps, foo }) { //if you have {Component, pageProps: { session, ...pageProps }, foo} make sure foo is outside the pageProps destructure
return (
<MyFooConsumer foo={foo}>
<Component {...pageProps} />
</MyFooConsumer>
)
}
//to send foo
MyApp.getInitialProps = async (appContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext);
return { ...appProps, foo: 'bar' }
}
while investigating a performance issue I found out that when destructuring props inside a component I am actually triggering a rerender. This does not happen if I am using props.propName or if I am destructuring the props object directly into the component paramenters:
// Rerendering
const StatefulLayout = (props) => {
const {isLoading, requestFn, initializationArray} = props
useEffect(() => {
requestFn(initializationArray);
}, [initializationArray]);
if (isLoading) {
return <PageLoading />;
}
return (
<Layout>
<React.Fragment>{children}</React.Fragment>
</Layout>
);
// Not rerendering
const StatefulLayout = ({
isLoading,
requestFn,
initializationArray
}) => {
useEffect(() => {
requestFn(initializationArray);
}, [initializationArray]);
if (isLoading) {
return <PageLoading />;
}
return (
<Layout>
<React.Fragment>{children}</React.Fragment>
</Layout>
);
What this happens? What would be the correct pattern?
The useSWR hook from swr works everywhere if I explicitly enter the fetcher.
const { data } = useSWR("http://...", fetcher);
However, if I used swr global configuration as shown below, the useSWR only works in First page but not in HeaderLayout component. I did some debugging and found out that in HeaderLayout doesn't receive the value from swr global configuration (SWRConfig in _app.tsx) even though it is wrapped inside.
I followed this doc https://nextjs.org/docs/basic-features/layouts#per-page-layouts for the page layout implementation
// _app.tsx
type NextPageWithLayout = NextPage & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(resource, init).then((res) => res.json()),
}}
>
{getLayout(<Component {...pageProps} />)}
</SWRConfig>
);
}
// pages/first
const First = () => {
const [searchInput, setSearchInput] = useState("");
const router = useRouter();
const { data } = useSWR("http://...");
return (
<div>...Content...</div>
);
};
First.getLayout = HeaderLayout;
// layout/HeaderLayout
const HeaderLayout = (page: React.ReactElement) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{page}
</>
);
};
Helpful links:
https://nextjs.org/docs/basic-features/layouts#per-page-layouts
https://swr.vercel.app/docs/global-configuration
Next.js context provider wrapping App component with page specific layout component giving undefined data
Your First.getLayout property should be a function that accepts a page and returns that page wrapped by the HeaderLayout component.
First.getLayout = function getLayout(page) {
return (
<HeaderLayout>{page}</HeaderLayout>
)
}
The HeaderLayout is a React component, its first argument contains the props passed to it. You need to modify its signature slightly to match this.
const HeaderLayout = ({ children }) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{children}
</>
);
};
Layouts doesnt work if you declare Page as const. So instead of const First = () => {...} do function First() {...}
Currently I have a BooksList component and I'm passing down props to my BooksDetails component when a title is clicked. How do I use an Apollo hook to only query on props change?
I'm not sure how to do this using hooks. I've looked through the UseQuery documentation from Apollo. I couldn't find documentation on UseLazyQuery.
I've tried the following, but it always returns undefined:
const { loading, error, data } = useQuery(getBookQuery, {
options: (props) => {
return {
variables: {
id: props.bookId
}
}
}
})
BookList:
const BookList = () => {
const {loading, error, data} = useQuery(getBooksQuery)
const [selectedId, setId] = useState('');
return (
<div id='main'>
<ul id='book-list'>
{data && data.books.map(book => (
<li onClick={() => setId(book.id)} key={book.id}>{book.name}</li>
)) }
</ul>
<BookDetails bookId={selectedId} />
</div>
);
};
export default BookList;
BookDetails:
const BookDetails = (props) => {
const { loading, error, data } = useQuery(getBookQuery, {
options: (props) => {
return {
variables: {
id: props.bookId
}
}
}
})
console.log(data)
return (
<div id='book-details'>
<p>Output Book Details here</p>
</div>
);
};
export default BookDetails;
EDIT - I forgot to add that my GetBookQuery has a parameter of ID so an example would be getBookQuery(123).
Use the useLazyQuery like this instead:
const [getBook, { loading, error, data }] = useLazyQuery(getBooksQuery);
Full example:
import React from 'react';
import { useLazyQuery } from '#apollo/react-hooks';
const BookList = () => {
const [getBook, { loading, error, data }] = useLazyQuery(getBooksQuery);
return (
<div id='main'>
<ul id='book-list'>
{data && data.books.map(book => (
<li onClick={() => getBook({ variables: { id: book.id } })}} key={book.id}>{book.name}</li>
)) }
</ul>
<BookDetails data={data} />
</div>
);
};
export default BookList;
Examples and documentation can be found in Apollo's GraphQL documentation
https://www.apollographql.com/docs/react/data/queries/
I was also following along the same example and I came up here with the same question. I tried doing the following way. This might be helpful for someone.
BookDetails.js:
function BookDetails({ bookId }) {
const [loadDetails, { loading, error, data }] = useLazyQuery(getBook);
useEffect(() => {
if (bookId) {
loadDetails({ variables: { id: bookId } });
}
}, [bookId, loadDetails]);
if (!bookId) return null;
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
// for example purpose
return <div>{JSON.stringify(data)}</div>;
}
export default BookDetails;