setState through Context not setting state (NextJS) - reactjs

I'm taking my first pass at using NextJS, and also my first pass at using a headless CMS (DatoCMS in this case).
Everything was actually working fine, but I found myself using a lot of prop drilling, to get information returned from the CMS down into deeply nested components, and that was becoming cumbersome. After some googling, I decided to try to use React Context to avoid prop drilling as described here.
The first use case is getting site branding info and menu items down to the nav component (this info will almost always be the same, everywhere on the site, but there might be some sections (e.g., landing pages) where they're different).
The problem is, the React useState set function as passed through the ContextProvider doesn't actually seem to set the state. There are several questions on here about state not being updated in various scenarios, but this isn't setting the initial state after NextJS grabs the data from getStaticProps, but here it's not doing it even if I manually call the set function directly in the component wrapped in ContextProvider, never mind somewhere farther down the stack.
As described in that blog post, I have the context provider pulled out into its own component:
// context/header.js
import { createContext, useContext, useState } from 'react';
const HeaderContext = createContext();
export function HeaderProvider({children}) {
const [header, setHeader] = useState(null);
const value = { header, setHeader };
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
Then, in _app.js, I wrap my <Header /> component in the context provider:
// pages/_app.js
import Header from '../components/Header';
import { HeaderProvider } from '../lib/context/header';
function MyApp({ Component, pageProps }) {
return (
<>
<HeaderProvider>
<Header { ...pageProps } />
</HeaderProvider>
<Component {...pageProps} />
</>
)
}
export default MyApp
I'm just starting this out, so right now, everything is designed to be a static page (no server side rendering or client side React yet).
The main page, from index.js, successfully grabs the data from DatoCMS:
// pages/index.js
import { request } from "../lib/datocms";
import { HOMEPAGE_QUERY } from '../lib/query';
export async function getStaticProps() {
const data = await request({
query: HOMEPAGE_QUERY
});
return {
props: { data }
};
}
export default function Home({ data }) {
return (
<div>
This is the home page
</div>
);
}
So far, this is working, because in my <Header /> component, I can log the data passed.
As I understand how NextJS works, that getStaticProps call populates pageProps which get passed back to the Component (the page) to generate the static html. The results of the request call in getStaticProps is in fact making it's way back to pageProps to be used both in the <Component /> and the <Header /> in _app.js, but when I try to use the setHeader() function from header.js, pulled in by calling useHeader(), the value of the stateful header is always null.
// components/Header.js
import React, { useEffect } from 'react'
import { useHeader } from '../lib/context/header';
import Navbar from './Navbar';
export default function Header({data}) {
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
console.log(HeaderData);
// outputs:
// {
// siteName: 'My Site',
// companyLogo: {
// alt: 'My Site Logo',
// height: 113,
// url: 'https://www.datocms-assets.com/<url>',
// width: 122
// },
// menu: { title: 'primary', menuItems: [ [Object], [Object] ] }
// }
const { setHeader } = useHeader();
useEffect(() => {
setHeader(HeaderData)
}, [HeaderData]);
return (
<header>
<Navbar />
</header>
)
}
And then using the context in <Navbar />:
import React from 'react';
import { useHeader } from '../lib/context/header';
export default function Navbar() {
const { header } = useHeader();
console.log(header); // null :(
return (
<div>This is my nav</div>
)
}
How do I actually get my context state into <Navbar />?
The problem is, the header context in <Navbar /> is always the initial state of null. There was a similar question, but it wasn't using NextJS, just React, and the answer there seemed to be that he was trying to use the useContext call outside of the component chain he had wrapped in ContextProvider. Maybe I'm missing something, but I'm pretty sure that's not my issue.
So, I did the natural debugging thing, and started adding a bunch of console.logs. Before I even get to the <Navbar /> component, it appears that the setHeader() call isn't actually updating the state.
Here's an updated <Header />:
export default function Header({data}) {
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
console.log('Set HeaderData to');
console.log(HeaderData);
// In the initial code, I just pulled in setHeader here, but now pulling in header for debugging purposes
const { header, setHeader } = useHeader();
setHeader(HeaderData); // various incantations here
console.log('Just set header, header is');
console.log(header)
But no matter what incantation I use at "various incantations here", the result logged to console is always the same:
Set HeaderData to
{
siteName: 'My Site',
companyLogo: {
alt: 'My Site Logo',
height: 113,
url: 'https://www.datocms-assets.com/<url>',
width: 122
},
menu: { title: 'primary', menuItems: [ [Object], [Object] ] }
}
Just set header, header is
null
I've tried setHeader(HeaderData), which seems most analogous to what he did in that original blog post, but since that wasn't working, I also tried setHeader({HeaderData}) and setHeader({...HeaderData}), but the results are identical.
Why isn't this set setting?

There are a few issues with the code as written. Firstly you shouldn't try to console log the new state value after setting it. It won't work. Calling setHeader doesn't change the value of header in this call. It causes it to be changed for the next rendering of the component (which will happen immediately).
Secondly, don't use useEffect to synchronize your state.
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
const { setHeader } = useHeader();
useEffect(() => {
setHeader(HeaderData)
}, [HeaderData]);
This code fragment used in any component which is an ancestor of a HeaderContext will cause an infinite render loop. The useEffect will fire each time HeaderData changes. But HeaderData is constructed each time the component is rendered. So the useEffect will fire each time the component is rendered, and it will call setHeader which will force a re-render, which closes the loop.
If you're trying to simply specify the initial state (and not trying to update it in response to some event), simply pass the initial state to the provider component. e.g.
// context/header.js
import { createContext, useContext, useState } from 'react';
const HeaderContext = createContext();
const defaultHeaderState = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
export function HeaderProvider({
initialState = defaultHeaderState,
children
}) {
const [header, setHeader] = useState(initialState);
const value = { header, setHeader };
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}

Chad's answer was definitely right, directionally — don't use useEffect, and "just set it". The problem was,
how to set it from data coming in via getStaticProps?
The answer was to extract it at the _app.js level from pageProps, and pass it directly as a value to the context provider. In fact, there's no longer even a reason to use setState:
// pages/_app.js
import { HeaderProvider } from '../lib/context/header';
function MyApp({ Component, pageProps }) {
// pull the initial data out of pageProps
const headerData = {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data.siteBranding.siteName,
companyLogo: pageProps.data.siteBranding.companyLogo,
menu: pageProps.data?.menu
}
return (
<>
{ /* pass it as a value to the context provider */ }
<HeaderProvider value={headerData}>
<Header { ...pageProps } />
</HeaderProvider>
<Component {...pageProps} />
</>
)
}
and the context provider component gets simplified down to
// context/header.js
import { createContext, useContext } from 'react';
const HeaderContext = createContext();
export function HeaderProvider({value, children}) {
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
Finally, everywhere you want to use it (in a descendent of the context provider, HeaderProvider), you can just:
import { useHeader } from '../lib/context/header';
const headerData = useHeader();

Related

why does a Context reload when user leaves browser tab and returns back

I am using a Context to serve data across all the components of my app. I am using Next.js as the React framework. I notice that every time, the user moves to another tab of the browser and returns to the application tab, the context gets reloaded. I put a log in the useEffect(()=> {console.log(...)}, []) of the Context.js
The effect of this is: In my use-case, the context is to share some db data and functions. Consequently, the database re-loading unnecessarily occurs.
The app is a standard, simple one. I am leaving out the business logic since it is not important to the question. The relevant code is shown below
// _app.js (I am using next.js)
import Layout from 'components/Layout'
import { ThemeProvider } from "next-themes";
import { DataProvider } from 'contexts/DataContext'
...
function MyApp({ Component, pageProps }) {
const router = useRouter()
// simplified the return
return (
<Layout>
<ThemeProvider>
<DataProvider>
<Component {...pageProps} />
</DataProvider>
</ThemeProvider>
</Layout>
)
}
export default MyApp
and the context
# DataContext.js
import { createContext, useEffect, useState, useRef } from "react"
const DataContext = createContext()
export function DataProvider({ children}) {
// some data
const [list, setList] = useState([])
const [somedata, setSomeData] = useState(null)
// some functions
const loadInit = () => {
...
}
const getData = (params) => {
...
}
// other functions
...
...
useEffect(() => {
// this gets called everytime user returns to tab in browser
// causing the loadinit to be reloaded everytime the user
// returns to the open application in the tab
loadinit()
}
}, [])
return (
<DataContext.Provider value={{
list,
somedata,
getData,
....
}}>
{children}
</DataContext.Provider>
)
}
export default DataContext
And this is how the Context is used in any page
// index.js
import DataContext from 'contexts/DataContext'
import Header from 'components/Header'
...
const Home = () => {
...
const { list, getData } = useContext(DataContext)
...
return (
.... list will be used here
)
}
export default Home
Is there a way to avoid it? Currently, I am setting a state in DataContext to check whether it is a first time load or not.
Thanks.

Reducer state update causing a router wrapped in HOC to rerender in a loop

I found that the issue is stemming from a Higher Order Component that wraps around a react-router-dom hook.
This Higher Order Component is imported from #auth0/auth0-react and is a requirement in our project to handle logging out with redirect.
However, even just a basic HOC, the issue is persisting.
in my App.js file, I have a react-redux provider. And inside the provider I have a ProtectLayout component.
ProtectLayout checks for an error reducer, and if the error property in the reducer has a value, it sets a toast message, as seen below.
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import PageLoader from "../loader/PageLoader";
import { useToast } from "../toast/ToastContext";
import { selectError } from "../../store/reducers/error/error.slice";
import ProtectedRoute from "../routes/ProtectedRoute";
const JobsPage = Loadable({
loader: () => import("../../screens/jobs/JobsPage"),
loading: () => <PageLoader loadingText="Getting your jobs..." />
});
const ProtectedLayout = () => {
const { openToast } = useToast();
const { error } = useSelector(selectError);
const getErrorDetails = async () => {
if (error) {
if (error?.title || error?.message)
return { title: error?.title, message: error?.message };
return {
title: "Error",
message: `Something went wrong. We couldn't complete this request`
};
}
return null;
};
useEffect(() => {
let isMounted = true;
getErrorDetails().then(
(e) =>
isMounted &&
(e?.title || e?.message) &&
openToast({ type: "error", title: e?.title, message: e?.message })
);
return () => {
isMounted = false;
};
}, [error]);
return (
<Switch>
<ProtectedRoute exact path="/" component={JobsPage} />
</Switch>
);
};
export default ProtectedLayout;
ProtectLayout returns another component ProtectedRoute. ProtectedRoute renders a react-router-dom Route component, which the component prop on the Route in the component prop passed into ProtectedRoute but wrapped in a Higher Order Component. In my actual application, as aforementioned, this is the withAuthenticationRequired HOC from #auth0/auth0-react which checks if an auth0 user is logged in, otherwise it logs the user out and redirects to the correct URL.
import React from "react";
import { Route } from "react-router-dom";
const withAuthenticationRequired = (Component, options) => {
return function WithAuthenticationRequired(props) {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, ...args }) => {
return <Route component={withAuthenticationRequired(component)} {...args} />;
};
export default ProtectedRoute;
However, in one of the Route components, JobsPage the error reducer state is updated on mount, so what happens is the state gets updated, the ProtectedLayout re-renders, which then re-renders ProtectedRoute, which then re-renders JobPage which triggers the useEffect again, which updates the state, so you end up in an infinite loop.
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getGlobalError } from "../../store/reducers/error/error.thunk";
const JobsPage = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getGlobalError(new Error("test")));
}, []);
return (
<div>
JOBS PAGE
</div>
);
};
export default JobsPage;
I have no idea how to prevent this rendering loop?
Really all I want to do, is that when there is an error thrown in a thunk action, it catches the error and updates the error reducer state. That will then trigger a toast message, using the useToast hook. Perhaps there is a better way around this, that what I currently have setup?
I have a CodeSandbox below to recreate this issue. If you click on the text you can see the re-renders occur, if you comment out the useEffect hook, it will basically crash the sandbox, so might be best to only uncomment when you think you have resolved the issue.
Any help would be greatly appreciated!

