REST API Response Flickering on Page Load - javascript

I've got component that displays contact information from a dealer as chosen by the user. To be more specific, a user selects their location, setting a cookie which then is used to define the API call. I pull in the contact information of the dealer in that location using Axios, store it in a context, and then display the information as necessary through several components: the header, a "current location" component etc. However, I'm having an issue with the content flickering each time the page is refreshed.
I've tried storing the JSON response in local storage, but, for a brief moment on page load, it shows as undefined, making the flicker continue. Obviously, I'm needing to eliminate that so that the data persists.
I've got it working via ApiContext, and I'm displaying the data in my Header component. Below is the code for both:
ApiContext.tsx
import React, { createContext, useEffect, useState } from 'react';
import axios from 'axios';
const contextObject = {} as any;
export const context = createContext(contextObject);
export const ApiContext = ({ children }: any) => {
const [selectedDealer, setselectedDealer] = useState(`1`);
useEffect(() => {
axios
.get(`${process.env.GATSBY_API_ENDPOINT}/${selectedDealer}`)
.then((response) => setselectedDealer(response.data));
}, [selectedDealer]);
const changeDealer = (id: any) => {
setselectedDealer(id);
};
const { Provider } = context;
return (
<Provider value={{ data: selectedDealer, changeDealer: changeDealer }}>
{children}
</Provider>
);
};
Header.tsx
import React, { ReactNode, useContext, useEffect, useState } from 'react';
import Logo from 'assets/svg/logo.svg';
import css from 'classnames';
import { Button } from 'components/button/Button';
import { Link } from 'components/link/Link';
import { MenuIcon } from 'components/menu-icon/MenuIcon';
import { context } from 'contexts/ApiContext';
import { NotificationBar } from '../notification-bar/NotificationBar';
import s from './Header.scss';
import { MainNav } from './navigation/MainNav';
interface HeaderProps {
navigationContent: ReactNode;
}
export const Header = ({ navigationContent }: HeaderProps) => {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const data = useContext(context);
const buttonLabel = data ? data.name : 'Find a Dealer';
const buttonLink = data ? `tel:${data.phone}` : '/find-a-dealer';
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 10;
if (isScrolled !== scrolled) {
setScrolled(!scrolled);
}
};
document.addEventListener('scroll', handleScroll, { passive: true });
return () => {
document.removeEventListener('scroll', handleScroll);
};
}, [scrolled]);
return (
<>
<NotificationBar notificationContent={navigationContent} />
<header className={scrolled ? css(s.header, s.header__scrolled) : s.header}>
<nav className={s.header__navigation}>
<ul className={s.header__container}>
<li className={s.header__logo}>
<Link to="/" className={s.header__link}>
<Logo />
</Link>
</li>
<li className={s.header__primary}>
<MainNav navigationItems={navigationContent} />
</li>
<li className={s.header__utility}>
<Button href={buttonLink}>{buttonLabel}</Button>
</li>
<li className={s.header__icon}>
<MenuIcon onClick={() => setOpen(!open)} />
</li>
</ul>
</nav>
</header>
</>
);
};
I would assume that this is because the API call is being triggered each time the page is refreshed, so I wonder if there's any way to persist the data in a more efficient way?
Thanks in advance!

Your ApiContext.tsxcould persist the data in localStorage is such a way:
import React, { createContext } from 'react';
import axios from 'axios';
import { makeUseAxios } from 'axios-hooks';
import { useCookie } from 'hooks/use-cookie';
const contextObject = {} as any;
export const context = createContext(contextObject);
const useAxios = makeUseAxios({
axios: axios.create({ baseURL: process.env.GATSBY_API_ENDPOINT }),
});
const loadData = (cookie) => {
const stored = localStorage.getItem("data");
const parsed = JSON.parse(stored);
// You can also store a lastSync timestamp along with the data, so that you can refresh them if necessary
if (parsed) return parsed;
const [{data}] = useAxios(`${cookie}`);
if (!isEqual(parsed, data)) {
localStorage.setItem('data', JSON.stringify(data));
}
return data
}
export const ApiContext = ({ children }: any) => {
const [cookie] = useCookie('one-day-location', '1');
const [{ data }] = loadData(cookie);
const { Provider } = context;
return <Provider value={data}>{children}</Provider>;
};
The above implementation will only fetch the data once, so remember to refresh them at some point inside your code and update the localStorage item, or use a timestamp to compare and force the api call as commented in my code.
Keep in mind that even this implementation may take a fraction of a second to be completed, so I would suggest to always use loaders/spinners/skeletons while your application is fetching the required data.

