Check whether React.Suspense is ready - javascript

I'm using the Intersection Observer API in React to add some animations. I am adding as Intersection Entries some elements.
The problem is that I have the app in multiple languages, and due to the implementation that the tool I am using to translate has, I need to wrap all my components into React.Suspense to wait for languages to load.
When useEffect queries for the elements, they aren't still in the DOM, and therefore they are not assigned as entries.
This is my custom hook:
hooks/useObserver.js
import { useState } from "react";
import { useEffect, useRef } from "react";
export function useObserver(config = {}) {
const [elements, setElements] = useState([]);
const [entries, setEntries] = useState([]);
const observer = useRef(
new IntersectionObserver(observedEntries => {
setEntries(observedEntries);
}, config)
);
useEffect(() => {
const { current: currentObserver } = observer;
currentObserver.disconnect();
if (elements.length > 0) {
elements.forEach(el => currentObserver.observe(el));
}
return () => {
if (currentObserver) {
currentObserver.disconnect();
}
};
}, [elements]);
return { observer: observer.current, setElements, entries };
}
and this is my main component:
App.jsx
import Header from "./components/Header";
import Hero from "./components/Hero";
import Footer from "./components/Footer";
import { Loader } from "./components/shared/Loader";
import { useObserver } from "./hooks/useObserver";
import { useEffect, Suspense } from "react";
function App() {
const { entries, setElements } = useObserver({});
useEffect(() => {
const sections = document.querySelectorAll("section.animated-section");
setElements(sections);
};
}, [setElements]);
useEffect(() => {
entries.forEach(entry => {
entry.target.classList.toggle("section-visible", entry.isIntersecting);
});
}, [entries]);
return (
<Suspense fallback={<Loader />}>
<Header />
<Hero />
<Footer />
</Suspense>
);
}
export default App;
I tried to set a timeout to wait some seconds and then add the elements as entries, and it works correctly:
useEffect(() => {
const observeElements = () => {
const sections = document.querySelectorAll("section.animated-section");
setElements(sections);
};
const observeElementsTimeout = setTimeout(observeElements, 3000);
return () => clearTimeout(observeElementsTimeout)
}, [setElements]);
I want to know if:
There is a way to know when React.Suspense is ready
There is a better approach to solve my problem
Thanks in advance!!

Related

How to fetch API as soon as page is loaded in React?

Whenever I visit a page it should automatically fetch the API
import React from 'react'
const Component = () => {
fetch("api url").then((res) => console.log(res))
return (
<div>comp</div>
)
}
export default Component
It is very simple using react hook use effect please learn basics of useffect hook on react docs or any youtube tutorial and as for the answer
import React, { useEffect } from 'react'
const comp = () => {
useEffect(() => {
fetch("api url").then((res)=>console.log(res))
}, [])
return (
<div>comp</div>
)
}
export default comp
here empty dependency means every time page loads only once
use the useEffect for this.
The useEffect method will execute the passed callback on the mount of the component and on every time one of the dependency array parameters is changed. therefore:
const Comp = () => {
useEffect(() => {
fetch("api url").then((res)=>console.log(res))
}, []);
return (
<div>comp</div>
)
}
Will make the callback to fire only once (because the empty dependency array) on the component mount.
You should use the useEffect Hook in your principal component like app.js
import React, {useEffect} from 'react'
useEffect(() => {
fetch("api url").then((res)=>console.log(res))
}, []);
Be careful, this manipulation can consume a lot of resources (a lot of data to fetch etc.)
Thery
import React, { useState, useEffect } from 'react'
const Comp = () => {
const [ data, setData ] = useState([]);
const getData = async () => {
const res = await fetch("api url");
const data = await res.json();
setData(data)
}
useEffect(()=>{ getData() },[]);
return (
<>
<div>comp</div>
// dispaly your data here from data state
</>
)
}
export default Comp;
Fetch and use data with useState
const initialValue = {};
const comp = () => {
const [data, setData] = useState(initialValue);
useEffect(() => {
let ignore = false;
const fetchData = async () => {
const res = fetch("api url");
if (ignore) { return; }
setData(res.json())
return () => {
ignore = true;
}
}
, [])
return (
<div>comp {data.prop}</div>
)
}
More on working with state
More about useEffect life cycle
Hope it helps
You don't need to use the API function like this, it will be called continuously, you need to use useEffect hook, when your component reloads useEffect will be called, and you can learn about the useEffect dependency here,
import React, { useEffect, useState } from 'react'
const comp = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch("api url").then((res)=> {
console.log(res)
setData(res)
} )
}, [])
return (
// use data state to show the data here
<div>comp</div>
)
}
export default comp;