NextJS: Context values undefined in production (works fine in development)

A "dark mode" feature has been implemented on my Next.js application using React's Context api.
Everything works fine during development, however, Context provider-related problems have arisen on the built version — global states show as undefined and cannot be handled.
_app.tsx is wrapped with the ThemeProvider as such:
// React & Next hooks
import React, { useEffect } from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
// Irrelevant imports
// Global state management
import { Provider } from "react-redux";
import store from "../redux/store";
import { AuthProvider } from "../context/UserContext";
import { ThemeProvider } from "../context/ThemeContext";
// Components
import Layout from "../components/Layout/Layout";
import Footer from "../components/Footer/Footer";
// Irrelevant code
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
// Applying different layouts depending on page
switch (Component.name) {
case "HomePage":
return (
<Provider store={store}>
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
<Footer color="fff" />
</AuthProvider>
</ThemeProvider>
</Provider>
);
case "PageNotFound":
return (
<>
<Component {...pageProps} />
<Footer color="#f2f2f5" />
</>
);
default:
// Irrelevant code
}
}
export default MyApp;
The ThemeContext correctly exports both its Provider and Context:
import { createContext, ReactNode, useState, useEffect } from "react";
type themeContextType = {
darkMode: boolean | null;
toggleDarkMode: () => void;
};
type Props = {
children: ReactNode;
};
// Checks for user's preference.
const getPrefColorScheme = () => {
return !window.matchMedia
? null
: window.matchMedia("(prefers-color-scheme: dark)").matches;
};
// Gets previously stored theme if it exists.
const getInitialMode = () => {
const isReturningUser = "dark-mode" in localStorage; // Returns true if user already used the website.
const savedMode = localStorage.getItem("dark-mode") === "true" ? true : false;
const userPrefersDark = getPrefColorScheme(); // Gets user's colour preference.
// If mode was saved ► return saved mode else get users general preference.
return isReturningUser ? savedMode : userPrefersDark ? true : false;
};
export const ThemeContext = createContext<themeContextType>(
{} as themeContextType
);
export const ThemeProvider = ({ children }: Props) => {
// localStorage only exists on the browser (window), not on the server
const [darkMode, setDarkMode] = useState<boolean | null>(null);
// Getting theme from local storage upon first render
useEffect(() => {
setDarkMode(getInitialMode);
}, []);
// Prefered theme stored in local storage
useEffect(() => {
localStorage.setItem("dark-mode", JSON.stringify(darkMode));
}, [darkMode]);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
{children}
</ThemeContext.Provider>
);
};
The ThemeToggler responsible for updating the darkMode state operates properly during development (theme toggled and correct value console.loged upon clicking), however it doesn't do anything during production (console.logs an undefined state):
import React, { FC, useContext } from "react";
import { ThemeContext } from "../../context/ThemeContext";
const ThemeToggler: FC = () => {
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
const toggleTheme = () => {
console.log(darkMode) // <--- darkMode is undefined during production
toggleDarkMode();
};
return (
<div className="theme-toggler">
<i
className={`fas ${darkMode ? "fa-sun" : "fa-moon"}`}
data-testid="dark-mode"
onClick={toggleTheme}
></i>
</div>
);
};
export default ThemeToggler;
The solutions/suggestions I've looked up before posting this were to no avail.
React Context API undefined in production — react and react-dom are on the same version.
Thanks in advance.
P.S. For those wondering why I am using both Redux and Context for global state management:
Context is best suited for low-frequency and simple state updates such as themes and authentication.
Redux is better for high-frequency and complex state updates in addition to providing a better debugging tool — Redux DevTools.
P.S.2 Yes, it is better – performance-wise – to install FontAwesome's dependencies rather than use a CDN.
Thanks for sharing the code. It's well written. By reading it i don't see any problem. Based on your component topology, as long as your ThemeToggler is defined under any page component, your darkMode can't be undefined.
Here's your topology of the site
<MyApp>
<Provider>
// A. will not work
<ThemeProvider>
<HomePage>
// B. should work
</HomePage>
</ThemeProvider>
// C. will not work
</Provider>
</MyApp>
Although your ThemeProvider is a custom provider, inside ThemeContext.Provider is defined with value {{ darkMode, toggleDarkMode }}. So in theory you can't get undefined unless your component ThemeToggler is not under a HomePage component. I marked two non working locations, any component put under location A or C will give you undefined.
Since you have a condition for HomePage, you can run into this problem if you are on other pages. So in general you should wrap the ThemeProvider on top of your router.
<ThemeProvider>
<AuthProvider>
{Component.name != "PageNotFound" && (
<Component {...pageProps} />
)}
</AuthProvider>
</ThemeProvider>
You get the point, you want to first go through a layer that theme always exist before you fire up a router.
You can confirm if this is the case by doing the following test.
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
)
}
If this works in production, then it confirms it. To be honest, this problem also exists in the dev, however maybe due to your routes change too quickly, it normally hides these issues.

