Use swr with Next global context: entire page gets re-rendered - reactjs

I have the following piece of code:
function MyApp({ Component, pageProps }: AppProps) {
const { tronLinkAuth, tronLinkLoading, mutateTronLink } = useTronLink();
const { authenticatedUser, authLoading, authLoggedOut, mutateAuth } = useAuthenticatedUser();
return (
<React.StrictMode>
<CSSReset />
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<ChakraProvider theme={theme}>
<AuthenticationContext.Provider value={{
tronLinkAuth, tronLinkLoading, mutateTronLink,
authenticatedUser, authLoading, authLoggedOut, mutateAuth
}}>
<Component {...pageProps} />
</AuthenticationContext.Provider>
</ChakraProvider>
</React.StrictMode>
)
}
Example useAuthenticatedUser:
export default function useAuthenticatedUser() {
const { data, mutate, error } = useSWR("api_user", fetcher, {
errorRetryCount: 0
});
const loading = !data && !error;
const loggedOut = error && error instanceof UnauthorizedException;
return {
authLoading: loading,
authLoggedOut: loggedOut,
authenticatedUser: data as AuthenticatedUser,
mutateAuth: mutate
};
}
The code works, but my entire webpage gets re-rendered when swr propagates its result.
For example:
const Login: NextPage = () => {
console.log('login update');
return (
<>
<Head>
<title>Register / Login</title>
</Head>
<Navbar />
<Box h='100vh'>
<Hero />
</Box>
<Box h='100vh' pt='50px'>
Test second page
</Box>
</>
)
}
export default Login;
When using useContext in the Navbar, it also re-renders the entire LoginPage, including the Hero, while this is not my purpose.
const Navbar: React.FC = () => {
const authState = useContext(AuthenticationContext);
...
I'm also confused as for why the logs appear in the server console, as this is supposed to be executed client-side.
Edit: not an issue, this is only on first render.
How to solve?
I'm interested in using swr for this use case, because it allows me to re-verify the authentication status e.g. on focus but use the cached data meanwhile.
Edit:
Confusing. The following log:
function MyApp({ Component, pageProps }: AppProps) {
console.log('app');
const { tronLinkAuth, tronLinkLoading, mutateTronLink } = useTronLink();
const { authenticatedUser, authLoading, authLoggedOut, mutateAuth } = useAuthenticatedUser();
Also gets printed out every time I switch tabs and activate the swr.
So it re-renders the entire tree? Doesn't seem desirable...

I currently went with the easier solution, i.e. use useSwr immediately on the component that uses the data.
From the docs:
Each component has a useSWR hook inside. Since they have the
same SWR key and are rendered at the almost same time, only 1 network
request will be made.
You can reuse your data hooks (like useUser in the example above)
everywhere, without worrying about performance or duplicated requests.
So it can be leveraged to re-use it wherever needed without having to worry about global state re-renders.
In case there is an alternative response how to use the global Context Provider, don't hesitate to share.

Related

React useContext, NextJS static page generation, and rendering

I'm using React useContext to avoid prop-drilling, and building static pages in NextJS, as described in this Technouz post (NB: this is not about the NextJS getStaticProps context parameter).
The basic functionality is working; however, I can't figure out the right way to update the context from components farther down the chain.
At a high level, I have this:
// pages/_app.js
function MyApp({ Component, pageProps }) {
const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} );
return (
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
// components/Header.js
export default function Header() {
const { headerData } = useHeader();
return (
<header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar />
</header>
)
}
// lib/context/header.js
const HeaderContext = createContext();
export function HeaderProvider({value, children}) {
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
The Navbar component also uses the context.
That all works. I query the data from a headless CMS using getStaticProps, and everything gets passed through pageProps, and when I run npm run build, I get all of my static pages with the appropriate headers.
But, now I'm extending things, and not all pages are the same. I use different models at the CMS level, and want to display different headers for landing pages.
Inside of [pages].js, I handle that thusly:
const Page = ({ data }) => {
switch (data.pageType) {
case 'landing-page':
return (
<PageLandingPage data={data} />
);
case 'page':
default:
return (
<PageStandard data={data} />
);
}
}
Now, if we're building a static landing page instead of a static standard page, the whole hierarchy would look something like this:
<HeaderProvider value={{ headerData, setHeaderData }}>
<Header>
{ headerData.urgentBanner && <UrgentBanner {...headerData.urgentBanner}/> }
<Navbar>
<ul>
{menu && <MenuList type='primary' menuItems={menu.menuItems} />}
</ul>
</Navbar>
</Header>
<PageLandingPage {...pageProps}> // *** Location 2
<LandingPageSection>
<Atf> // *** Location 1
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
</Atf>
</LandingPageSection>
</PageLandingPage>
</HeaderProvider>
Location 1 and Location 2 are where I want to update the context. I thought I had that working, by doing the following at Location 1:
// components/Atf.js
export default function Atf({content}) {
// this appeared to work
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
return (
<section>
{ socialProof && <SocialProof { ...socialProof } />}
<Attention { ...attentionDetails }/>
</section>
)
}
I say "thought", because I was, in fact, getting my <UrgentBanner> component properly rendered on the landing pages. However, when digging into the fact that I can't get it to work at Location 2, I discovered that I was actually getting warnings in the console about "cannot update a component while rendering a different component" (I'll come back to this).
Now to Location 2. I tried to do the same thing here:
// components/PageLandingPage.js
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// this, to me, seems the same as above, but isn't working at all
if (giveawayLandingPage?.headerMenu) {
const { headerData, setHeaderData } = useHeader();
setHeaderData(
{
...headerData,
menu: { ...giveawayLandingPage.headerMenu }
}
);
}
return (
<div>
{giveawayLandingPage.lpSection.map(section => <LandingPageSection details={section} key={section.id} />)}
</div>
)
}
To me, that appears that I'm doing the same thing that "worked" in the <Atf> component, but ... it's not working.
While trying to figure this out, I came across the aforementioned error in the console. Specifically, "Cannot update a component (MyApp) while rendering a different component (Atf)." And I guess this is getting to the heart of the problem — something about how/when/in which order NextJS does its rendering when it comes to generating its static pages.
Based on this answer, I initially tried wrapping the call in _app.js in a useEffect block:
// pages/_app.js
...
/* const [ headerData, setHeaderData ] = useState( {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
} ); */
const [ headerData, setHeaderData ] = useState({});
useEffect(() => {
setHeaderData({
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data?.siteBranding.siteName,
companyLogo: pageProps.data?.siteBranding.companyLogo,
menu: pageProps.data?.menu
});
}, []);
But that didn't have any impact. So, based on this other answer, which is more about NextJS, though it's specific to SSR, not initial static page creation, I also wrapped the setState call in the <Atf> component at Location 1 in a useEffect:
// components/Atf.js
...
const { headerData, setHeaderData } = useHeader();
/* setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
) */
useEffect(() => {
setHeaderData(
{
...headerData,
urgentBanner: content.find((record) => 'UrgentBannerRecord' === record?.__typename)
}
)
}, [setHeaderData])
That did stop the warning from appearing in the console ... but it also stopped the functionality from working — it no longer renders my <UrgentBanner> component on the landing page pages.
I have a moderately good understanding of component rendering in React, but really don't know what NextJS is doing under the covers when it's creating its initial static pages. Clearly I'm doing something wrong, so, how do I get my context state to update for these different types of static pages?
(I presume that once I know the Right Way to do this, my Location 2 problem will be solved as well).
I ended up fixing this by moving from useState to useReducer, and then setting all of the state, including the initial state, at the page level. Now, _app.js is simplified to
function MyApp({ Component, pageProps }) {
return (
<HeaderProvider>
<Header />
<Component {...pageProps} />
</HeaderProvider>
)
}
export default MyApp
And the context hook setup uses the reducer and provides it back to the provider:
// lib/context/header.js
const initialState = {};
const HeaderContext = createContext(initialState);
function HeaderProvider({ children }) {
const [headerState, dispatchHeader] = useReducer((headerState, action) => {
switch (action.type) {
case 'update':
const newState = { ...headerState, ...action.newState };
return newState;
default:
throw new Error('Problem updating header state');
}
}, initialState);
return (
<HeaderContext.Provider value={{ headerState, dispatchHeader }}>
{children}
</HeaderContext.Provider>
);
}
function useHeader() {
return useContext(HeaderContext);
}
export { HeaderProvider, useHeader }
Then, everywhere you want to either get the state or set the state, as long as you're inside of the <Provider>, you're good to go. This was a little confusing at first, because it's not obvious that when you useContext, what it's doing is returning the current value, and the value is provided both with the state, and with the dispatch function, so when you want to set something, you query the "value", but destructure to get the "setter" (i.e., the dispatch function).
So, for example, in my "location 2" from the initial question, it now looks like
import React, { useEffect } from 'react';
import { useHeader } from '../lib/context/header';
const PageLandingPage = ({ data }) => {
const giveawayLandingPage = data.giveawayLandingPage;
// here's where we get the "setter" through destructuring the `value`
// let foo = useHeader();
// console.log(foo);
// > { headerState, dispatchHeader }
const { dispatchHeader } = useHeader();
useEffect(() => {
dispatchHeader({
newState: {
menu: { ...giveawayLandingPage.headerMenu }
},
type: 'update'
});
}, []);
...

Problem with wrapping whole React App (all pages) in certain functionality

I have React App consisting of few pages. LandingPage (to be clear, it is really the first page in app flow)is like this:
export const LocalLandingPage = () => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return (...here goes actual content...);
};
const LandingPage = withRouter(wrappedInLinkToSearchHOC(WithSnackBarHOC(LocalLandingPage)));
Here I call hook useFavorites which gathers some methods dealing with a specific subset of local Storage content(and dispatches actions to Redux store as well). The code above return call is checks support for local Storage and tells if some specific items are stored and in what quantity.
The story is that I have realized that if someone enters not the LandingPage but any other page of the app, the code will not be executed and support for localStorage not checked.
Besides, it is really not LandingPage business to deal with storage, so it should be removed anywhere.
My idea was to write HOC and wrap the application. So, here is this HOC (to keep things simple it is initially JS not TS) To make useFavorites work, it had to be a hook, too.
import useFavorites from '../hooks/useFavorites';
const useCheckSupportForLocalStorage = Component => {
const { Favorites } = useFavorites();
// React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
// }, []);
return props => <Component {...props} />;
};
export default useCheckSupportForLocalStorage;
Having it done, I have tried to use it on App like this
function App() {
return (
<Switch>
... here are routes...
</Switch>
);
}
export default useCheckSupportForLocalStorage(App);
It throws an error:
React Hook "useCheckSupportForLocalStorage" cannot be called at the top level.
That is truth, no doubt. So, the next idea was to create a temporary component from all routes.
function Routes() {
const Routes = useCheckSupportForLocalStorage(
<>
<Route exact path={Paths.landing} component={Awaiting(StarWars)} />
...here is the rest or outes...
</>,
);
return <Routes />;
}
export default Routes;
And use it in rewritten App like this
function App() {
return (
<Switch>
<Routes />
</Switch>
);
}
but it throws error
Routes' cannot be used as a JSX component.
Its return type '(props: any) => Element' is not a valid JSX element.
Type '(props: any) => Element' is missing the following properties from type 'ReactElement<any, any>': type, props, key
Forcing useCheckSupportForLocalStorage th have return type of ReactElement doesn't help, just leads to other error. I have checked few others options as well. What is wrong, how should it be written?
Basically I could stop using useFavorites in this hook, then it would be just a function - but it would be extreme headache.
You say
To make useFavorites work, it had to be a hook, too.
That is not true, since you have it in a component it will work just fine. And my understanding is that you want to wrap your whole app with it, so no need for the HOC. Just use it at the top of your app hierarchy.
something like
import useFavorites from '../hooks/useFavorites';
const CheckSupportForLocalStorage = ({ children }) => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return children;
};
export default CheckSupportForLocalStorage;
and
function App() {
return (
<CheckSupportForLocalStorage>
<Switch>
... here are routes...
</Switch>
</CheckSupportForLocalStorage>
);
}
export default App;