React search using debounce

I am trying to implement a search that makes a new query on each character change. After n milliseconds, I need to make a change to the object that stores some properties.
//user typing
const onInputChange = (e) => {
let searchInput = e.target.value;
useDebounce(
handleSearchPropsChange({
filter: {
searchInput,
dateRange: {
start,
end
}
}
}), 1000
);
}
The function I am using for the delayed call
import {debounce} from 'lodash';
import {useRef} from 'react';
export function useDebounce(callback = () => {}, time = 500) {
return useRef(debounce(callback, time)).current;
}
But I am getting the error:
Invalid hook call. Hooks can only be called inside of the body of a function component. This
could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
A example without lodash, just Hooks.
UseDebounce.js
import { useEffect, useCallback } from 'react';
export default function useDebounce(effect, dependencies, delay) {
const callback = useCallback(effect, dependencies);
useEffect(() => {
const timeout = setTimeout(callback, delay);
return () => clearTimeout(timeout);
}, [callback, delay]);
}
App.js
import React, { useState } from 'react';
import useDebounce from './useDebounce';
import data from './data';
export default function App() {
const [search, setSearch] = useState('');
const [filteredTitle, setFilteredTitle] = useState([]);
// DeBounce Function
useDebounce(() => {
setFilteredTitle(
data.filter((d) => d.title.toLowerCase().includes(search.toLowerCase()))
);
}, [data, search], 800
);
const handleSearch = (e) => setSearch(e.target.value);
return (
<>
<input
id="search"
type="text"
spellCheck="false"
placeholder="Search a Title"
value={search || ''}
onChange={handleSearch}
/>
<div>
{filteredTitle.map((f) => (
<p key={f.id}>{f.title}</p>
))}
</div>
</>
);
}
Demo : Stackblitz

How to properly mock `antd` Upload Dragger with jest?