I got this worked out, using a hook that persists my state, storing it in a localStorage item.
usePersistState.ts
import { useEffect, useState } from 'react';
export const usePersistState = (key: string, defaultValue: string) => {
const [value, setValue] = useState(() => {
if (typeof window !== 'undefined') {
const stickyValue = window.localStorage.getItem(key);
return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
}
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Then, in ApiContext, I set my default state, but when that state changes, it updates and persists the state. Here's my context component now:
ApiContext.tsx
import React, { createContext, useEffect } from 'react';
import { usePersistState } from 'hooks/use-persist-state';
import axios from 'axios';
const contextObject = {} as any;
export const context = createContext(contextObject);
const LOCAL_STORAGE_KEY_SELECTED_DEALER = 'selectedDealerInformation';
export const ApiContext = ({ children }: any) => {
const [selectedDealer, setselectedDealer] = usePersistState(LOCAL_STORAGE_KEY_SELECTED_DEALER, '1');
useEffect(() => {
axios
.get(`${process.env.GATSBY_API_ENDPOINT}/${selectedDealer}`)
.then((response) => setselectedDealer(response.data));
}, [selectedDealer]);
const changeDealer = (id: any) => {
setselectedDealer(id);
};
localStorage.setItem(LOCAL_STORAGE_KEY_SELECTED_DEALER, JSON.stringify(selectedDealer));
const { Provider } = context;
return (
<Provider value={{ data: selectedDealer, changeDealer: changeDealer }}>{children}</Provider>
);
};

Related

useContext with a connect button

I am trying to make a connect button when the user clicks on it and connect he store the value inside a global state that i can use inside the whole application but this code doesn't work at all , why is it wrong ?
import React, { useState, useEffect } from 'react';
export const UserContexts = React.createContext();
const UserContext = ({children}) => {
const [getUser, setGetUser] = useState(null);
function connect() {
ethereum.request({ method : 'eth_requestAccounts'}).then(accounts => {
const account = accounts[0];
setGetUser(account)
})
}
useEffect(() => {
getUser ? null : connect();
},[])
const { Provider } = UserContexts;
return (
getUser ? <Provider value={getUser} >
{children}
</Provider>: null
)
}
export default UserContext
// navbar
import UserContext from './userContext'
<button onClick={UserContext.connect()} > connect </button>
when a user clicks on navbar connect button he login then when he login the _app saves the state globally so I can use it everywhere inside the app , I know this is a wrong syntax but how can I make it work ?
I solved this problem combined useContext and useReducer.
import React, {createContext,useContext,useEffect,useReducer} from "react";
const UserContext = createContext();
export function useBlockchainContext() {
return useContext(BlockchainContext);
}
function reducer(state, { type, payload }) {
return {
...state,
[type]: payload,
};
};
const init_state = {
user: ""
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, init_state);
return (
<BlockchainContext.Provider value={[state, dispatch]}>
{children}
</BlockchainContext.Provider>
)
}
// navbar
import { useBlockchainContext } from "../userContext";
export default function NavBar() {
const [state,dispatch] = useBlockchainContext();
const connect = ()=> {
ethereum.request({ method : 'eth_requestAccounts'}).then(accounts => {
const account = accounts[0];
dispatch({
type: "user",
payload: {
account
}
});
})
};
return(
<button onClick={()=>connect()} >{state.user !==""?state.user.slice(0, 4) + "..." + state.user.slice(-4):connect}</button>
)
}

React useEffect and React-Query useQuery issue?

I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.

Storing Axios response in AppContext

I'm trying to build a section of my site that will dynamically pull in contact information from a REST API. At this point I'm using the sample API from https://jsonplaceholder.typicode.com.
I'm trying to use axios, specifically the useAxios and makeUseAxios hooks, to fetch the API, and store it in an app context that I can then use throughout the site, preferably with the useContext hook. Additionally, I'm needing to be able to update the API call based on user interactions, allowing them to select a location, have the API update, and store that in the app's context so that it all updates dynamically.
Essentially, I've gotten a basic useContext scenario working based on this tutorial, but I am struggling with how to store the JSON response in such a way that I can reference it in useContext. Here's the closest thing I've had going so far:
AppContext.tsx
import React, { createContext, ReactNode } from 'react';
import axios from 'axios';
import { makeUseAxios } from 'axios-hooks';
import { useCookie } from 'hooks/use-cookie';
export const AppContext = createContext();
export const DealerContextProvider = ({children}: any) => {
const useAxios = makeUseAxios({
axios: axios.create({ baseURL: 'https://jsonplaceholder.typicode.com/users/' }),
});
const LOCAL_STORAGE_KEY_DEALER = '_selectedDealerInformation';
const [cookie] = useCookie('one-day-location', '1');
const [dealerInfo] = useAxios(`${cookie}`);
return (
<AppContext.Provider value={[dealerInfo]}>
{children}
</AppContext.Provider>
);
};
And my header component, where I'm trying to access it:
import React, { ReactNode, useEffect, useState, useContext } from 'react';
import { AppContext } from 'components/app-context/AppContext';
import Logo from 'assets/svg/logo.svg';
import css from 'classnames';
import { Button } from 'components/button/Button';
import { Link } from 'components/link/Link';
import { NotificationBar } from '../notification-bar/NotificationBar';
import s from './Header.scss';
import { MenuIcon } from 'components/menu-icon/MenuIcon';
import { MainNav } from './navigation/MainNav';
interface HeaderProps {
navigationContent: ReactNode;
}
export const Header = ({ navigationContent }: HeaderProps) => {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const blogInfo = useContext(AppContext);
const buttonLabel = blogInfo ? `${blogInfo.name}` : 'Find a Dealer';
const buttonLink = blogInfo ? `tel:${blogInfo.name}` : '/find-a-dealer';
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 10;
if (isScrolled !== scrolled) {
setScrolled(!scrolled);
}
};
document.addEventListener('scroll', handleScroll, { passive: true });
return () => {
document.removeEventListener('scroll', handleScroll);
};
}, [scrolled]);
return (
<>
<NotificationBar notificationContent={navigationContent} />
<header
className={scrolled ? css(s.header, s.header__scrolled) : s.header}
data-open={open ? 'true' : ''}
>
<nav className={s.header__navigation}>
<ul className={s.header__container}>
<li className={s.header__logo}>
<Link to="/" className={s.header__link}>
<Logo />
</Link>
</li>
<li className={s.header__primary}>
<MainNav navigationItems={navigationContent} />
</li>
<li className={s.header__utility}>
<Button href={buttonLink}>{buttonLabel}</Button>
</li>
<li className={s.header__burger}>
<MenuIcon onClick={() => setOpen(!open)} />
</li>
</ul>
</nav>
</header>
</>
);
};
What I'm needing is for the button in header__utility to dynamically display the name and phone number of the chosen dealer. I can clarify anything as needed, I'm newish to React and am still learning how to express all that I'm needing.
Thanks!
Alright, I did a ton of digging over the last 24 hours, and I was able to find a solution.
I've got my context, now named ApiContext for clarity.
ApiContext.tsx
import React, { createContext } from 'react';
import axios from 'axios';
import { makeUseAxios } from 'axios-hooks';
import { useCookie } from 'hooks/use-cookie';
const contextObject = {} as any;
export const context = createContext(contextObject);
const useAxios = makeUseAxios({
axios: axios.create({ baseURL: process.env.GATSBY_API_ENDPOINT }),
});
export const ApiContext = ({ children }: any) => {
const [cookie] = useCookie('one-day-location', '1');
const [{ data }] = useAxios(`${cookie}`);
const { Provider } = context;
return <Provider value={data}>{children}</Provider>;
};
Then, to use it across components, I wrap my AppLayout in <ApiContext>:
AppLayout.tsx
import React, { ReactNode } from 'react';
import { ApiContext } from 'contexts/ApiContext';
import { graphql, StaticQuery } from 'gatsby';
import { Devtools } from '../devtools/Devtools';
import { Footer } from '../footer/Footer';
import { Header } from '../header/Header';
import s from './AppLayout.scss';
interface AppLayoutProps {
children: ReactNode;
location: string;
}
const isDev = process.env.NODE_ENV === 'development';
// tslint:disable no-default-export
export default ({ children }: AppLayoutProps) => {
return (
<StaticQuery
query={`${NavQuery}`}
render={(data) => (
<>
<ApiContext>
<Header navigationContent={data.prismic.allNavigations.edges[0].node} />
<div className={s.layout}>
{children}
<Footer navigationItems={data.prismic.allNavigations.edges[0].node} />
{isDev && <Devtools />}
</div>
</ApiContext>
</>
)}
/>
);
};
const NavQuery = graphql`
query NavQuery {
prismic {
allNavigations {
edges {
node {
...NotificationBar
...NavigationItems
...FooterNavigationItems
}
}
}
}
}
`;
And in my Header component I can access the data with the useContext hook:
Header.tsx
import React, { ReactNode, useContext, useEffect, useState } from 'react';
import Logo from 'assets/svg/logo.svg';
import css from 'classnames';
import { Button } from 'components/button/Button';
import { Link } from 'components/link/Link';
import { MenuIcon } from 'components/menu-icon/MenuIcon';
import { context } from 'contexts/ApiContext';
import { NotificationBar } from '../notification-bar/NotificationBar';
import s from './Header.scss';
import { MainNav } from './navigation/MainNav';
interface HeaderProps {
navigationContent: ReactNode;
}
export const Header = ({ navigationContent }: HeaderProps) => {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const data = useContext(context);
console.log(data);
const buttonLabel = data ? data.name : 'Find a Dealer';
const buttonLink = data ? `tel:${data.phone}` : '/find-a-dealer';
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 10;
if (isScrolled !== scrolled) {
setScrolled(!scrolled);
}
};
document.addEventListener('scroll', handleScroll, { passive: true });
return () => {
document.removeEventListener('scroll', handleScroll);
};
}, [scrolled]);
return (
<>
<NotificationBar notificationContent={navigationContent} />
<header
className={scrolled ? css(s.header, s.header__scrolled) : s.header}
data-open={open ? 'true' : ''}
>
<nav className={s.header__navigation}>
<ul className={s.header__container}>
<li className={s.header__logo}>
<Link to="/" className={s.header__link}>
<Logo />
</Link>
</li>
<li className={s.header__primary}>
<MainNav navigationItems={navigationContent} />
</li>
<li className={s.header__utility}>
<Button href={buttonLink}>{buttonLabel}</Button>
</li>
<li className={s.header__burger}>
<MenuIcon onClick={() => setOpen(!open)} />
</li>
</ul>
</nav>
</header>
</>
);
};
I'm still working on additional optimizations, specifically eliminating a flicker when loading in a page, and also updating the API on a button click, utilizing useCookie, which is a custom hook I've built out. Hopefully this gives some clarity to anyone else searching for this info, it took me hours and hours to determine a solution. Cheers.

