Why is this NextJS query string undefined on subsequent page reloads? - javascript

I have a NextJS application where I am building a search results page based on a query parameter being passed. I am using the next/router to pick up the query string. The first time the page loads after compilation the querystring gets picked up and the call to the API is successful and returns results. Every subsequent try, whether on a F5 refresh, reloading the page via the reload button, or accessing the page again the call to the API gives no results because the querystring is undefined.
the URL:
http://localhost:3000/search?search=blah
import { useRouter } from "next/router";
import React, { useEffect, useState } from 'react';
import axios from 'axios'
export default function SearchResults1(props) {
const { query } = useRouter();
const [searchResults, setSearchResults] = useState({})
const [ getQuery, setQuery] = useState({})
useEffect(() => {
function fetchData(){
setQuery(query.search)
console.log(getQuery)
if (getQuery) {
axios.get(`http://localhost:3001/search/${getQuery}`)
.then((response) => {
setSearchResults(response.data)
})
.catch((error) => {
console.warn(error)
})
}
}
fetchData()
},[getQuery, setQuery])
I tried adding the getQuery and setQuery in the useEffect hook so it would be triggered when there are changes, but i still get undefined whenever i load the page a second time.
I also tried removing the getQuery and setQuery and tried without using state, but I still get the same result if I call the query.search directly - the first time it works, subsequently it does not.
I did notice that if I called {query.search} in the return HTML it always renders - so I am thinking it has something to do with perhaps the API call happening and not waiting for the query to be populated.
return (
<div>{query.search}</div> <!--this always works and shows the correct value-->
)
Any thoughts or suggestions on how I can achieve consistent results and always return the query parameters so I can make a good API call? I'm fairly confident I am missing something obvious.

Instead of parsing it directly with the useRouter(), try to get the router ready state and access it once it is ready in the useEffect.
import { useRouter } from "next/router";
import React, { useEffect, useState } from 'react';
import axios from 'axios'
export default function SearchResults1(props) {
const router = useRouter();
const [searchResults, setSearchResults] = useState({})
//const query = router.query; <-- or use this. The point is to access router.isReady in useEffect
useEffect(() => {
if (!router.isReady) return; // <-- only use the query when it is ready.
function fetchData(){
axios.get(`http://localhost:3001/search/${router.query.search}`)
.then((response) => {
setSearchResults(response.data)
})
.catch((error) => {
console.warn(error)
})
}
fetchData()
},[router.isReady]) // <-- use this router.isReady
According to the Next.js, It will be an empty object during prerendering if the page doesn't have data fetching requirements.
https://nextjs.org/docs/api-reference/next/router
This way, we can make sure the router information is ready for use when you access it.

Related

Why does my useEffect() fetch data unreliably. I cannot map through the data because I am getting some undefined responses - ReactJS

I have a sidebar which sets a category Id on click and shows product information based on this Id.
In order to display each product's details, I make an api post call using axios in a useEffect() hook and setData with useState hook.
However, when I try to console.log the data, I get unreliable data back, that is, some undefined and some data.
import { useState, useEffect, useCallback, useMemo } from "react";
import axios from "axios";
import ProductCard from "../Product-cards/_medium-product-card";
const ProductDisplay = ({ subCategorySkus, categoryId, allSkus }) => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState();
const apiRequest = { product_skus: allSkus };
const productData = useProductData(apiRequest);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.post("/products", apiRequest);
setData(response.data);
setIsLoading(false);
} catch (err) {
console.log(err.message);
}
};
fetchData();
}, [categoryId]);
console.log(data);
return isLoading ? (
" data is loading ..."
) : (
<div className="product-display">
<h3>Child component Id: {categoryId}</h3>
<ProductCard categoryId={categoryId} />
</div>
);
};
export default ProductDisplay;
You can see here what I get in the console:
.
I have tried adding an if condition, but I am not sure that addresses the issue. I have tried with different dependencies in the useEffect. But I feel there is something I am not understanding.
The api is sending the information correctly, but I get several responses in the console.log and some of them come in as undefined.
This means that I can do a data.map because it crashes the component.
Any help?
UPDATE:
#moonwave99 provided the answer (see below). For the benefit of others who may browse through this question and to clarify what the solution entails, I am including an image below, showing where I changed my code. I think that most importantly, I had not initialised my useState() to an empty array, like this: useState([]).
Any further clarification of the issues here by anyone who knows more, is welcomed. As this is only a quick fix without enough context to understand the logic.

How can I assign data from Http response in const Component?