How to share a single MUI useScrollTrigger return value among multiple components?

I am currently using MUI's useScrollTrigger hook to determine the appearance of three components - NavBar, a post FAB a back to top button e.g.:
export default function NavBar() {
const isScrolledDown = useScrollTrigger({ target: window, threshold: 100 });
return (
<>
<Slide in={!isScrolledDown} >
<AppBar>
<Toolbar>
</Toolbar>
</AppBar>
</Slide>
<Toolbar />
<BackToTopFAB isScrolledDown={isScrolledDown} />
<PostCreateFAB isScrolledDown={isScrolledDown} />
</>
);
}
Since I do not want to make the browser listen for three separate "scroll" events, I am currently drilling the hook's return value from the NavBar into the two buttons.
However, as a result, I am unable to decouple the two buttons from the NavBar.
Does anyone have any suggestions how this may be possible, so that all three components share the same hook return value? If having multiple "scroll" listeners is not DOM-intensive, I am also willing to consider that
React hook is designed to be reusable, you probably want to move the useScrollTrigger hook to the components that need it like below:
const useCustomScrollTrigger = () => useScrollTrigger({ target: window, threshold: 100 });
const BackToTopFAB = () => {
const isScrolledDown = useCustomScrollTrigger();
return (...)
}
const PostCreateFAB = () => {
const isScrolledDown = useCustomScrollTrigger();
return (...)
}
const MyAppBar = () => {
const isScrolledDown = useCustomScrollTrigger();
return (
<Slide in={!isScrolledDown} >
<AppBar />
</Slide>
)
}
export default function NavBar() {
return (
<>
<MyAppBar />
<OtherContent />
<BackToTopFAB />
<PostCreateFAB />
</>
);
}
Doing so has a couple of advantages:
Your code is easier to read because the logic is hidden away in each specific component. Code readability is one of the most important factors when choosing between trade-offs IMO. Several additional event listeners should never impact your application performance in any way.
Improve your the performance of the parent component since there is no props at the top-level component, if the isScrolledDown state is changed, only 3 isolated components are re-rendered as a result. Otherwise, other components in the page like OtherContent also need to be rendered because the state in the parent component changes.
You can also have a look at some react state management libraries like redux-toolkit if you want to store the state in a single place and access it anywhere in the components regardless of its position in the hierarchy:
import { createSlice } from '#reduxjs/toolkit'
const { actions } = createSlice({
name: 'globalState',
initialState: { isScrolledDown: false },
reducers: {
setIsScrolledDown: (state, action) => {
state.isScrolledDown = action.payload
},
},
})
const ScrollLisenter = () => {
const isScrolledDown = useScrollTrigger({ /* ... */ });
const dispatch = useDispatch()
useEffect(() => {
dispatch(actions.setIsScrolledDown(isScrolledDown));
}, [isScrolledDown]);
return null
}
const BackToTopFAB = () => {
const isScrolledDown = useSelector(state => state.globalState.isScrolledDown);
return (...)
}
const PostCreateFAB = () => {
const isScrolledDown = useSelector(state => state.globalState.isScrolledDown);
return (...)
}
<App>
<ScrollLisenter />
<NavBar />
</App>
Related Question
Does adding too many event listeners affect performance?