I have the following react code in my project
import React from 'react';
import { Upload } from 'antd';
const { Dragger } = Upload;
...
<Dragger
accept={ACCEPTED_FORMATS}
beforeUpload={beforeUpload}
data-testid="upload-dragger"
maxCount={1}
onChange={({ file: { status } }) => {
if (status === 'done') onUploadComplete();
}}
progress={progress}
showUploadList={false}
>
{/* here i have a button, code ommited for clarity, if needed i'll post it */}
</Dragger>
And I want to test if the callback function onUploadComplete() was called when file.status is 'done'.
Here is how i am doing the test right now:
I have a jest.mock to simulate a dumb request that will always succeed
import React from 'react';
import { fireEvent, render, waitFor } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { DraggerProps } from 'antd/lib/upload';
import userEvent from '#testing-library/user-event';
import UploadCompanyLogo from '../UploadCompanyLogo'; // This is the component where the dragger is placed
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
console.log('log test');
return (
<Dragger
{...props}
action="greetings"
customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0);
}}
/>
);
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
The test itself (same file as the mock), where it renders the component (where antd will be imported), simulate an upload and then checks if the callback has been called.
it('completes the image upload', async () => {
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(elementRenderer({ onUploadComplete }));
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
const uploadDragger = await waitFor(() => getByTestId('upload-dragger'));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalledTimes(1);
expect(onUploadComplete).toHaveBeenCalledWith();
});
elementRenderer
const elementRenderer = ({
onUploadComplete = () => {}
}) => (
<ApplicationProvider>
<UploadCompanyLogo
onUploadComplete={onUploadComplete}
/>
</ApplicationProvider>
);
ApplicationProvider
import React, { PropsWithChildren } from 'react';
import { ConfigProvider as AntdProvider } from 'antd';
import { RendererProvider as FelaProvider } from 'react-fela';
import { createRenderer } from 'fela';
import { I18nextProvider } from 'react-i18next';
import antdExternalContainer, {
EXTERNAL_CONTAINER_ID,
} from 'src/util/antdExternalContainer';
import { antdLocale } from 'src/util/locales';
import rendererConfig from 'src/fela/felaConfig';
import i18n from 'src/i18n';
const ApplicationProvider = (props: PropsWithChildren<{}>) => {
const { children } = props;
return (
<AntdProvider locale={antdLocale} getPopupContainer={antdExternalContainer}>
<FelaProvider renderer={createRenderer(rendererConfig)}>
<I18nextProvider i18n={i18n}>
<div className="antd-local">
<div id={EXTERNAL_CONTAINER_ID} />
{children}
</div>
</I18nextProvider>
</FelaProvider>
</AntdProvider>
);
};
export default ApplicationProvider;
This is currently not working, but have already been improved with the help of #diedu.
The console.log() I have put in the MockedDragger it's currently not showing. If I put a console.log() in both component and mockedDragger, it prints the component log.
Any tips on how to proceed?
I have already seen this issue and didn’t help.
The first thing you should change is the return you are doing in your mock. You are returning a new component called MockedDragger, not Dragger.
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
The next thing is the event firing. According to this issue you should use RTL user-event library and wrap the call in act
import userEvent from "#testing-library/user-event";
...
await act(async () => {
userEvent.upload(uploadDragger, file);
});
...
and finally, due to some asynchronous code running you need to flush the pending promises before checking the call
const flushPromises = () => new Promise(setImmediate);
...
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled()
this is the full version
import { render, waitFor } from "#testing-library/react";
import App from "./App";
import userEvent from "#testing-library/user-event";
import { act } from "react-dom/test-utils";
import { DraggerProps } from "antd/lib/upload";
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
return <Dragger {...props} customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0)
}} />;
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
it("completes the image upload", async () => {
const onUploadComplete = jest.fn();
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(
<App onUploadComplete={onUploadComplete} />
);
const file = new File(["(⌐□_□)"], "chucknorris.png", { type: "image/png" });
const uploadDragger = await waitFor(() => getByTestId("upload-dragger"));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled();
});
Unfortunately, I couldn't set up a codesandbox to show it's working, but let me know if you face any issue and I can push the code to a repo.

React Hooks/Context & Elastictic UI. Problem with fetched data (REST) in function Component