My goal is to ultimately create add a shopping cart functionality using context API. In order to do so I need to get the products from my database and store it in an array.
Currently, the challenge I'm facing is how to retrieve the data from the axios response and store it in a variable that will be passed on within a const component. Apparently, the issue is that the variable gets passed to the child before the Axios response is complete.
I tried using the await keyword, but got an error regarding not being in an async function. Hence, I tried plugging the async keyword but that didn't work as it yielded errors.
I was able to retrieve data from axios within class components with success, however, I am unable to do so in these const.
Here is my code:
Context.js
import { createContext, useContext, useReducer } from "react";
import React from "react";
import ProductService from "../services/ProductService";
import { cartReducer } from "./Reducers";
const Cart = createContext();
const Context = ({ children }) => {
let products = [];
console.log("part1", products);
ProductService.getAllProducts().then((res) => {
products = res.data; //Also tried setState({ products : res.data})
console.log("response: ", products);
});
const [state, dispatch] = useReducer(cartReducer, {
products: products,
cart: [],
});
return <Cart.Provider value={{ state, dispatch }}>{children}</Cart.Provider>;
};
export const CartState = () => {
return useContext(Cart);
};
export default Context;
ProductService.js
import axios from "axios";
const PRODUCT_BASE_URL = "http://localhost:8080/api/v1/";
class ProductService {
getAllProducts() {
return axios.get(PRODUCT_BASE_URL + "products");
}
getProductsByCategory(category) {
return axios.get(PRODUCT_BASE_URL + "products/" + category);
}
getProductById(id) {
return axios.get(PRODUCT_BASE_URL + "product/" + id);
}
}
export default new ProductService();
Reducers.js
export const cartReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
HomeComponenet.jsx
import React from "react";
import SlideShowComponent from "./SlideShowComponent";
import HomeCategoriesComponent from "./HomeCategoriesComponent";
import FeaturedProductsComponent from "./FeaturedProductsComponent";
import { CartState } from "../context/Context";
// class HomeComponent extends React.Component
function HomeComponent() {
const { state } = CartState();
console.log("Cart Inside the Home Component: ", state);
return (
<>
<SlideShowComponent />
<div>
<HomeCategoriesComponent />
</div>
<div>
<FeaturedProductsComponent />
</div>
</>
);
}
export default HomeComponent;
First of all, I've created a fixed example here: https://stackblitz.com/edit/react-jqtcia?file=src/Context.js
You are correct, you need to await the result from axios. In React, we use the useEffect hook for things with side effects or that should not be done as part of the render. Renders in react should be non blocking, that is they should not be dependent on things like data fetching.
A simple example of this would be if we needed it in local state. This example renders without the data, then re-renders once the data is available.
const [products, setProducts] = useState([]);
useEffect(async () => {
const { data } = await ProductService.getAllProducts();
setProducts(data);
}, []);
return <div>{products.length > 0 ? `${products.length} products` : 'Loading...'</div>
NOTE: the , []); means that this will fire once, when the first render happens.
This fixes the first part of your problem, getting the result out of the request/axios.
However, the second part and most important part is that you weren't using this value. You were attempting to insert the result as part of the initial state, but this was empty by the time it was created. As you are using reducers (useReducer), this means you need to dispatch an action for each event and handle all the relevant events to data fetching in the reducer. That means, you should need to be able to handle:
Some data is in a loading state (e.g., pagination or first load)
The data failed to load
The data is partially loaded
The data has fully loaded
I've created a minimal happy example (there is no pagination and data fetching always succeeds):
Context.js
useEffect(async () => {
const { data: loadedProducts } = await ProductService.getAllProducts();
console.log('response: ', JSON.stringify(loadedProducts));
dispatch({ type: 'PRODUCTS_LOADED', products: loadedProducts });
console.log(state);
}, []);
Reducers.js
export const cartReducer = (state, action) => {
console.log(action);
switch (action.type) {
case 'PRODUCTS_LOADED':
const newState = { ...state, products: action.products };
console.log(newState);
return newState;
default:
return state;
}
};
In fact, if you'd done this with your original code, it would've worked:
ProductService.getAllProducts().then((res) => {
dispatch({ type: 'PRODUCTS_LOADED', products : res.data});
console.log("response: ", products);
});
However, this has a different bug: It will refetch the data and then dispatch the event each time Context.js is re-rendered.
Since you've asked for more information in your comment, I'll provide it here (comments were not big enough).
I've linked the relevant API documentation for the hooks above, these provide pretty good information. React does a pretty good job of explaining the what, and why of these hooks (and the library itself). Seriously, if you haven't read their documentation, you should do so.
Additional resources:
What, when and how to use useEffect - In short, if you have something to do that isn't rendering such as data fetching, it's a side-effect and should be in a useEffect.
What is a reducer in JavaScript/React/Redux - Reducers are a pattern to make shared state easier to manage and test. The basic idea is that you define a reducer, which takes an initial state and an event/action, and produces a new state. The key idea is that there must be no side-effects, the same action and state will always produce the same result, no matter the date/time, network state, etc, etc. This makes testing and reasoning about things easier, but at the cost of a more complex state management.
However, something important that I ignored in your original question is that you are kind of reinventing the wheel here. There is already a library that will centralise your state and make it available via context, it's the library that originally invented reducers: redux. I'm guessing you have read something like this article about using context instead of redux, however the advantage of redux for you is that there is a litany of documentation about how to use it and solve these problems.
My recommendation for you is to make sure you need/want redux/reducers. It has it's value and I personally love the pattern but if you are just getting started on React, you would be better off just using useState in my opinion.

