I created a custom useFetch() hook so I can make my code more dynamic and less repetitive. The problem is that I can't display my data in App.js.
I get these errors:
Cannot read properties of undefined (reading 'map').
react-dom.development.js:67 Warning: 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.
I did a console.log(genres) to see if there are any errors from my custom hook, but it works fine, logs all the genres. The problem is caused as soon as I try to display my data using the map method.
CodeSandbox link
useFetch.js
import { useReducer, useEffect } from "react";
import axios from "axios";
const ACTIONS = {
API_REQUEST: "api-request",
FETCH_DATA: "fetch-data",
ERROR: "error",
};
const initialState = {
data: [],
loading: false,
error: null,
};
function reducer(state, { type, payload }) {
console.log(payload);
switch (type) {
case ACTIONS.API_REQUEST:
return { ...state, data: [], loading: true };
case ACTIONS.FETCH_DATA:
return { ...state, data: payload, loading: false };
case ACTIONS.ERROR:
return { ...state, data: [], error: payload };
default:
return state;
}
}
function useFetch(url) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: ACTIONS.API_REQUEST });
axios
.get(url)
.then((res) => {
dispatch({ type: ACTIONS.FETCH_DATA, payload: res.data });
})
.catch((e) => {
dispatch({ type: ACTIONS.ERROR, payload: e.error });
});
}, [url]);
return state;
}
export default useFetch;
App.js
import "./styles.css";
import useFetch from "./useFetch";
export default function App() {
const BASE_URL =
"https://api.themoviedb.org/3/genre/movie/list?api_key=${API_KEY}";
const { data: genres, loading, error } = useFetch(BASE_URL);
console.log(genres);
return (
<div className="App">
{genres.genres.map((genre) => (
<div key={genre.id}>{genre.name}</div>
))}
</div>
);
}
Your initial state has data as an array:
const initialState = {
data: [],
loading: false,
error: null,
};
And your App component is trying to read the property genres on that array as soon as it loads. There is no property on an array with that name, so genres.genres is undefined, and the map call on it will throw an error.
I would initialise initialState.data as {genres: []}, by passing the data container as another argument to your hook rather than hardcoding it into the hook file.
function useFetch(url, data) {
const [state, dispatch] = useReducer(reducer, {...initialState, data});
...
}
const { data: genres, loading, error } = useFetch(BASE_URL, {genres: []});
Related
After passing the product ID I'm being redirected to the product details page. In that page, I have details of the product such as name, destination, price and date etc. As I'm using redux and getting the data from mongodb the date is saved as 2021-11-23T18:00:00.000Z this format. So after fetching the data in my product details page I used flight?.data?.date.split("T")[0] to only show the date. But after clicking the button to go to the product details page it shows the following error TypeError: Cannot read properties of undefined (reading 'split'). But when I remove the split part from the code it is working fine. I guess the only reason is the code is running before the data is loaded. So for that I tried to show loading spinner before the data loads by dispatching loading action. But it isn't working. I've used redux thunk for other actions that is why I've added async in Action. Otherwise it gives me dispatch is not defined.
Details page
import { CircularProgress } from "#mui/material";
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
import { getFlight, loadingStatus } from "../../actions/flights";
import "./Details.css";
const FlightDetails = () => {
const flight = useSelector((state) => state.flights);
console.log("flight", flight.loading);
const dispatch = useDispatch();
const { id } = useParams();
useEffect(() => {
dispatch(loadingStatus(true));
dispatch(getFlight(id));
dispatch(loadingStatus(false));
}, [id, dispatch]);
return (
<div className="details_parent">
{flight.loading ? (
<CircularProgress />
) : (
<div className="details_container">
<div className="form_data">
<span>From</span>
<h3>{flight?.data?.from}</h3>
</div>
<div className="form_data">
<span>To</span>
<h3>{flight?.data?.to}</h3>
</div>
<div className="form_data">
<span>Airline</span>
<h3>{flight?.data?.airline}</h3>
</div>
<div className="form_data">
<span>Trip type</span>
<h3>{flight?.data?.flightType}</h3>
</div>
<div className="form_data">
<span>Date(yyyy-mm-dd)</span>
<h3>{flight?.data?.date.split("T")[0]}</h3>
</div>
<div className="form_data">
<span>Price</span>
<h1>${flight?.data?.price}</h1>
</div>
</div>
)}
</div>
);
};
export default FlightDetails;
Reducer
const initialState = {
data: [],
loading: false,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "GET_FLIGHTS":
return { ...state, data: action.payload };
case "GET_FLIGHT":
return { ...state, data: action.payload };
case "CREATE_FLIGHT":
return { ...state, data: { ...action.payload } };
case "SEARCH":
return { ...state, data: action.payload };
case "LOADING":
return { ...state, loading: action.payload };
default:
return state;
}
};
export default reducer;
Actions
export const loadingStatus = (status) => async (dispatch) => {
dispatch({
type: "LOADING",
payload: status,
});
};
React runs a first render before running any useEffect calls. Change your initial loading state to true.
const initialState = {
data: [],
loading: true,
};
why you are not using flight?.data?.date?.split("T")[0]
but in this case you have to set loading in initial data of reducer to true
const initialState = {
data: [],
loading: true,
};
Rather than setting separate actions for change the loading state I've set the dispatching before getting the data in actions. And after the promise is returned I've again set the dispatch to false.
Actions to get the specific flight data
export const getFlight = (id) => async (dispatch) => {
try {
dispatch({
type: "LOADING",
payload: true,
});
const { data } = await api.fetchFlight(id);
dispatch({
type: "GET_FLIGHT",
payload: data,
});
dispatch({
type: "LOADING",
payload: false,
});
} catch (error) {
console.log(error);
}
};
I want to use the error value as an alert in my application. my server response also gave me the correct response. but when used in reacts error value shows undefined.
<b>Response Getting from API request</b>
{"success":false,"error":"Product Not Found"}
<b>code in react file</b>
const alert = useAlert();
const dispatch = useDispatch();
const { loading, error, products } = useSelector((state) => state.products);
useEffect(() => {
if (error) {
alert.error(error);
}
dispatch(getProduct());
}, [dispatch, error, alert]);
<b>Reducer</b>
export const productReducer = (state = { products: [] }, action) => {
switch (action.type) {
case ALL_PRODUCT_REQUEST:
return {
loading: true,
products: [],
};
case ALL_PRODUCT_SUCCESS:
return {
loading: false,
products: action.payload.products,
productsCount: action.payload.productsCount,
};
case ALL_PRODUCT_FAIL:
return {
loading: false,
error: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
<b>action</b>
export const getProduct = () => async (dispatch) => {
try {
dispatch({ type: ALL_PRODUCT_REQUEST });
const { data } = await axios.get("/api/v1/products");
dispatch({
type: ALL_PRODUCT_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: ALL_PRODUCT_FAIL,
payload: error.response.data.message,
});
}
};
<b>store file</b>
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import {
productReducer,
productDetailsReducer,
} from "./reducers/productReducer";
const reducer = combineReducers({
products: productReducer,
productDetails: productDetailsReducer,
});
let initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
Not able to see this function works alert. error(error).when I console this error it shows undefined. I want to show an alert "Product Not Found" when getting an error.
Not sure what is your redux structure is, but it seems like you are extracting out your state from the redux store incorrectly. So maybe try to replace this
const { loading, error, products } = useSelector((state) => state.products);
with
const { loading, error, products } = useSelector((state) => {
return {
loading: state.loading,
products: state.products,
error:state.error
});
EDIT:
State present in the useSelector is your root reducer which means it contains all the reducers in it, just for example, your product reducer, your cart reducer, user reducer and so on. Since, productReducer is being used in your root reducer, you should first access the productReducer and then its properties which should look like below (adjust the variables as per your variable names)
const { loading, error, products } = useSelector((state) => {
return {
loading: state.products.loading,
products: state.products.products,
error:state.products.error
});
This is because useSelector actually accesses data from the state (managed by Redux) on the frontend. This is data after fetching from the API so this is purely the data part from the API and not error or loading
Here, you have destructured fields of whatever gets returned when you try to access the products slice of your state. Which will not have fields like loading or error but an array of product objects.
To fix this, you should store error and loading responses from your API in your Redux state in a structured and only then can you access them in each of your React components accessing this state.
eg code :
const { loading, error, products } = useSelector((state) => {
// Curly braces with return should not be on next line
return {
loading: state.loading,
products: state.products,
error:state.error
});
Also, I would suggest using packages like react-query to address such use cases well
Alternative
Write your own hook for handling such async tasks
Code for hook
import { useCallback, useEffect, useState } from "react"
export default function useAsync(callback, dependencies = []) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState()
const [value, setValue] = useState()
// Simply manage 3 different states and update them as per the results of a Promise's resolution
// Here, we define a callback
const callbackMemoized = useCallback(() => {
setLoading(true)
setError(undefined)
setValue(undefined)
callback()
// ON SUCCESS -> Set the data from promise as "value"
.then(setValue)
// ON FAILURE -> Set the err from promise as "error"
.catch(setError)
// Irresp of fail or success, loading must stop after promise has ran
.finally(() => setLoading(false))
// This function runs everytime some dependency changes
}, dependencies)
// To run the callback function each time it itself changes i.e. when its dependencies change
useEffect(() => {
callbackMemoized()
}, [callbackMemoized])
return { loading, error, value }
}
Using this hook in the component
import useAsync from "./useAsync"
export default function AsyncComponent() {
const { loading, error, value } = useAsync(() => {
// 3 states and their updated versions are returned while the promise is getting resolved
return new Promise((resolve, reject) => {
const success = false
setTimeout(() => {
success ? resolve("Hi") : reject("Error")
}, 1000)
})
})
return (
<div>
<div>Loading: {loading.toString()}</div>
<div>{error}</div>
<div>{value}</div>
</div>
)
}
using nextjs for server-side-rendering trying to get the state from redux store in getServerSideProps(). but getting emtpy value.
getting data from redux in client side inside the component with const productList = useSelector(state => state.productList) const { loading, error, products } = productList works fine. but when using getServersideProps() im getting emtpy results.
index.js:
import store from '../redux/store'
export default function Home({products, error, loading}) {
const dispatch = useDispatch()
useEffect(() => {
dispatch(listProducts())
}, [dispatch])
return (
<>
<Header />
<Products loading={loading} error={error} products={products} />
<Footer />
</>
)
}
export async function getServerSideProps() {
const state = store.getState()
const { loading, error, products } = state.productList
return {props: {products: products, loading: loading, error: error}}
}
*note: even when i did console.log(store.getState()) inside the component its still returning empy array
reducer:
export const productListReducer = (state = { products: [] }, action) => {
switch (action.type) {
case 'PRODUCT_LIST_REQUEST':
return { loading: true, products: [] }
case 'PRODUCT_LIST_SUCCESS':
return { loading: false, products: action.payload }
case 'PRODUCT_LIST_FAIL':
return { loading: false, error: action.payload }
default:
return state
}
}
action:
import axios from 'axios'
export const listProducts = () => async (dispatch) => {
try {
dispatch({ type: 'PRODUCT_LIST_REQUEST' })
const { data } = await axios.get('/api/products')
dispatch({
type: 'PRODUCT_LIST_SUCCESS',
payload: data
})
} catch (error) {
dispatch({
type: 'PRODUCT_LIST_FAIL',
payload: error.response && error.response.data.message
? error.response.data.message : error.message
})
}
}
store.js:
const reducer = combineReducers({
productList: productListReducer,
categoryList: categoryListReducer,
})
const initialState = {}
const middleware = [thunk]
const store = createStore(
reducer, initialState, composeWithDevTools(applyMiddleware(...middleware))
)
export default store
try invoking getState() directly and don't forget to pass it as argument and also make sure you have passed the store to your app component
export async function getServerSideProps(getState) {
const state = getState()
const { loading, error, products } = state.productList
return {props: {products: products, loading: loading, error: error}}
}
The issue is useDispatch is a React-Redux function, but the store has not been connected to the React components.
Instead of useDispatch try store.dispatch instead:
import store from '../redux/store'
export default function Home({products, error, loading}) {
useEffect(() => {
store.dispatch(listProducts())
})
return (
<>
...
</>
)
}
Note, the array passed to useEffect controls when that effect is run, so it would not make sense to pass in the dispatch function. See this post for more details.
You could also connect the Redux store to the React components using React-Redux and keep using useDispatch.
I'm trying to render the data from the following object of data which is coming from an API.
{
"code": 0,
"c": "verified",
"d": "verified",
"leaseInfo": {
"infoId": 6
},
"cpfPrice": "500.00",
"carCurrentLocation": {
"id": 1,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
},
"n": "verified",
"p": "false",
"ownerCarInfo": {
"brand": "Ferrari",
"model": "0"
},
"serviceFeeRate": 0.10,
"depositPrice": "100.00",
"pics": [
{
"picid": 49,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
},
],
"items": {
"itemid": 5,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
}
}
I'm using react-redux to dispatch an action, where I will be provided with the data under a state named 'carDetails'.
However, when I try to access the data, if my component is refreshed, carDetails becomes undefined and hence gives "Cannot read property ownerCarInfo of undefined."
I'm obtaining and de-structuring the data of carDetails like this in my React component:
import React, {useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
const CarInfo = ({ match }) => {
const dispatch = useDispatch();
const details = useSelector((state) => state.carDetails);
const { loading, carDetails } = details;
const {pics, carCurrentLocation, items, ownerCarInfo} = carDetails;
useEffect(() => {
dispatch(getCarDetails(match.params.id));
}, [dispatch, match]);
return (
<div>
{loading ? (
<Loader></Loader>
) : (
<>
<p>{d.depositPrice}</p>
<p>{ownerCarInfo.brand}</p>
</>
)}
</div>
);
)
}
As long as the component or the React application is not refreshed, it retrieves data and displays it correctly. The carDetails becomes an empty array as soon as the page is refreshed.
This is the getCarDetails() action:
export const getCarDetails = (id) => async (dispatch, getState) => {
try {
dispatch({
type: CAR_DETAILS_REQUEST,
});
const { userLogin } = getState();
const { userInfo } = userLogin;
const config = {
headers: {
Authorization: userInfo.token,
'Content-Type': 'application/json',
},
};
const { data } = await axios.get(
`${BASE_API}/car/info/getDetails/${id}/${userInfo.bscId}`,
config
);
dispatch({
type: CAR_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: CAR_DETAILS_FAIL,
payload:
error.response && error.response.data.msg
? error.response.data.msg
: error.msg,
});
}
};
This is my reducer:
export const carsDetailsReducer = (state = { carDetails: [] }, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return { loading: true };
case CAR_DETAILS_SUCCESS:
return { loading: false, carDetails: action.payload };
case CAR_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
This is how I declare carDetails in the redux store.
const reducer = combineReducers({
carDetails: carsDetailsReducer,
});
What is the cause for carDetails becoming undefined and the useEffect not running on page refresh?
If you are using axios your action should look like this with async function and await while you are calling API.
If you are passing API car id in the api link then pass the id in the parameters:
import axios from "axios";
export const loadData = (id) => async (dispatch) => {
dispatch({
type: "CAR_DETAILS_REQUEST",
});
const detailData = await axios.get("http:\\****/id");
dispatch({
type: "CAR_DETAILS_SUCCESS",
payload: {
success: detailData.data,
},
});
};
Reducer:
const initailState = { carDetails: [], loading: true };
export const carsDetailsReducer = (state = initailState, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return { ...state,
loading: true
};
case CAR_DETAILS_SUCCESS:
return {...state,
loading: false,
carDetails: action.payload
};
case CAR_DETAILS_FAIL:
return { ...state,
loading: false,
error: action.payload };
default:
return ...state;
}
};
Your useEffect should only work when data is fetched:
import React, {useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
const CarInfo = ({ match }) => {
const dispatch = useDispatch();
const details = useSelector((state) => state.carDetails);
const { loading, carDetails } = details;
const {pics, carCurrentLocation, items, ownerCarInfo} = carDetails;
useEffect(() => {
dispatch(getCarDetails(id));
}, [dispatch]);
return (
<div>
{loading ? (
<Loader></Loader>
) : (
<>
<p>{d.depositPrice}</p>
<p>{ownerCarInfo.brand}</p>
</>
)}
</div>
You can also use it without a useEffect by making an onclick() function like this:
const loadDetailHandler = () => {
dispatch(getCarDetails(id));
};
return (
<div onClick={loadDetailHandler} >
</div>
If carDetails initial state is an array, then why are you destructuring object properties from it in your UI? Question for another time...
If after reloading the page the state reverts back to the initial state, an empty array is still a defined object. You need to track down what is causing your state.carDetails.carDetails to become undefined. If you examine your reducer notice that your CAR_DETAILS_REQUEST case wipes the carDetails state out and it becomes undefined. Honestly I'm surprised you aren't seeing this issue when your code runs normally without a page reload.
You need to hold on to that state. For good measure, you should always shallow copy the existing state when computing the next state object unless you've good reason to omit parts of state.
export const carsDetailsReducer = (state = { carDetails: [] }, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return {
...state, // <-- shallow copy existing state
loading: true,
};
case CAR_DETAILS_SUCCESS:
return {
...state, // <-- shallow copy existing state
loading: false,
carDetails: action.payload
};
case CAR_DETAILS_FAIL:
return {
...state, // <-- shallow copy existing state
loading: false,
error: action.payload,
};
default:
return state;
}
};
for me, I think you should save the state in the
`case CAR_DETAILS_REQUEST:
return {
...state, // <-- shallow copy existing state
loading: true,
};
`
to be able to use it before o when you want to use a reducer you should each case
have the old state the reducer return the same sharp of initial state that put it you also used is loading and that not found in the initial state
so try to make the shape of the state
state={
isloading:false,
carDetails: []
}
also try each time to same the state by {...state ,is loading:true}
The problem is in CAR_DETAILS_REQUEST. You only return { loading: true }; so carDetails will be lost and become undefined.
Just update your reducer like this:
case CAR_DETAILS_REQUEST:
return { ...state, loading: true };
I'm trying to create a loading state for my Redux but it looks to "slow" to get updated.
First action fetchDB => setLoading: true => once over setLoading: false
Second action fetchCat => doesn't have the time to fire it that crashes
Really simple:
set loading action:
export const setLoading = () => {
return async (dispatch) => {
await dispatch({ type: SET_LOADING }); // no payload by default goes to true
};
};
set loading reducer:
import {
FETCH_DB,
SET_LOADING,
} from "../types"
const initalState = {
db: [],
loading: false,
}
export default (state = initalState, action) => {
switch (action.type) {
// this like the other cases sets loading to FALSE
case FETCH_DB:
return {
...state,
db: action.payload,
current: null,
loading: false,
}
case FETCH_CAT_FOOD:
return {
...state,
food: action.payload,
loading: false,
}
case FETCH_CAT_DESIGN:
return {
...state,
design: action.payload,
loading: false,
}
case SET_LOADING:
return {
...state,
loading: true,
}
default:
return state
}
}
then action I use that creates the problem:
export const fetchCat = kindof => {
return async dispatch => {
dispatch(setLoading()) // looks like that it doesn't get fired
const response = await axios
.get(`http://localhost:5000/api/categories/${kindof}`)
.then(results => results.data)
try {
await dispatch({ type: `FETCH_CAT_${kindof}`, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
and then the file (a custom component) that creates the problem.
It crashes cause categories.map is undefined.
It doesn't find loading: true so the loader doesn't stop.
import React, { useState, useEffect, Fragment } from "react"
import { Spinner } from "react-bootstrap"
import { connect, useDispatch, useSelector } from "react-redux"
import CatItem from "./CatItem" // custom component
import { fetchCat, setLoading } from "../../../store/actions/appActions"
const MapCat = ({ kindof, loading, categories }) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchCat(kindof)) // gives the category I want to fetch
// eslint-disable-next-line
}, [categories])
if (!loading) {
return (
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
)
} else {
return (
<Fragment>
<div>
{categories.map(item => (
<CatItem item={item} />
))}
</div>
</Fragment>
)
}
}
const mapStateToProps = (state, kindof) =>
({
loading: state.appDb.loading,
categories: state.appDb[kindof],
})
export default connect(mapStateToProps, { fetchCat, setLoading })(MapCat)
I think that it is supposed to work like this:
loading: false (by default) => true => time to fetch => false
But doesn't look like working. Any idea?
Firstly setLoading needs to return a plain object with type and payload
export const setLoading = () => ({ type: SET_LOADING });
In fetchCat the then is not required. Also async await for dispatch is not required.
export const fetchCat = (kindof) => {
return (dispatch) => {
dispatch(setLoading()); //<---this should now be ok.
const response = await axios.get(`http://localhost:5000/api/categories/${kindof}`)
// .then((results) => results.data); //<----- not required as you are using await
try {
dispatch({ type: `FETCH_CAT_${kindof}`, payload: response.data }); //<--- use response.data ...also async/await for dispatch is not rquired.
} catch (error) {
console.log("await error", error);
}
};
};
The 2nd arg of mapStateToProps is ownProps which is an object
const mapStateToProps = (state, ownProps) =>
({
loading: state.appDb.loading,
categories: state.appDb[ownProps.kindof],
})
You have quite a bit different way of calling dispatch. Let me list them out
dispatch(fetchCat(kindof)) // gives the category I want to fetch
await dispatch({ type: `FETCH_CAT_${kindof}`, payload: response })
You can see, await or not basically is the way you use async operation. However dispatch takes type and payload to function, which means you have to make sure what you send to dispatch is with the right object. Of course Redux does accept custom format via plugins, so maybe if you throw it a async as input, the reducer might understand it as well?
Please double check each dispatch first, for example, write a function that only dispatch one type of action. Only after you make each call working, don't move to assemble them together into a bundled call.