I have the main page 'feed' where i used to have three functions, but I moved it into custom context. The console logs in context file output all the objects correctly, but nothing is visible in feed when i concole.log them.
context file:
import React, { useEffect, createContext, useContext, useState } from 'react'
import { useMemo } from 'react';
import getPosts from '../api/getPosts';
import filterImportedPosts from '../utils/filterImportedPosts';
export const ItemContext = createContext({
postData: {}, setPostData: () => { }
});
export const FilteredItemsContext = createContext({ filteredItems: [], setFilteredItems: () => { } })
export const FilterContext = createContext({ filter: '', setFilter: () => { } })
export function useItemContext() {
return useContext(ItemContext)
}
export function useFilteredItemsContext() {
return useContext(FilteredItemsContext)
}
export function useFilterContext() {
return useContext(FilterContext)
}
export default function PostProvider({ children }) {
const [postData, setPostData] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
const [filter, setFilter] = useState('');
useEffect(() => {
getPosts(setPostData)
console.log('postData: ', postData)
}, []);
useEffect(() => {
// console.log(filter);
const tempFiltItems = filterImportedPosts(postData, filter);
setFilteredItems(tempFiltItems);
console.log('tempFiltItems: ', filteredItems)
}, [filter, postData]);
const filteredItemsState = useMemo(() => {
return { filteredItems, setFilteredItems }
}, [filteredItems, setFilteredItems])
return (
<FilterContext.Provider value={{ filter, setFilter }}>
<FilteredItemsContext.Provider value={filteredItemsState}>
<ItemContext.Provider value={{ postData, setPostData }}>
{children}
</ItemContext.Provider>
</FilteredItemsContext.Provider >
</FilterContext.Provider>
)
}
and here the feed file:
import React, { useState, useEffect } from 'react';
import SinglePost from '../components/singlePost/singlePost';
import FilterPane from '../components/filterPane/filterPane.feedPost';
import { Box, Spinner, Text } from '#chakra-ui/react';
import getPosts from '../api/getPosts';
import Loader from '../../common/Loader';
import filterImportedPosts from '../utils/filterImportedPosts';
import PostProvider, { useFilterContext, useFilteredItemsContext, useItemContext } from './../context/PostDataContext';
export default function Feed() {
//-----------------IMPORT DATA FROM SERVER----------------------
const [error, setError] = useState(null);
const { postData, setPostData } = useItemContext();
const { filter, setFilter } = useFilterContext();
const { filteredItems, setFilteredItems } = useFilteredItemsContext();
useEffect(() => {
console.log(filteredItems)
}, [filteredItems, setFilteredItems])
// this helps while the data is loaded
// if (postData.length === 0) {
// return (
// <Box pos='absolute' top='45vh' left='40%'>
// <Loader />
// </Box>
// )
// }
// console.log(filteredItems);
return (
<PostProvider>
<Box mt={'7vh'} mb={'7vh'} ml={'3vw'} mr={'3vw'} zIndex={200}>
<FilterPane
setFilter={setFilter}
filter={filter}
filteredItems={filteredItems}
/>
{error && (
<div>Error occurred while loading profile info. Details: {error}</div>
)}
{!error && (
<>
{filteredItems.map((item, index) => {
return <SinglePost key={index} item={item} />;
})}
</>
)}
</Box>
</PostProvider>
);
}
console window printscreen. As you can see the filteredItems in context exist but nothing gets shown in the actual feed - the objects are empty. Could someone assist please?
I am pulling data from a crypto coin API. It has 250 coins data in one request. But if I load all of them, the data is not loaded and component tries to render which gives an error. I am following the regular practice of await and useEffect but still the error is persistent.
const Home = () => {
const [search, setSearch] = useContext(SearchContext);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const getCoinsData = async () => {
try {
const response = await Axios.get(
`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&per_page=100&page=1&sparkline=true&price_change_percentage=1h%2C24h%2C7d`
);
setData(response.data);
setLoading(false);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getCoinsData();
}, []);
const negStyle = {
color: "#D9534F",
};
const positiveStyle = {
color: "#95CD41",
};
return (
<div className="home">
<div className="heading">
<h1>Discover</h1>
<hr className="line" />
</div>
{!loading || data ? (
<div style={{ width: "100%", overflow: "auto" }}>
<table *the entire table goes here* />
</div>
) : (
<img className="loading-gif" src={Loading} alt="Loading.." />
)}
</div>
);
};
export default Home;
This is the entire code. Still when I try to refresh, it gives errors based on how much data loads. Sometimes, .map function is not defined or toFixed is defined etc. It does not keep loading till the whole data is loaded.
Can you show the errors and how did you initialize your state loading and data so we can debug better ?
Otherwise, what I usually do in this case is:
if (!loading && data) return <Table />;
return <img className="loading-gif" ... />;
import { QueryClient, QueryClientProvider, useQuery } from "react-query";
import axios from "axios";
import React from "react";
import { Image } from "#chakra-ui/image";
const Crypto = () => {
const { data, isLoading } = useQuery("crypto", () => {
const endpoint =
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&per_page=80&page=1&sparkline=true&price_change_percentage=1h%2C24h%2C7d";
return axios.get(endpoint).then(({ data }) => data);
});
return (
<>
{!isLoading && data ? (
data?.map((e, id) => <Image key={id} src={e.image} />)
) : (
<p>Loading</p>
)}
</>
);
};
export default function App() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Crypto />
</QueryClientProvider>
);
}
CodeSandBox Link, Preview
enter image description here
I have a React app which utilizes the context hook. The app functions properly but I am having difficulty writing passing tests.
My context looks like
import React, { createContext } from 'react';
const DataContext = createContext();
export const DataProvider = (props) => {
const [personSuccessAlert, setPersonSuccessAlert] = React.useState(false);
return (
<DataContext.Provider
value={{
personSuccessAlert,
setPersonSuccessAlert,
}}>
{props.children}
</DataContext.Provider>
);
};
export const withContext = (Component) => (props) => (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
The app uses this context in a useEffect hook
import React, { useEffect } from 'react';
import { Alert } from '../../../components/alert';
const PersonRecord = ({
match: {
params: { id },
},
setPersonSuccessAlert,
personSuccessAlert,
}) => {
const closeAlert = () => {
setTimeout(() => {
setPersonSuccessAlert(false);
}, 3000);
};
useEffect(() => {
closeAlert();
}, [location]);
return (
<>
<Alert
open={personSuccessAlert}
/>
</>
);
};
export default withContext(PersonRecord);
This all works as expected. When I run my tests I know I need to import the DataProvider and wrap the component but I keep getting an error.
test('useeffect', async () => {
const history = createMemoryHistory();
history.push('/people');
const setPersonSuccessAlert = jest.fn();
const { getByTestId, getByText } = render(
<DataProvider value={{ setPersonSuccessAlert }}>
<MockedProvider mocks={mocksPerson} addTypename={false}>
<Router history={history}>
<PersonRecord match={{ params: { id: '123' } }} />
</Router>
</MockedProvider>
</DataProvider>,
);
const alert = getByTestId('styled-alert');
await act(async () => new Promise((resolve) => setTimeout(resolve, 4000)));
});
There are a few different errors I get depending on how I change things up but the most common is
[TypeError: setPersonSuccessAlert is not a function]
I think my context is setup slightly different than others which is why I am having trouble using other methods found on here.
I am using useEffect to hit an api and display some data from the response.It works well in console but when i try to display the data in a component it throws an error.I am checking for the loading state though.I am showing the data after a i get a response then where does this null coming from
App.js file:
import { useState, useEffect } from 'react';
import Details from './components/Details/Details';
import Header from './components/Header/Header';
import GlobalStyle from './globalStyles';
const API_KEY = 'Private';
// const URL = `https://geo.ipify.org/api/v1?apiKey=${API_KEY}&ipAddress=${ip}`;
function App() {
const [ip, setIp] = useState('8.8.8.8');
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(
`https://geo.ipify.org/api/v1?apiKey=${API_KEY}&ipAddress=${ip}`
);
const json = await res.json();
setResponse(json);
setIsLoading(false);
} catch (error) {
setError(error);
}
};
fetchData();
// return { response, error, isLoading };
}, [ip]);
return (
<>
<GlobalStyle />
<Header getIp={(q) => setIp(q)} />
<Details isLoading={isLoading} res={response} error={error} />
</>
);
}
export default App;
Header.js file:
import { useState } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import React from 'react';
import { Form, FormInput, Head, HeadLine, Button } from './Header.elements';
// import { useFetch } from '../../useFetch';
const Header = ({ getIp }) => {
const [input, setInput] = useState('');
const onChange = (q) => {
setInput(q);
getIp(q);
};
return (
<>
{/* styled components */}
<Head>
<HeadLine>IP Address Tracker</HeadLine>
<Form
onSubmit={(e) => {
e.preventDefault();
onChange(input);
setInput('');
}}
>
<FormInput
value={input}
onChange={(e) => {
setInput(e.target.value);
}}
placeholder='Search for any IP address or Domain'
/>
<Button type='submit'>
<FaArrowRight />
</Button>
</Form>
</Head>
</>
);
};
export default Header;
Details.js file:
import React from 'react';
import { Box, Location } from './Details.elements';
const Details = ({ res, error, isLoading }) => {
console.log(res);
return isLoading ? (
<div>loading...</div>
) : (
<>
<Box>
<Location>{res.location.city}</Location>
</Box>
</>
);
};
export default Details;
the error it shows:
That happens because on the first render, Details component will receive isLoading=false and res=null, so it will try to render the box so it's throwing the error.
You can initialize isLoading as true.
const [isLoading, setIsLoading] = useState(true);
Or render the Location if res has some value.
<Box>
{res && <Location>{res.location.city}</Location>}
</Box>
According to React documentation :
https://reactjs.org/docs/hooks-reference.html
By default, effects run after every completed render, but you can
choose to fire them only when certain values have changed.
So your component is rendering at least once with isLoading as false before even the API call starts.
You have two choices here:
Set isLoading initial value to true
Add optional chaining res?.location.city
https://codesandbox.io/s/stackoverflow-67755606-uuhqk
I am using getServerSideProps in pages/post/index.js:
import React from "react";
import Layout from "../../components/Layout";
function Post({ post }) {
console.log("in render", post);
return (
<Layout title={post.name}>
<pre>{JSON.stringify(post, undefined, 2)}</pre>
</Layout>
);
}
export async function getServerSideProps({ query }) {
return fetch(
`${process.env.API_URL}/api/post?id=${query.id}`
)
.then(result => result.json())
.then(post => ({ props: { post } }));
}
export default Post;
When I directly load /post/2 it works as expected but when I go from /posts to /post/2 by clicking on a link:
<Link
as={`/post/${post.id}`}
href={`/post?id=${post.id}`}
>
It looks like nothing happens for 2 seconds (the api delay) and then the content shows. I can see in the network tab that _next/data/development/post/9.json is being loaded by fetchNextData.
I would like to show a loading spinner when I move from one route to another using next/Link but I can't find any documentation on getServerSideProps that allows me to do this.
When I directly go to /post/:id I'd like the data to be fetched server side and get a fully rendered page (works) but when I then move to another route the data should be fetched from the client (works). However; I would like to have a loading indicator and not have the UI freeze up for the duration of the data request.
Here is an example using hooks.
pages/_app.js
import Router from "next/router";
export default function App({ Component, pageProps }) {
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const start = () => {
console.log("start");
setLoading(true);
};
const end = () => {
console.log("finished");
setLoading(false);
};
Router.events.on("routeChangeStart", start);
Router.events.on("routeChangeComplete", end);
Router.events.on("routeChangeError", end);
return () => {
Router.events.off("routeChangeStart", start);
Router.events.off("routeChangeComplete", end);
Router.events.off("routeChangeError", end);
};
}, []);
return (
<>
{loading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}
You can use nprogress in your _app.js
import NProgress from 'nprogress';
import "nprogress/nprogress.css";
import Router from 'next/router';
NProgress.configure({
minimum: 0.3,
easing: 'ease',
speed: 800,
showSpinner: false,
});
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
or dynamic import to _app.js to reduce bundle size
ProgessBar.js
import Router from 'next/router';
import NProgress from 'nprogress';
import "nprogress/nprogress.css";
NProgress.configure({
minimum: 0.3,
easing: 'ease',
speed: 500,
showSpinner: false,
});
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
export default function () {
return null;
}
_app.js
import dynamic from 'next/dynamic';
const ProgressBar = dynamic(() => import('components/atoms/ProgressBar'), { ssr: false });
const App = () => {
...
return <>
...
<ProgressBar />
</>
}
Ps: If you want to change color of progress bar, you can override in global css, something like this
#nprogress .bar {
background: #6170F7 !important;
height: 3px !important;
}
You can create a custom hook:
usePageLoading.ts
import Router from 'next/router';
import { useEffect, useState } from 'react';
export const usePageLoading = () => {
const [isPageLoading, setIsPageLoading] = useState(false);
useEffect(() => {
const routeEventStart = () => {
setIsPageLoading(true);
};
const routeEventEnd = () => {
setIsPageLoading(false);
};
Router.events.on('routeChangeStart', routeEventStart);
Router.events.on('routeChangeComplete', routeEventEnd);
Router.events.on('routeChangeError', routeEventEnd);
return () => {
Router.events.off('routeChangeStart', routeEventStart);
Router.events.off('routeChangeComplete', routeEventEnd);
Router.events.off('routeChangeError', routeEventEnd);
};
}, []);
return { isPageLoading };
};
and then inside your App component use it:
_app.js
import Router from "next/router";
import { usePageLoading } from './usePageLoading';
export default function App({ Component, pageProps }) {
const { isPageLoading } = usePageLoading();
return (
<>
{isPageLoading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}
How about simply adding a component level loading state to Post (vs. adding a loader on App Level for every route change since some route changes might not require server side rendering).
Setting the isLoading state to true when the relevant query param changes, in this case the post id, and setting the state to false once the props, in this case the post data, updated.
Along these lines:
pages/post/index.js:
import React from "react";
import Layout from "../../components/Layout";
import { useRouter } from 'next/router';
function Post({ post }) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
// loading new post
useEffect(()=> {
setIsLoading(true);
}, [router.query?.id]);
// new post loaded
useEffect(()=> {
setIsLoading(false)
}, [post]);
return (
<>
{isLoading ? (
<h1>Loading...</h1>
) : (
<Layout title={post.name}>
<pre>{JSON.stringify(post, undefined, 2)}</pre>
</Layout>
)}
</>
);
}
export async function getServerSideProps({ query }) {
return fetch(
`${process.env.API_URL}/api/post?id=${query.id}`
)
.then(result => result.json())
.then(post => ({ props: { post } }));
}
export default Post;
Just adding to the previous answers, you can receive a url parameter in the event handlers, and use those to filter out which route you want a loading state and which not. Simple example in _app.js:
function MyApp({ Component, pageProps: { ...pageProps } }: AppProps) {
const router = useRouter();
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
const handleChangeStart = (url: string) => {
if (url === "<root_to_show_loading>") {
setIsLoading(true);
}
};
const handleChangeEnd = (url: string) => {
if (url === "<root_to_show_loading") {
setIsLoading(false);
}
};
router.events.on("routeChangeStart", handleChangeStart);
router.events.on("routeChangeComplete", handleChangeEnd);
router.events.on("routeChangeError", handleChangeEnd);
}, []);
return (
<main>
{isLoading ? <LoadingSpinner /> : <Component {...pageProps} />}
</main>
);
}
export default MyApp;
**Here is how I did it in NextJs with Material UI and nprogress**
import '../styles/globals.css';
import { useEffect, useState } from 'react';
import Router from 'next/router';
import NProgress from 'nprogress';
import { useStyles } from '../src/utils';
import { CircularProgress } from '#material-ui/core';
NProgress.configure({ showSpinner: false });
function MyApp({
Component,
pageProps
}) {
const classes = useStyles();
const [loading, setLoading] = useState(false);
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
const start = () => {
console.log('start');
NProgress.start();
setLoading(true);
};
const end = () => {
console.log('findished');
NProgress.done();
setLoading(false);
};
Router.events.on('routeChangeStart', start);
Router.events.on('routeChangeComplete', end);
Router.events.on('routeChangeError', end);
return () => {
Router.events.off('routeChangeStart', start);
Router.events.off('routeChangeComplete', end);
Router.events.off('routeChangeError', end);
};
}, []);
return (
<>
{loading ? (
<div className={classes.centered}>
<CircularProgress size={25} color='primary' />
</div>
) : (
<Component {...pageProps} />
)}
</>
);
}
export default MyApp;
Result:
Progress bar like NProgress in 90 lines of code (vs NProgress v0.2.0 is 470 lines .js + 70 lines .css):
import { useEffect, useReducer, useRef } from 'react';
import { assert } from './assert';
import { wait } from './wait';
import { getRandomInt } from './getRandomNumber';
let waitController: AbortController | undefined;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function useProgressBar({
trickleMaxWidth = 94,
trickleIncrementMin = 1,
trickleIncrementMax = 5,
dropMinSpeed = 50,
dropMaxSpeed = 150,
transitionSpeed = 600
} = {}) {
// https://stackoverflow.com/a/66436476
const [, forceUpdate] = useReducer(x => x + 1, 0);
// https://github.com/facebook/react/issues/14010#issuecomment-433788147
const widthRef = useRef(0);
function setWidth(value: number) {
widthRef.current = value;
forceUpdate();
}
async function trickle() {
if (widthRef.current < trickleMaxWidth) {
const inc =
widthRef.current +
getRandomInt(trickleIncrementMin, trickleIncrementMax); // ~3
setWidth(inc);
try {
await wait(getRandomInt(dropMinSpeed, dropMaxSpeed) /* ~100 ms */, {
signal: waitController!.signal
});
await trickle();
} catch {
// Current loop aborted: a new route has been started
}
}
}
async function start() {
// Abort current loops if any: a new route has been started
waitController?.abort();
waitController = new AbortController();
// Force the show the JSX
setWidth(1);
await wait(0);
await trickle();
}
async function complete() {
assert(
waitController !== undefined,
'Make sure start() is called before calling complete()'
);
setWidth(100);
try {
await wait(transitionSpeed, { signal: waitController.signal });
setWidth(0);
} catch {
// Current loop aborted: a new route has been started
}
}
function reset() {
// Abort current loops if any
waitController?.abort();
setWidth(0);
}
useEffect(() => {
return () => {
// Abort current loops if any
waitController?.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
start,
complete,
reset,
width: widthRef.current
};
}
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useProgressBar } from './useProgressBar';
const transitionSpeed = 600;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function RouterProgressBar(
props?: Parameters<typeof useProgressBar>[0]
) {
const { events } = useRouter();
const { width, start, complete, reset } = useProgressBar({
transitionSpeed,
...props
});
useEffect(() => {
events.on('routeChangeStart', start);
events.on('routeChangeComplete', complete);
events.on('routeChangeError', reset); // Typical case: "Route Cancelled"
return () => {
events.off('routeChangeStart', start);
events.off('routeChangeComplete', complete);
events.off('routeChangeError', reset);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return width > 0 ? (
// Use Bootstrap, Material UI, Tailwind CSS... to style the progress bar
<div
className="progress fixed-top bg-transparent rounded-0"
style={{
height: 3, // GitHub turbo-progress-bar height is 3px
zIndex: 1091 // $zindex-toast + 1 => always visible
}}
>
<div
className="progress-bar"
style={{
width: `${width}%`,
//transition: 'none',
transition: `width ${width > 1 ? transitionSpeed : 0}ms ease`
}}
/>
</div>
) : null;
}
How to use:
// pages/_app.tsx
import { AppProps } from 'next/app';
import Head from 'next/head';
import { RouterProgressBar } from './RouterProgressBar';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>My title</title>
<meta name="description" content="My description" />
</Head>
<RouterProgressBar />
<Component {...pageProps} />
</>
);
}
More here: https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
To add to the previous answers and show complete code, you can add a delay with setTimeout when setting state in the event hook to avoid a flicker of loading on fast loading routes (either static routes, or server routes ready to go).
import Router from 'next/router';
import { useEffect, useRef, useState } from 'react';
const usePageLoad = (delay = 200) => {
const timeoutRef = useRef();
const [loading, setLoading] = useState(false);
useEffect(() => {
const start = () => {
timeoutRef.current = window.setTimeout(() => {
setLoading(true);
}, delay);
};
const end = () => {
window.clearTimeout(timeoutRef.current);
setLoading(false);
};
Router.events.on('routeChangeStart', start);
Router.events.on('routeChangeComplete', end);
Router.events.on('routeChangeError', end);
return () => {
Router.events.off('routeChangeStart', start);
Router.events.off('routeChangeComplete', end);
Router.events.off('routeChangeError', end);
};
}, [delay]);
return loading;
};
export default usePageLoad;
Then use this hook in _app and adjust the delay as needed for your application.
import PageLoader from '../components/PageLoader';
import usePageLoad from '../components/use-page-load';
const App = ({ Component, pageProps }) => {
const loading = usePageLoad();
return (
{
loading
? <PageLoader />
: <Component {...pageProps} />
}
);
};