How to get object out of React component

I am making an weather app with API, I am successfully receiving the data with API in the function but I am not able to take it out of the function
here is the code
import React, {useState} from 'react'
import {Text, View} from 'react-native'
const axios = require('axios');
let HomeScreen =() => {
let key = "XXXX"
axios.get(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
.then(function (response) {
// handle success
console.log(response)
})
return(
<Text>This is {response}</Text>
)
}
export default HomeScreen
If you want to simply return data from the API use a normal JS function, not a React component.
function getData() {
return axios(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
}
It will return a promise which you can then use somewhere else.
async main() {
const data = await getData();
}
If you want your component to render data retrieved from the API you need to do things differently.
Use useEffect to run once when the component is mounted, and use useState to store the data once it's been retrieved. The state will then inform the JSX how to render.
Note that the response from the API is an object with location and current properties. You can't just add that to the JSX because React won't accept it. So, depending on what value you need from the data, you need to target it specifically.
Here's an example that returns the text value from the condition object of the current object: "It is Sunny".
const { useEffect, useState } = React;
function Example() {
// Initialise your state with an empty object
const [data, setData] = useState({});
// Call useEffect with an empty dependency array
// so that only runs once when the component is mounted
useEffect(() => {
// Retrieve the data and set the state with it
async function getData() {
const key = 'XXX';
const data = await axios(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
setData(data.data);
}
getData();
}, []);
// If there is no current property provide a message
if (!data.current) return <div>No data</div>;
// Otherwise return the current condition text
return (
<p>It is {data.current.condition.text}</p>
);
}

React useEffect rendering more than once

I'm new in Hooks. I coded a project with componentDidMount.
Now I'm learning about hooks and rewriting this project with hooks. I want to fetch the data and print it on the console first.
However, it renders 3 times. It is probably because I used 2 setState in useEffect. However, in one of them I set the data to data array and in the other I keep the loading value for spinner control. How can I use useEffect like componentDidMount just one time to pull data and set my states?
When I write the console into useEffect, "React Hook useEffect has a missing dependency: 'data'." warning and returns an empty list.
Btw I deleted strictmode.
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
const { data } = await axios.get("/data/data.json");
setData(data);
setLoading(false);
};
fetchData();
}, []);
console.log(data);
return <div className="App">App</div>;
}
export default App;
The component re-renders normally after each state update.
Hence, in your example, it re-renders when the value of data is updated and again, when loading is updated.
Please check this demo to examine how the renderings occur after each state change:
Initial render
After loading is set to true
After data is fetched and set
After loading is set to false
I think having the data variable twice might be causing a conflict with the linter. You can rename your data coming from your API call to prevent the warning: "React Hook useEffect has a missing dependency: 'data'."
Your component will re-render on each state update, but the useEffect will only run when your dependencies change. Since they aren't going to change, the API call only happens once.
To prove it, you can move the console.log(result) in your useEffect and see it only logs once. However, make sure you call it on your result and not data, because the state won't be updated until the next render after calling setData.
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
const { data: result } = await axios.get("/data/data.json");
setData(result);
setLoading(false);
console.log(result); // runs once
};
fetchData();
}, [setData, setLoading]);
console.log(data); // runs 3 times
return <div className="App">App</div>;
}
export default App;

Why is useState() not setting the default value?

I have created following reactComponent. From what I understand useState([]); should set the comments variable to an array. But it does not. I get an exeption that I Try to run .map() on an object. What I have to do is Object.values(comments) to make it work but I don't understand why. My api definetly returns an array of comment objects.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default ({ postId }) => {
const [comments, setComments] = useState([]);
const fetchData = async () => {
const res = await axios.get(
`http://localhost:4001/posts/${postId}/comments`
);
setComments(res.data);
};
useEffect(() => {
fetchData();
}, []);
const renderedComments = comments.map(comment => {
return <li key={comment.id}>{comment.content}</li>;
});
return <ul>{renderedComments}</ul>;
};
Your use of state is correct as far as I can tell, the fact that Object.values works on the data handed back to you implies it is definitely either an object or an array, have you run Array.isArray(res.data) as part of your troubleshooting process?
As stated by a commenter above, the most likely scenario is that you are getting an object back from your back end. Other things you can do to confirm its the data handed back to you at fault would be to simply comment out your useEffect and see if it still throws the same exception.

Categories