I'm quite new to React Hooks/Context so I'd appreciate some help. Please don' t jump on me with your sharp teeth. I Checked other solutions and some ways i've done this before but can't seem to get it here with the 'pick from the list' way.
SUMMARY
I need to get the municipios list of names inside of my const 'allMunicipios'(array of objects) inside of my Search.js and then display a card with some data from the chosen municipio.
TASK
Get the data from eltiempo-net REST API.
Use Combobox async element from Elastic UI to choose from list of municipios.
Display Card (from elastic UI too) with some info of chosen municipio.
It has to be done with function components / hooks. No classes.
I'd please appreciate any help.
WHAT I'VE DONE
I've created my reducer, context and types files in a context folder to fecth all data with those and then access data from the component.
I've created my Search.js file. Then imported Search.js in App.js.
I've accesed the REST API and now have it in my Search.js
PROBLEM
Somehow I'm not beeing able to iterate through the data i got.
Basically i need to push the municipios.NOMBRE from api to the array const allMunicipios in my search.js component. But when i console log it it gives me undefined. Can;t figure out why.
I'll share down here the relevant code/components. Thanks a lot for whoever takes the time.
municipiosReducer.js
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
export default (state, action) => {
switch (action.type) {
case SEARCH_MUNICIPIOS:
return {
...state,
municipios: action.payload,
};
case GET_MUNICIPIO:
return {
...state,
municipio: action.payload,
};
case CLEAR_MUNICIPIOS:
return {
...state,
municipios: [],
};
case GET_WEATHER: {
return {
...state,
weather: action.payload,
};
}
default:
return state;
}
};
municipiosContext.js
import { createContext } from "react";
const municipiosContext = createContext();
export default municipiosContext;
MunicipiosState.js
import React, { createContext, useReducer, Component } from "react";
import axios from "axios";
import MunicipiosContext from "./municipiosContext";
import MunicipiosReducer from "./municipiosReducer";
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
const MunicipiosState = (props) => {
const initialState = {
municipios: [],
municipio: {},
};
const [state, dispatch] = useReducer(MunicipiosReducer, initialState);
//Search municipios
//In arrow functions 'async' goes before the parameter.
const searchMunicipios = async () => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios`
// 08 means barcelona province. This should give me the list of all its municipios
);
dispatch({
type: SEARCH_MUNICIPIOS,
payload: res.data.municipios,
});
};
//Get Municipio
const getMunicipio = async (municipio) => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios/${municipio.CODIGOINE}`
//CODIGOINE is in this REST API kind of the ID for each municipio.
//I intent to use this later to get the weather conditions from each municipio.
);
dispatch({ type: GET_MUNICIPIO, payload: res.municipio });
};
const dataMunicipiosArray = [searchMunicipios];
//Clear Municipios
const clearMunicipios = () => {
dispatch({ type: CLEAR_MUNICIPIOS });
};
return (
<MunicipiosContext.Provider
value={{
municipios: state.municipios,
municipio: state.municipio,
searchMunicipios,
getMunicipio,
clearMunicipios,
dataMunicipiosArray,
}}
>
{props.children}
</MunicipiosContext.Provider>
);
};
export default MunicipiosState;
Search.js
import "#elastic/eui/dist/eui_theme_light.css";
import "#babel/polyfill";
import MunicipiosContext from "../contexts/municipiosContext";
import MunicipiosState from "../contexts/MunicipiosState";
import { EuiComboBox, EuiText } from "#elastic/eui";
import React, { useState, useEffect, useCallback, useContext } from "react";
const Search = () => {
const municipiosContext = useContext(MunicipiosContext);
const { searchMunicipios, municipios } = MunicipiosState;
useEffect(() => {
return municipiosContext.searchMunicipios();
}, []);
const municipiosFromContext = municipiosContext.municipios;
const bringOneMunicipio = municipiosContext.municipios[0];
let municipiosNames = municipiosFromContext.map((municipio) => {
return { label: `${municipio.NOMBRE}` };
});
console.log(`municipiosFromContext`, municipiosFromContext);
console.log(`const bringOneMunicipio:`, bringOneMunicipio);
console.log(`municipiosNames:`, municipiosNames);
const allMunicipios = [
{ label: "santcugat" },
{ label: "BARCELONETA" },
{ label: "BARCE" },
];
const [selectedOptions, setSelected] = useState([]);
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState([]);
let searchTimeout;
const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};
// combo-box
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, []);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
return (
<div>
<EuiComboBox
placeholder="Search asynchronously"
async
options={options}
selectedOptions={selectedOptions}
isLoading={isLoading}
onChange={onChange}
onSearchChange={onSearchChange}
/>
<button>Lista de municipios</button>
</div>
);
};
export default Search;
also the
Home.js
import React, { useState } from "react";
import { EuiComboBox, EuiText } from "#elastic/eui";
// import { DisplayToggles } from "../form_controls/display_toggles";
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import Search from "./Search";
import MunicipioCard from "./MunicipioCard";
const Home = () => {
return (
<div>
<EuiText grow={false}>
<h1>Clima en la provincia de Barcelona</h1>
<h2>Por favor seleccione un municipio</h2>
</EuiText>
<Search />
<MunicipioCard />
</div>
);
};
export default Home;
App.js
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import { EuiText } from "#elastic/eui";
import React from "react";
import Home from "./components/Home";
import MunicipiosState from "./contexts/MunicipiosState";
import "./App.css";
function App() {
return (
<MunicipiosState>
<div className="App">
<EuiText>
<h1>App Component h1</h1>
</EuiText>
<Home />
</div>
</MunicipiosState>
);
}
export default App;
You are using forEach and assigning the returned value to a variable, however forEach doesn't return anything. You should instead use map
let municipiosNames = municipiosFromContext.map((municipio) => {
return `label: ${municipio.NOMBRE}`;
});
As per your comment:
you data is loaded asynchronously, so it won't be available on first render and since functional components depend on closures, you onSearchChange function takes the value from the closure at the time of creation and even if you have a setTimeout within it the updated value won't reflect
The solution here is to add municipiosFromContext as a dependency to useEffect
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, [municipiosFromContext]);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);