React context not updating

I have set a basic sample project that use Context to store the page title, but when I set it the component is not rerendered.
Principal files:
Context.js
import React from 'react'
const Context = React.createContext({})
export default Context
AppWrapper.js
import React from 'react'
import App from './App'
import Context from './Context'
function AppWrapper () {
return (
<Context.Provider value={{page: {}}}>
<App />
</Context.Provider>
)
}
export default AppWrapper
App.js
import React, { useContext } from 'react';
import Context from './Context';
import Home from './Home';
function App() {
const { page } = useContext(Context)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
);
}
export default App;
Home.js
import React, { useContext } from 'react'
import Context from './Context'
function Home () {
const { page } = useContext(Context)
page.title = 'Home'
return (
<p>Hello, World!</p>
)
}
export default Home
full code
What am I doing wrong?
Think about React context just like you would a component, if you want to update a value and show it then you need to use state. In this case your AppWrapper where you render the context provider is where you need to track state.
import React, {useContext, useState, useCallback, useEffect} from 'react'
const PageContext = React.createContext({})
function Home() {
const {setPageContext, page} = useContext(PageContext)
// essentially a componentDidMount
useEffect(() => {
if (page.title !== 'Home')
setPageContext({title: 'Home'})
}, [setPageContext])
return <p>Hello, World!</p>
}
function App() {
const {page} = useContext(PageContext)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
)
}
function AppWrapper() {
const [state, setState] = useState({page: {}})
const setPageContext = useCallback(
newState => {
setState({page: {...state.page, ...newState}})
},
[state, setState],
)
const getContextValue = useCallback(
() => ({setPageContext, ...state}),
[state, updateState],
)
return (
<PageContext.Provider value={getContextValue()}>
<App />
</PageContext.Provider>
)
}
Edit - Updated working solution from linked repository
I renamed a few things to be a bit more specific, I wouldn't recommend passing setState through the context as that can be confusing and conflicting with a local state in a component. Also i'm omitting chunks of code that aren't necessary to the answer, just the parts I changed
src/AppContext.js
export const updatePageContext = (values = {}) => ({ page: values })
export const updateProductsContext = (values = {}) => ({ products: values })
export const Pages = {
help: 'Help',
home: 'Home',
productsList: 'Products list',
shoppingCart: 'Cart',
}
const AppContext = React.createContext({})
export default AppContext
src/AppWrapper.js
const getDefaultState = () => {
// TODO rehydrate from persistent storage (localStorage.getItem(myLastSavedStateKey)) ?
return {
page: { title: 'Home' },
products: {},
}
}
function AppWrapper() {
const [state, setState] = useState(getDefaultState())
// here we only re-create setContext when its dependencies change ([state, setState])
const setContext = useCallback(
updates => {
setState({ ...state, ...updates })
},
[state, setState],
)
// here context value is just returning an object, but only re-creating the object when its dependencies change ([state, setContext])
const getContextValue = useCallback(
() => ({
...state,
setContext,
}),
[state, setContext],
)
return (
<Context.Provider value={getContextValue()}>
...
src/App.js
...
import AppContext, { updateProductsContext } from './AppContext'
function App() {
const [openDrawer, setOpenDrawer] = useState(false)
const classes = useStyles()
const {
page: { title },
setContext,
} = useContext(Context)
useEffect(() => {
fetch(...)
.then(...)
.then(items => {
setContext(updateProductsContext({ items }))
})
}, [])
src/components/DocumentMeta.js
this is a new component that you can use to update your page names in a declarative style reducing the code complexity/redundancy in each view
import React, { useContext, useEffect } from 'react'
import Context, { updatePageContext } from '../Context'
export default function DocumentMeta({ title }) {
const { page, setContext } = useContext(Context)
useEffect(() => {
if (page.title !== title) {
// TODO use this todo as a marker to also update the actual document title so the browser tab name changes to reflect the current view
setContext(updatePageContext({ title }))
}
}, [title, page, setContext])
return null
}
aka usage would be something like <DocumentMeta title="Whatever Title I Want Here" />
src/pages/Home.js
each view now just needs to import DocumentMeta and the Pages "enum" to update the title, instead of pulling the context in and manually doing it each time.
import { Pages } from '../Context'
import DocumentMeta from '../components/DocumentMeta'
function Home() {
return (
<>
<DocumentMeta title={Pages.home} />
<h1>WIP</h1>
</>
)
}
Note: The other pages need to replicate what the home page is doing
Remember this isn't how I would do this in a production environment, I'd write up a more generic helper to write data to your cache that can do more things in terms of performance, deep merging.. etc. But this should be a good starting point.
Here is a working version of what you need.
import React, { useState, useContext, useEffect } from "react";
import "./styles.css";
const Context = React.createContext({});
export default function AppWrapper() {
// creating a local state
const [state, setState] = useState({ page: {} });
return (
<Context.Provider value={{ state, setState }}> {/* passing state to in provider */}
<App />
</Context.Provider>
);
}
function App() {
// getting the state from Context
const { state } = useContext(Context);
return (
<>
<h1>Title: {state.page.title}</h1>
<Home />
</>
);
}
function Home() {
// getting setter function from Context
const { setState } = useContext(Context);
useEffect(() => {
setState({ page: { title: "Home" } });
}, [setState]);
return <p>Hello, World!</p>;
}
Read more on Hooks API Reference.
You may put useContext(yourContext) at wrong place.
The right position is inner the <Context.Provider>:
// Right: context value will update
<Context.Provider>
<yourComponentNeedContext />
</Context.Provider>
// Bad: context value will NOT update
<yourComponentNeedContext />
<Context.Provider>
</Context.Provider>

ReactJS: How to correctly pass variables with fetch results through context?

Need help, just started to learn React. I'm trying to pass variables with json data to a component for further use, but catching the errors. what should I change to use variables with json() data from Store.js in the product.js component? THanks for your time!
https://jsfiddle.net/constant101/xu7zdn26/3/ for better visibility
//Store export(receiving data from the server and assigning them to variables)
import React, {useState, useEffect} from 'react'
import axios from 'axios'
export const ListContext = React.createContext([]);
export const ItemContext = React.createContext([]);
function Store() {
const [storeProducts, setStoreProducts] = useState([]);
const [detailProduct, setDetailProduct] = useState([]);
useEffect(() => {
axios.get('/products/')
.then(res => {
console.log(res)
setStoreProducts(res.data)
})
},[])
console.log('storeProducts:', storeProducts)
useEffect(() => {
axios.get('/products/:productId')
.then(res => {
console.log(res)
setDetailProduct(res.data)
})
},[])
console.log('detail product:', detailProduct)
return (
<ListContext.Provider value={[storeProducts, setStoreProducts]}>
<ItemContext.Provider value={[detailProduct, setDetailProduct]}>
<product/>
</ItemContext.Provider>
</ListContext.Provider>
);
}
export const detailProduct
//product.js ( file that uses data from the fetch)
import React, { useReducer, createContext, useContext, useState } from 'react';
import {ListContext, ItemContext } from '../Store';
import { useProductActions } from '../actions';
import { SET_PRODUCT_DETAILS } from '../actions/types';
const [storeProducts] = useContext(ListContext);
const [detailProduct] = useContext(ItemContext);
let tempProducts = [];
storeProducts.forEach(item => tempProducts.push({ ...item })
);
const initialState = {
products: tempProducts,
productDetails: { ...detailProduct }
};
console.log(storeProducts)
const productReducer = (state, action) => {
switch (action.type) {
case SET_PRODUCT_DETAILS:
return {
...state,
productDetails: action.payload
};
default:
throw new Error('Invalid action type');
}
};
export const ProductContext = createContext(initialState);
export const useProductState = () => {
return useContext(ProductContext);
};
export const ProductProvider = ({ children }) => {
const [state, dispatch] = useReducer(productReducer, initialState);
const productActions = useProductActions(state, dispatch);
return (
<ProductContext.Provider value={{ productState: state, productActions }}>
{children}
</ProductContext.Provider>
);
};
Well, assuming your request is right, i saw a syntax mistake. You should pass
<ListContext.Provider value={{storeProducts, setStoreProducts}}> instead of
<ListContext.Provider value={[storeProducts, setStoreProducts]}>
The reason:
a provider requires a prop called value with an Object inside.
In that case, you were passing an array.
it would be the same if you did:
<ListContext.Provider
value={{
storeProducts: storeProducts,
setStoreProducts: setStoreProducts
}}
>
but to follow the DRY principle, it's recommended to do that way described earlier

Categories

Resources