So I am trying to store a global state using context to allow me to use the same state across different components.
The issue I am having is that when I set the global state in 1 component and try to access it in the other component to use the state. It appears to be null and I cannot figure out why?
The first component where I set the global state in will always be rendered before the component shown that seems to have an empty value for the global state.
GlobalStateProvider component:
import React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
const defaultActivitiesState = [];
const globalStateContext = React.createContext(defaultActivitiesState);
const dispatchStateContext = React.createContext([]);
export const useGlobalState = () =>
[
React.useContext(globalStateContext),
React.useContext(dispatchStateContext)
];
const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = React.useReducer((state, newValue) => (state, newValue),
defaultActivitiesState
);
return (
<globalStateContext.Provider value={state}>
<dispatchStateContext.Provider value={dispatch}>
{children}
</dispatchStateContext.Provider>
</globalStateContext.Provider>
);
}
export default GlobalStateProvider;
Component I set the global state in:
import react from "react";
import { useState, useEffect, useMemo } from "react";
import { MapContainer, TileLayer, Popup, Polyline } from "react-leaflet";
import axios from "axios";
import polyline from "#mapbox/polyline";
import MapComp from "./MapComp";
import { useGlobalState } from "./GlobalStateProvider";
function Map() {
// ------- global state
const [activities, setActivities] = useGlobalState(); // global state
//const [activities, setActivities] = useState([]);
//const [polylines, setPolylines] = useState(null); // as empty array value is still truthy
const [isLoading, setIsLoading] = useState(true);
const [mapMode, setMapMode] = useState("light");
const [mapStyle, setMapStyle] = useState(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
);
const [showMarkers, setShowMarkers] = useState(true);
useEffect(() => {
setActivitieData();
console.log("activities after useEffect", activities)
}, []);
const polylines = useMemo(() => {
console.log("activities inside memo", activities)
console.log("activities.len =", activities.length);
if (activities.length) {
console.log("past len");
const polylineArray = [];
for (const item of activities) {
const polylineData = item.map.summary_polyline;
const activityName = item.name;
const activityType = item.type;
polylineArray.push({
positions: polyline.decode(polylineData),
name: activityName,
activityType: activityType,
});
}
setIsLoading(false);
return polylineArray;
}
return null;
}, [activities]);
const toggleMarkers = () => {
setShowMarkers((show) => !show);
};
const getActivityData = async () => {
console.log("calling")
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
console.log("Global activities state = ", activities);
};
return !isLoading && polylines ? (
<>
<div className="select-container">
<button className="toggle-markers" onClick={() => toggleMarkers()}>
Toggle Markers
</button>
</div>
<MapComp
className={`${mapMode}`}
activityData={{ polylines }}
showMarkers={showMarkers}
/>
</>
) : (
<div>
<p>Loading...</p>
</div>
);
}
export default Map;
component that has an empty value for global state:
import React from 'react';
import { useGlobalState } from './GlobalStateProvider';
function ActivityList() {
const [activities, setActivities] = useGlobalState();
let displayValues;
displayValues =
activities.map((activity) => {
return (
<div>
<p>{activity.name}</p>
<p>{activity.distance}m</p>
</div>
);
})
return (
<>
<p>Values</p>
{displayValues}
</>
);
}
export default ActivityList;
App.js:
function App() {
return (
<GlobalStateProvider>
<div className="App">
<NavBar />
<AllRoutes />
</div>
</GlobalStateProvider>
);
}
export default App;
I used this code to show the id of a product.
import { useState } from 'react';
import axios from 'axios';
const BASE_URL = "https://makeup-api.herokuapp.com/api/v1/products";
const useGetProduct = () => {
const [products, setProducts] = useState([]);
const [singleProduct, setSingleProduct] = useState(null);
const getTopProducts = () => {
axios.get(`${BASE_URL}.json`, {
params: {
product_tags: 'Canadian',
},
})
.then(Response => setProducts(Response.data));
};
const getSingleProduct = () => {
axios.get(`${BASE_URL}/1048.jason`)
.then(Response => setSingleProduct(Response.data));
};
return {
products,
getTopProducts,
singleProduct,
getSingleProduct,
}
};
export default useGetProduct;
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import useGetProduct from "../hooks/useGetProduct";
const Product = () => {
const { id } = useParams();
const { singleProduct, getSingleProduct } = useGetProduct();
useEffect(() => {
getSingleProduct();
}, []);
return (
<div>
<p>Product: {id}</p>
</div>
);
};
export default Product;
app.js
<Route exact path="/product/:id" element={<Product />} />
But when I change <p>Product: {id}</p> to <p>Product: {singleProduct?.name}</p> the product name does not display. Instead, it just shows Product: without the details about the product on localhost and I am not sure why.
const Product = () => {
const { id } = useParams();
const { singleProduct, getSingleProduct } = useGetProduct();
useEffect(() => {
getSingleProduct();
}, []);
return (
<div>
<p>Product: {singleProduct?.name}</p>
</div>
);
};
export default Product;
I want to show details about a single product via a hook in react js.
const { id } = useParams();
const { singleProduct, getSingleProduct } = useGetProduct();
useEffect(() => {
getSingleProduct();
}, []);
You are not using the id from useParams. Shouldn't you pass it to getSingleProduct, I assume that singleProduct is undefined.
So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example
I am using React-Data-Grid to show my data in the table, but the copy functionality is not working.
Below is my Grid Component in React, In which I have connectd it to the Back end API as well as with React Redux Store.
import React, { Component, useState, useEffect } from "react";
import { connect } from "react-redux";
import ApiHelper from "../../api/ApiHelper";
import ReactDataGrid from "react-data-grid";
import { Toolbar, Data, Filters } from "react-data-grid-addons";
import JarvisSpinner from "../presentationalComponents/JarvisSpinner";
import { withRouter } from "react-router-dom";
import "../../css/styles.css";
import "../../css/JarvisGrid.css";
const JarvisGrid = props => {
const [pagesize, setPageSize] = useState(15);
const [data, setdata] = useState([]);
const [filters, setFilters] = useState({
filterfield: "Application Send",
filtervalue: "Base"
});
const [sort, setSort] = useState({
Field: "PublicOrderNumber",
Direction: "desc"
});
const [loading, setloading] = useState(true);
useEffect(() => {
//Set data
ReloadData();
}, [props.uid, props.CalendarDates.fromDate, props.CalendarDates.toDate]);
const ReloadData = () => {
ApiHelper.GetGridQueryResult(
props.uid,
props.filterField,
props.filterValue,
props.CalendarDates.fromDate,
props.CalendarDates.toDate
)
.then(response => {
console.log("response");
console.log(response);
console.log(response.status);
console.log("Checking the data");
console.log(response.data.responses);
setdata(response.data.responses);
setloading(false);
})
.catch(error => {
setdata([]);
switch (error.response.status) {
case 403:
console.log("Error code --> " + 403);
props.history.push("/unAuthorizedPage");
break;
default:
console.log("Error String --->" + error);
}
});
};
return (
<>
{loading ? (
<JarvisSpinner size="3x" />
) : (
<div className="JarvisGrid">
<ReactDataGrid
columns={props.columns}
rowGetter={i => data[i]}
rowsCount={data.length}
minHeight={500}
uid={props.uid}
enableRowSelect={null}
enableCellSelect
toolbar={<Toolbar enableFilter="true" />}
onAddFilter={filter => {
setFilters({
filterfield: filter.column.key,
filtervalue: filter.filterTerm
});
}}
onClearFilters={() =>
setFilters({
filterfield: "OrderType",
filtervalue: "Base"
})
}
onGridSort={(sortColumn, sortDirection) => {
setSort({ Field: sortColumn, Direction: sortDirection });
}}
/>
</div>
)}
</>
);
};
function mapStateToProps(state) {
return {
CalendarDates: state.calendarDates
};
}
export default withRouter(connect(mapStateToProps)(JarvisGrid));
Let me know if I need to pass any props to the React-data-grid that I might have missed
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} />
}
);
};