How to prevent parent component from re-rendering with React (next.js) SSR two-pass rendering?

So I have a SSR app using Next.js. I am using a 3rd party component that utilizes WEB API so it needs to be loaded on the client and not the server. I am doing this with 'two-pass' rendering which I read about here: https://itnext.io/tips-for-server-side-rendering-with-react-e42b1b7acd57
I'm trying to figure out why when 'ssrDone' state changes in the next.js page state the entire <Layout> component unnecessarily re-renders which includes the page's Header, Footer, etc.
I've read about React.memo() as well as leveraging shouldComponentUpdate() but I can't seem to prevent it from re-rendering the <Layout> component.
My console.log message for the <Layout> fires twice but the <ThirdPartyComponent> console message fires once as expected. Is this an issue or is React smart enough to not actually update the DOM so I shouldn't even worry about this. It seems silly to have it re-render my page header and footer for no reason.
In the console, the output is:
Layout rendered
Layout rendered
3rd party component rendered
index.js (next.js page)
import React from "react";
import Layout from "../components/Layout";
import ThirdPartyComponent from "../components/ThirdPartyComponent";
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
ssrDone: false
};
}
componentDidMount() {
this.setState({ ssrDone: true });
}
render() {
return (
<Layout>
{this.state.ssrDone ? <ThirdPartyComponent /> : <div> ...loading</div>}
</Layout>
);
}
}
export default Home;
ThirdPartyComponent.jsx
import React from "react";
export default function ThirdPartyComponent() {
console.log("3rd party component rendered");
return <div>3rd Party Component</div>;
}
Layout.jsx
import React from "react";
export default function Layout({ children }) {
return (
<div>
{console.log("Layout rendered")}
NavBar here
<div>Header</div>
{children}
<div>Footer</div>
</div>
);
}
What you could do, is define a new <ClientSideOnlyRenderer /> component, that would look like this:
const ClientSideOnlyRenderer = memo(function ClientSideOnlyRenderer({
initialSsrDone = false,
renderDone,
renderLoading,
}) {
const [ssrDone, setSsrDone] = useState(initialSsrDone);
useEffect(
function afterMount() {
setSsrDone(true);
},
[],
);
if (!ssrDone) {
return renderLoading();
}
return renderDone();
});
And you could use it like this:
class Home extends React.Component {
static async getInitialProps({ req }) {
return {
isServer: !!req,
};
};
renderDone() {
return (
<ThirdPartyComponent />
);
}
renderLoading() {
return (<div>Loading...</div>);
}
render() {
const { isServer } = this.props;
return (
<Layout>
<ClientSideOnlyRenderer
initialSsrDone={!isServer}
renderDone={this.renderDone}
renderLoading={this.renderLoading}
/>
</Layout>
);
}
}
This way, only the ClientSideOnlyRenderer component gets re-rendered after initial mount. 👍
The Layout component re-renders because its children prop changed. First it was <div> ...loading</div> (when ssrDone = false) then <ThirdPartyComponent /> (when ssrDone = true)
I had a similar issue recently, what you can do is to use redux to store the state that is causing the re-render of the component.
Then with useSelector and shallowEqual you can use it and change its value without having to re-render the component.
Here is an example
import styles from "./HamburgerButton.module.css";
import { useSelector, shallowEqual } from "react-redux";
const selectLayouts = (state) => state.allLayouts.layouts[1];
export default function HamburgerButton({ toggleNav }) {
let state = useSelector(selectLayouts, shallowEqual);
let navIsActive = state.active;
console.log("navIsActive", navIsActive); // true or false
const getBtnStyle = () => {
if (navIsActive) return styles["hamBtn-active"];
else return styles["hamBtn"];
};
return (
<div
id={styles["hamBtn"]}
className={getBtnStyle()}
onClick={toggleNav}
>
<div className={styles["stick"]}></div>
</div>
);
}
This is an animated button component that toggles a sidebar, all wrapped inside a header component (parent)
Before i was storing the sidebar state in the header, and on its change all the header has to re-render causing problems in the button animation.
Instead i needed all my header, the button state and the sidebar to stay persistent during the navigation, and to be able to interact with them without any re-render.
I guess now the state is not in the component anymore but "above" it, so next doesn't start a re-render. (i can be wrong about this part but it looks like it)
Note that toggleNav is defined in header and passed as prop because i needed to use it in other components as well. Here is what it looks like:
const toggleNav = () => {
dispatch(toggleLayout({ id: "nav", fn: "toggle" }));
}; //toggleLayout is my redux action
I'm using an id and fn because all my layouts are stored inside an array in redux, but you can use any logic or solution for this part.