useReducer in Context consumer does not update after change in some locations

I added a context that contains a useReducer hook to my Ionic React app. I'm seeing some strange behavior: when I update the context value with a dispatch call, then a consumer component will be updated on the page, but the exact same component on the tab bar does not get updated.
I followed this tutorial.
When I add console.log statements to check whether the components are being reloaded, I see that the component placed in the tab bar (<TabBarCounter>) is not being reloaded even though the context value has changed.
When I add console.log statement to check for re-rendering in my context provider, I see that it doesn't get re-rendered when a dispatch is called, either.
It seems like the context is being updated locally rather than globally. There is a comment in this answer:
You are updating your state correctly using a reducer but it will only
update local component state not the global context state.
That sounds a lot like the problem I am having here.
Here's some code:
MyContext.tsx
export const CountContext = React.createContext<any>({} as {
countState: CountState,
countDispatch: React.Dispatch<CountReducerActions>,
});
interface MyProps {
children: JSX.Element,
}
const reducer = (countState: CountState, action: CountReducerActions) => {
switch (action.type) {
case 'add1': {
countObject.total += 1;
return countObject;
}
default: {
throw new Error();
}
}
};
export const CountContextProvider: React.VFC<MyProps> = ({ children }: MyProps) => {
const [countState, countDispatch] = useReducer(
reducer,
{
total: 0,
},
);
return (
<CountContext.Provider value={{ countState, countDispatch }}>
{children}
</CountContext.Provider>
);
};
how I update the context
const { countState, countDispatch } = useContext(CountContext);
countDispatch({ type: 'add1' });
MyComponentThatDoesNotGetRerendered.tsx
import React, { useContext } from 'react';
import { IonBadge } from '#ionic/react';
import { CountContext } from '../../context/CountContext';
const TabBarCounter: React.VFC = () => {
const [countState] = useContext(CountContext);
return (
<IonBadge>
{countState.total}
</IonBadge>
);
};
export default TabBarCounter;
Router.tsx
<CountContextProvider>
<IonReactRouter>
<AppTabBar>
<IonRouterOutlet>
<Route exact path={myPageRoute}>
<MyPage />
</Route>
<Route>
<PageError404 />
</Route>
</IonRouterOutlet>
</AppTabBar>
</IonReactRouter>
</CountContextProvider>
AppTabBar.tsx
const AppTabBar: React.VFC<MyProps> = ({ children }: MyProps) => {
const [userObject] = useContext(UserContext);
return (
<IonTabs>
{children}
<IonTabBar slot="bottom" id="appTabBar">
<IonTabButton tab="tab-settings" href={routeTabSettings}>
<IonLabel>Settings</IonLabel>
</IonTabButton>
<IonTabButton
tab="tab-count"
href={routeTabCount}
>
<TabBarReviewCounter />
<IonLabel>Count</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
};
In this case, when the context is updated in <MyPage>, the <TabBarCounter> that is inside <AppTabBar> does not get updated, but the <TabBarCounter> inside <MyPage> does get updated.
How do I update the context correctly using useReducer() so that when I update the context value, all the consumers of that context get updated?
Take a look at your reducer. Instead of modifying state in immutable way you simply overwrite property without creating new reference, therefore context value never updates.
Some components may 'see' this change when they get rerendered because of some reason - local state change, prop change etc. They will reach context, look into provided object and see new value.
To fix it use spread operator to create new objects with keys from previous state and updated total property.
case 'add1': {
return {
...countObject,
total: countObject.total + 1,
};
}

Any reason why a <ContextProvider> HOC won't change it's default value?

export const FocusContext = React.createContext({
isUsingMouse: true,
setIsUsingMouse: () => {
console.error("FocusContext.Provider value not initialized");
},
});
export const FocusContextProvider = ({ children }) => {
const [isUsingMouse, setIsUsingMouse] = React.useState(true);
return (
<FocusContext.Provider value={{ isUsingMouse, setIsUsingMouse }}>
{children}
</FocusContext.Provider>
);
};
Aside from this file and it's instantiation near the root of the app, most files just import and useContext(FocusContext)
For some reason, the value is never taking effect, and setIsUsingMouse logs the defaultValue error
Is there any reason why this implementation of context as a HOC won't work?
It turns out this method works perfectly well.
The issue was that our <ContextProvider> wasn't being used in our Storybook, rather an outer integration layer (to interface with the mother app)

Resources