I have a hook that rules my requests. It has state "loading" that becoming true while loading
export const useHttp = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (url, method = 'GET', body = null, headers = {'Content-Type': 'application/json' }) => {
setLoading(true);
try {
const res = await fetch(url, {method, body, headers});
if (!res.ok) {
throw new Error(`Could not fetch ${url}, status: ${res.status}`);
}
const data = await res.json();
setLoading(false);
return data;
} catch(e) {
setLoading(false);
setError(e.message);
throw e;
}
}, [])
const clearError = useCallback(() => {
setError(null);
}, [])
return {
loading,
error,
request,
clearError
}
}
Also i have a service that makes requests:
import { useHttp } from "../hooks/http.hook";
const useNasaService = () => {
const { loading, error, request, clearError } = useHttp();
const _apiBase = 'https://api.nasa.gov/';
const _apiKey = 'api_key=DEMO_KEY';
const getMissionManifest = async (rover) => {
const res = await request(`${_apiBase}mars-photos/api/v1/manifests/${rover}/?${_apiKey}`);
return _transformManifestData(res.photo_manifest);
}
const getImagesData = async (rover, sol, page = 1) => {
const res = await request(`${_apiBase}mars-photos/api/v1/rovers/${rover}/photos?sol=${sol}&page=${page}&${_apiKey}`);
return res.photos.map(_transformImagesData);
}
const _transformImagesData = (data) => {
return {
id: data.id,
sol: data.sol,
earthDate: data.earth_date,
path: data.img_src,
camera: data.camera.full_name,
rover: data.rover.name
}
}
const _transformManifestData = (data) => {
return {
landingDate: data.landing_date,
launchDate: data.launch_date,
maxDate: data.max_date,
maxSol: data.max_sol,
name: data.name,
photos: data.photos,
status: data.status,
totalPhotos: data.total_photos
}
}
return {
loading,
error,
clearError,
getImagesData,
getMissionManifest
}
}
export default useNasaService;
Finally i have a component that needs state "loading" for disabling the inputs. The question is why "loading" is never getting true in this component:
import useNasaService from '../../services/useNasaService';
const RoverFilter = (props) => {
const { loading } = useNasaService();
console.log(loading); /* always false */
const onRadioChange = (e) => {
props.onRoverSelected(e.target.value);
props.onRoverClicked(e.target.value);
}
return (
<div className="roverFilter" >
<h2 className="roverFilter__title">Select rover</h2>
<div className="roverFilter__inputs">
<label htmlFor="curiosity">Curiosity</label>
<input disabled={loading} type="radio" name="rover-choise" id="curiosity" value="curiosity" onChange={onRadioChange}/>
<label htmlFor="opportunity">Opportunity</label>
<input disabled={loading} type="radio" name="rover-choise" id="opportunity" value="opportunity" onChange={onRadioChange}/>
<label htmlFor="spirit">Spirit</label>
<input disabled={loading} type="radio" name="rover-choise" id="spirit" value="spirit" onChange={onRadioChange}/>
<label htmlFor="perseverance">Perseverance</label>
<input disabled={loading} type="radio" name="rover-choise" id="perseverance" value="perseverance" onChange={onRadioChange}/>
</div>
</div>
)
}
export default RoverFilter;
By the way, in my app there are another components, where "loading" becoming true without any problems. I cant see the difference.
for example, here loading works good:
import { useEffect, useState } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import useNasaService from '../../services/useNasaService';
import ImageGallerySkeleton from '../imageGallerySkeleton/ImageGallerySkeleton';
import Spinner from '../spinner/Spinner';
import ErrorMessage from '../errorMessage/ErrorMessage';
import SliderModal from '../sliderModal/SliderModal';
const ImageGallery = (props) => {
const {loading, getImagesData, clearError, error} = useNasaService();
const [imagesData, setImagesData] = useState([]);
const [nextPage, setNextPage] = useState(1);
const [firstLoading, setFirstLoading] = useState(true);
const [imagesDataLoaded, setImagesDataLoaded] = useState(false);
const [itemIndex, setItemIndex] = useState(0);
const [sliderOpen, setSliderOpen] = useState(false);
const transitionDuration = 1000;
const onImagesDataLoaded = (newData) => {
setImagesData(data => [...data, ...newData]);
setNextPage(page => page 1);
setFirstLoading(false);
setImagesDataLoaded(true);
}
const onRequestImages = (rover, sol, page) => {
clearError();
if (!rover || !sol) return;
getImagesData(rover, sol, page)
.then(onImagesDataLoaded);
}
const onSliderClosed = () => {
setSliderOpen(false);
}
useEffect(() => {
onRequestImages(props.selectedRover, props.selectedSol, nextPage);
// eslint-disable-next-line
}, [props.selectedRover, props.selectedSol])
if (sliderOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "visible";
}
function renderItemList(arr) {
const itemList = arr.map((item, i) => {
return (
<CSSTransition
key={item.id}
in={imagesDataLoaded}
timeout={transitionDuration}
classNames='imageGallery__card'>
<li className="imageGallery__card"
onClick={() => {
setSliderOpen(true);
setItemIndex(i);
}}>
<img src={item.path} alt="img from mars"/>
<div className="imageGallery__descr">
<ul>
<li>Rover: {item.rover}</li>
<li>Earth_date: {item.earthDate}</li>
<li>Sol: {item.sol}</li>
<li>{item.camera}</li>
</ul>
</div>
</li>
</CSSTransition>
)
})
return (
<ul className="imageGallery__list">
<TransitionGroup component={null}>
{itemList}
</TransitionGroup>
</ul>
)
}
const spinner = loading && firstLoading ? <Spinner/> : null;
const skeleton = imagesData.length === 0 && firstLoading && !loading && !error ? <ImageGallerySkeleton/> : null;
const items = renderItemList(imagesData);
const errorMessage = error ? <ErrorMessage/> : null;
const counter = imagesData.length === 0 || error ? null :
<h2 className="imageGallery__title">
Showed {loading ? "..." : imagesData.length} photos of {props.totalPhotosInSol}
</h2>
const button = props.totalPhotosInSol === imagesData.length ? null :
<button
onClick={() => onRequestImages(props.selectedRover, props.selectedSol, nextPage)}
disabled={loading}
className="imageGallery__btn">{loading ? "Loading..." : "Load next page" }
</button>
const slider = <SliderModal
open={sliderOpen}
items={imagesData}
slideIndex={itemIndex}
onSliderClosed={onSliderClosed} />
const wrapStyles = firstLoading && loading ? {"padding": "50px"} : null;
return (
<section className="imageGallery" style={wrapStyles}>
{counter}
{spinner}
{skeleton}
{imagesData.length === 0 && !firstLoading ?
<h2 className="imageGallery__title">There is no photo for this sol</h2> :
items
}
{button}
{errorMessage}
{slider}
</section>
)
}
export default ImageGallery;
CodePudding user response:
When you call useState
in two different components, those states are independant from eachother. This is still true if you move the useState
calls inside a custom hook. If two components call useNasaService
(which calls useHttp
, which calls useState
), then the two components are creating their own states and own functions. If component A starts loading data, that will have no effect on component B.
So ImageGallery
is working because it makes a call to getImagesData
. This sets the loading state of ImageGallery
to true. No other components are affected by this though. When the loading finishes, ImageGallery
will set state to have the new data, but again, no other components can use this. RoverFilter
on the other hand never calls getImagesData
, so its loading state stays false, and it never gets any data.
In react, the typical way to share data is to lift state up. You have a component higher up in the tree, which is responsible for loading the data and setting state. That component then passes the data and functions down to any children that need it. You can either pass the data down using props, or if you need to pass the data a long distance you can consider using context instead.
There's also a wide variety of 3rd party libraries which can be used to manage global state. For example, Redux, Jotai, Zustand, MobX. These can make it simpler to share data between components in far-flung parts of the component tree.