How would I use React Hooks to replace my withAuth() HOC?

I've been spending a bunch of time reading up on React Hooks, and while the functionality seems more intuitive, readable, and concise than using classes with local state and lifecycle methods, I keep reading references to Hooks being a replacement for HOCs.
The primary HOC I have used in React apps is withAuth -- basically a function that checks to see if the currentUser (stored in Redux state) is authenticated, and if so, to render the wrapped component.
Here is an implementation of this:
import React, { Component } from "react";
import { connect } from "react-redux";
export default function withAuth(ComponentToBeRendered) {
class Authenticate extends Component {
componentWillMount() {
if (this.props.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
componentWillUpdate(nextProps) {
if (nextProps.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
render() {
return <ComponentToBeRendered {...this.props} />;
}
}
function mapStateToProps(state) {
return { isAuthenticated: state.currentUser.isAuthenticated };
}
return connect(mapStateToProps)(Authenticate);
}
What I can't see is how I can replace this HOC with hooks, especially since hooks don't run until after the render method is called. That means I would not be able to use a hook on what would have formerly been ProtectedComponent (wrapped with withAuth) to determine whether to render it or not since it would already be rendered.
What is the new fancy hook way to handle this type of scenario?
render()
We can reframe the question of 'to render or not to render' a tiny bit. The render method will always be called before either hook-based callbacks or lifecycle methods. This holds except for some soon-to-be deprecated lifecycle methods.
So instead, your render method (or functional component) has to handle all its possible states, including states that require nothing be rendered. Either that, or the job of rendering nothing can be lifted up to a parent component. It's the difference between:
const Child = (props) => props.yes && <div>Hi</div>;
// ...
<Parent>
<Child yes={props.childYes} />
</Parent>
and
const Child = (props) => <div>Hi</div>;
// ...
<Parent>
{props.childYes && <Child />}
</Parent>
Deciding which one of these to use is situational.
Hooks
There are ways of using hooks to solve the same problems the HOCs do. I'd start with what the HOC offers; a way of accessing user data on the application state, and redirecting to /signin when the data signifies an invalid session. We can provide both of those things with hooks.
import { useSelector } from "react-redux";
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const MySecurePage = props => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated && <MyPage {...props} />;
};
A couple of things happening in the example above. We're using the useSelector hook from react-redux to access the the state just as we were previously doing using connect, only with much less code.
We're also using the value we get from useSelector to conditionally fire a side effect with the useEffect hook. By default the callback we pass to useEffect is called after each render. But here we also pass an array of the dependencies, which tells React we only want the effect to fire when a dependency changes (in addition to the first render, which always fires the effect). Thus we will be redirected when isAuthenticated starts out false, or becomes false.
While this example used a component definition, this works as a custom hook as well:
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const useAuth = () => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated;
};
const MySecurePage = (props) => {
return useAuth() && <MyPage {...props} />;
};
One last thing - you might wonder about doing something like this:
const AuthWrapper = (props) => useAuth() && props.children;
in order to be able to do things like this:
<AuthWrapper>
<Sensitive />
<View />
<Elements />
</AuthWrapper>
You may well decide this last example is the approach for you, but I would read this before deciding.
Building on the answer provided by backtick, this chunk of code should do what you're looking for:
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
const withAuth = (ComponentToBeRendered) => {
const mapState = (state) => ({
isAuthenticated: state.currentUser.isAuthenticated,
});
const Authenticate = (props) => {
const { isAuthenticated } = useSelector(mapState);
useEffect(() => {
if (!isAuthenticated) {
props.history.push("/signin");
}
}, [isAuthenticated]);
return isAuthenticated && <ComponentToBeRendered {...props} />;
};
return Authenticate;
};
export default withAuth;
You could render this in a container using React-Router-DOM as such:
import withAuth from "../hocs/withAuth"
import Component from "../components/Component"
// ...
<Route path='...' component={withAuth(Component)} />

Resources