Custom hook error: Hooks can only be called inside of the body of a function component

I am trying to develop a custom hook which seems to be pretty easy but I am getting an error
Uncaught Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
This is my hook:
import React, { useState, useEffect } from 'react';
const useInfiniteScroll = (isLastPage: boolean, fetchFn: any) => {
const [pageCount, setPageCount] = useState(0);
const triggerFetchEvents = (): void => {
let response;
setPageCount(() => {
if (!isLastPage) {
response = fetchFn(pageCount + 1, 5, 'latest');
}
return pageCount + 1;
});
return response;
};
useEffect(() => {
triggerFetchEvents();
}, []);
return pageCount;
};
export default useInfiniteScroll;
And the component here I am calling it:
import React, { FC } from 'react';
import { connect } from 'react-redux';
import { fetchEvents } from '../../shared/actions/eventActions';
import { AppState } from '../../shared/types/genericTypes';
import EventModel from '../../shared/models/Event.model';
import EventListPage from '../../components/events/EventListPage';
import useInfiniteScroll from '../../shared/services/triggerInfiniteScroll';
type Props = {
fetchEvents?: any;
isLastPage: boolean;
eventsList?: EventModel[];
};
const mapState: any = (state: AppState, props: Props): Props => ({
eventsList: state.eventReducers.eventsList,
isLastPage: state.eventReducers.isLastPage,
...props
});
const actionCreators = {
fetchEvents
};
export const EventsScene: FC<Props> = props => {
const { eventsList, fetchEvents, isLastPage } = props;
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();
// const [pageCount, setPageCount] = useState(0);
// const triggerFetchEvents = (): void => {
// let response;
// setPageCount(() => {
// if (!isLastPage) {
// response = fetchEvents(pageCount + 1, 1, 'latest');
// }
// return pageCount + 1;
// });
// return response;
// };
// useEffect(() => {
// triggerFetchEvents();
// }, []);
if (!eventsList || !eventsList.length) return null;
return (
<EventListPage
eventsList={eventsList}
isLastPage={isLastPage}
triggerFetchEvents={useIn}
/>
);
};
export default connect(
mapState,
actionCreators
)(EventsScene);
I left the commented code there to show you that if I uncomment the code and remove useInfiniteScroll then it works properly.
What could I be missing?
UPDATE:
This is EventListPage component
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import EventModel from '../../shared/models/Event.model';
import { formatDate } from '../../shared/services/date';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Card from 'react-bootstrap/Card';
type Props = {
eventsList?: EventModel[];
isLastPage: boolean;
triggerFetchEvents: any;
};
export const EventListPage: React.FC<Props> = props => {
const { eventsList, triggerFetchEvents, isLastPage } = props;
const [isFetching, setIsFetching] = useState(false);
const fetchMoreEvents = (): Promise<void> =>
triggerFetchEvents().then(() => {
setIsFetching(false);
});
const handleScroll = (): void => {
if (
document.documentElement.offsetHeight -
(window.innerHeight + document.documentElement.scrollTop) >
1 ||
isFetching
) {
return;
}
return setIsFetching(true);
};
useEffect(() => {
if (isFetching) return;
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
if (!isFetching) return;
if (!isLastPage) fetchMoreEvents();
}, [isFetching]);
if (!eventsList) return null;
return (
<Container className='article-list mt-5'>
///...
</Container>
);
};
export default EventListPage;
In EventsScene, change useInfiniteScroll to be invoked directly at the function body top-level (not sure why you are creating this indirection in the first place):
// before
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();
// after
useInfiniteScroll(isLastPage, fetchEvents)
React expects Hook calls to only happen at the top-level as it relies on the order of Hooks to be always the same. If you wrap the Hook in a function, you can potentially invoke this function in many code locations disturbing the Hooks' order.
There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state. Link

Categories