I think the Space.js and Loading.js components re-render far too many times. I can understand it up til the very last 4 re-renders. What is causing them is a mystery.
[slug].js
import { useRouter } from "next/router";
import AuthLayout from "#/components/layouts/AuthLayout";
import { useEffect, useState } from "react";
import axios from "#/src/lib/axios";
import Container from "#mui/material/Container";
import Typography from "#mui/material/Typography";
import useBearStore from "stores/pages";
import Loading from "#/components/layouts/Auth/Loading";
export default function Space() {
console.log('Space.js: function()');
const {setLoading} = useBearStore();
const router = useRouter();
const { slug } = router.query;
const [space, setSpace] = useState(null);
const getSpace = async () => {
const { data } = await axios.get(`api/spaces/${slug}`);
setSpace(data.data);
setLoading(false);
};
useEffect(() => {
console.log("Space.js: useEffect()");
getSpace();
}, [slug]);
return (
<Loading>
<Container maxWidth="sm">
<Typography variant="h1" component="h1" gutterBottom>
{space?.name}
</Typography>
</Container>
</Loading>
);
}
Space.getLayout = function getLayout(page) {
return <AuthLayout>{page}</AuthLayout>;
};
Loading.js
import useBearStore from "stores/pages";
import { LinearProgress } from "#mui/material";
import { useEffect } from 'react'
export default function Loading({children}) {
const {loading, setLoading} = useBearStore();
console.log('Loading.js: function()');
useEffect(() => {
console.log('Loading.js: useEffect()');
setLoading(true);
}, []);
return loading ? <LinearProgress /> : children;
}
pages.js
import create from 'zustand'
const useBearStore = create((set) => ({
loading: true,
setLoading: (value) => set((state) => ({ loading: value }))
}))
export default useBearStore;
next.config.js
module.exports = {
// reactStrictMode: true,
// images: {
// domains: ["images.pexels.com"],
// },
};
Related
I would like to open and close an antd Modal component using a MobX store. I have the following code Here's a link to codesandbox https://codesandbox.io/s/nifty-dijkstra-g0kzs6?file=/src/App.js
import AppNavigation from "./components/Menu";
import ContactPopUp from "./components/Contact";
export default function App() {
return (
<div className="App">
<AppNavigation />
<ContactPopUp />
</div>
);
}
File with the MobXstore
import { createContext, useContext } from "react";
import AppStore from "./appStore";
interface Store {
appStore: AppStore;
}
export const store: Store = {
appStore: new AppStore()
};
export const StoreContext = createContext(store);
export function useStore() {
return useContext(StoreContext);
}
Separate file where I declare the store
import { makeAutoObservable } from "mobx";
export default class AppStore {
contactFormOpen = false;
constructor() {
makeAutoObservable(this);
}
setContactFormOpen = (isOpen: boolean) => {
console.log("Changed contact form to ", isOpen);
this.contactFormOpen = isOpen;
};
}
The Menu.tsx
import React from "react";
import { Menu, MenuProps } from "antd";
import { useStore } from "../store/store";
const AppNavigation = () => {
const { appStore } = useStore();
const menuItems: MenuProps["items"] = [
{
label: <a onClick={(e) => handleOpenContactForm(e)}>Contact</a>,
key: "Contact"
}
];
const handleOpenContactForm = (e: any) => {
e.preventDefault();
e.stopPropagation();
appStore.setContactFormOpen(true);
console.log("Open contact pop up", appStore.contactFormOpen);
};
return (
<Menu
items={menuItems}
theme="dark"
overflowedIndicator={""}
className="header__menu award-menu header__menu--md"
/>
);
};
export default AppNavigation;
ContactPopUp.tsx
import { Modal } from "antd";
import React, { useEffect, useState } from "react";
import { useStore } from "../store/store";
const ContactPopUp = () => {
const { appStore } = useStore();
const [visible, setVisible] = useState(appStore.contactFormOpen);
useEffect(() => {
setVisible(appStore.contactFormOpen);
}, [appStore.contactFormOpen]);
const handleCancel = () => {
appStore.setContactFormOpen(false);
console.log("Close contact from", appStore.contactFormOpen);
};
return (
<Modal title="Contact us" visible={visible} onCancel={handleCancel}>
<h2>Modal Open</h2>
</Modal>
);
};
export default ContactPopUp;
The mobx contactFormOpen clearly changes but the modal state does not. I really don't understand why... UseEffect also doesn't trigger a re render.
You just forgot most crucial part - every component that uses any observable value needs to be wrapped with observer decorator! Like that:
const ContactPopUp = () => {
const { appStore } = useStore();
const handleCancel = () => {
appStore.setContactFormOpen(false);
console.log('Close contact from', appStore.contactFormOpen);
};
return (
<Modal
title="Contact us"
visible={appStore.contactFormOpen}
onCancel={handleCancel}
>
<h2>Modal Open</h2>
</Modal>
);
};
// Here I've added `observer` decorator/HOC
export default observer(ContactPopUp);
And you don't need useEffect or anything like that now.
Codesandbox
I need to mock my custom hook when unit testing React component. I've read some stackoverflow answers but haven't succeeded in implementing it correctly.
I can't use useAuth without mocking it as it depends on server request and I'm only writing unit tests at the moment.
//useAuth.js - custom hook
import React, { createContext, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
const authContext = createContext();
function useProvideAuth() {
const [accessToken, setAccessToken] = useState('');
const [isAuthenticated, setAuthenticated] = useState(
accessToken ? true : false
);
useEffect(() => {
refreshToken();
}, []);
const login = async (loginCredentials) => {
const accessToken = await sendLoginRequest(loginCredentials);
if (accessToken) {
setAccessToken(accessToken);
setAuthenticated(true);
}
};
const logout = async () => {
setAccessToken(null);
setAuthenticated(false);
await sendLogoutRequest();
};
const refreshToken = async () => {
const accessToken = await sendRefreshRequest();
if (accessToken) {
setAccessToken(accessToken);
setAuthenticated(true);
} else setAuthenticated(false);
setTimeout(async () => {
refreshToken();
}, 15 * 60000 - 1000);
};
return {
isAuthenticated,
accessToken,
login,
logout
};
}
export function AuthProvider({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
AuthProvider.propTypes = {
children: PropTypes.any
};
const useAuth = () => {
return useContext(authContext);
};
export default useAuth;
The test I've written
import React from 'react';
import { render, fireEvent, screen } from '#testing-library/react';
import { NavBar } from '../App';
jest.resetAllMocks();
jest.mock('../auth/useAuth', () => {
const originalModule = jest.requireActual('../auth/useAuth');
return {
__esModule: true,
...originalModule,
default: () => ({
accessToken: 'token',
isAuthenticated: true,
login: jest.fn,
logout: jest.fn
})
};
});
describe('NavBar when isAuthenticated', () => {
it('LogOut button is visible when isAuthenticated', () => {
render(<NavBar />);
expect(screen.getByText(/log out/i)).toBeVisible();
});
});
The function I'm writing tests on:
//App.js
import React from 'react';
import cn from 'classnames';
import useAuth, { AuthProvider } from './auth/useAuth';
import './App.css';
import '../node_modules/bootstrap/dist/css/bootstrap.css';
function App() {
return (
<AuthProvider>
<Router>
<NavBar />
</Router>
</AuthProvider>
);
}
const NavBarSignUpButton = () => (
<button className='button info'>
Sign up
</button>
);
const NavBarLogoutButton = () => {
const auth = useAuth();
const handleLogOut = () => {
auth.logout();
};
return (
<button className='button info' onClick={handleLogOut}>
Log out
</button>
);
};
export const NavBar = () => {
const isAuthenticated = useAuth().isAuthenticated;
const loginButtonClassName = cn({
btn: true,
invisible: isAuthenticated
});
return (
<nav className='navbar navbar-expand-lg navbar-light'>
<div className='container'>
<div className='d-flex justify-content-end'>
<div className='navbar-nav'>
<button className={loginButtonClassName}>
Log In
</button>
{isAuthenticated ? <NavBarLogoutButton /> : <NavBarSignUpButton />}
</div>
</div>
</div>
</nav>
);
};
The test code above doesn't throw any errors. However, the test fails as useAuth().isAuthenticated is always false (but I'm mocking it to return true). It doesn't change whether I test App or only NavBar
What am I doing wrong?
I made a super minified example that should show the mocking works. It just features the hook itself and a component returning YES or NO based on the hook. The test
useAuth.js
import {createContext, useContext} from 'react'
const authContext = createContext()
const useAuth = () => {
return useContext(authContext)
}
export default useAuth
component.js
import useAuth from './useAuth'
export const Component = () => {
const isAuthenticated = useAuth().isAuthenticated
return isAuthenticated ? 'YES' : 'NO'
}
component.test.js
import React from 'react'
import {render, screen} from '#testing-library/react'
import {Component} from './component'
jest.mock('./useAuth', () => {
const originalModule = jest.requireActual('./useAuth')
return {
__esModule: true,
...originalModule,
default: () => ({
accessToken: 'token',
isAuthenticated: true,
login: jest.fn,
logout: jest.fn,
}),
}
})
describe('When isAuthenticated', () => {
it('Component renders YES', () => {
render(<Component />)
screen.getByText(/YES/i)
})
})
In this case, the component does in fact render YES and the test passes. This makes me thing there are other things involved. When I change the mock to false, the test fails because it renders NO.
I'm having trouble testing a page that has Context and useEffect using Jest and Testing-library, can you help me?
REPO: https://github.com/jefferson1104/padawan
My Context: src/context/personContext.tsx
import { createContext, ReactNode, useState } from 'react'
import { useRouter } from 'next/router'
import { api } from '../services/api'
type PersonData = {
name?: string
avatar?: string
}
type PersonProviderProps = {
children: ReactNode
}
type PersonContextData = {
person: PersonData
loading: boolean
handlePerson: () => void
}
export const PersonContext = createContext({} as PersonContextData)
export function PersonProvider({ children }: PersonProviderProps) {
const [person, setPerson] = useState<PersonData>({})
const [loading, setLoading] = useState(false)
const router = useRouter()
function checkAvatar(name: string): string {
return name === 'Darth Vader'
? '/img/darth-vader.png'
: '/img/luke-skywalker.png'
}
async function handlePerson() {
setLoading(true)
const promise1 = api.get('/1')
const promise2 = api.get('/4')
Promise.race([promise1, promise2]).then(function (values) {
const data = {
name: values.data.name,
avatar: checkAvatar(values.data.name)
}
setPerson(data)
setLoading(false)
router.push('/battlefield')
})
}
return (
<PersonContext.Provider value={{ person, handlePerson, loading }}>
{children}
</PersonContext.Provider>
)
}
My Page: src/pages/battlefield.tsx
import { useContext, useEffect } from 'react'
import { useRouter } from 'next/router'
import { PersonContext } from '../context/personContext'
import Person from '../components/Person'
const Battlefield = () => {
const { person } = useContext(PersonContext)
const router = useRouter()
useEffect(() => {
if (!person.name) {
router.push('/')
}
})
return <Person />
}
export default Battlefield
My Test: src/tests/pages/Battlefield.spec.tsx
import { render, screen } from '#testing-library/react'
import { PersonContext } from '../../context/personContext'
import Battlefield from '../../pages'
jest.mock('../../components/Person', () => {
return {
__esModule: true,
default: function mock() {
return <div data-test-id="person" />
}
}
})
describe('Battlefield page', () => {
it('renders correctly', () => {
const mockPerson = { name: 'Darth Vader', avatar: 'darth-vader.png' }
const mockHandlePerson = jest.fn()
const mockLoading = false
render(
<PersonContext.Provider
value={{
person: mockPerson,
handlePerson: mockHandlePerson,
loading: mockLoading
}}
>
<Battlefield />
</PersonContext.Provider>
)
expect(screen.getByTestId('person')).toBeInTheDocument()
})
})
PRINSCREEN ERROR
enter image description here
I found a solution:
The error was happening because the path where I call the Battlefield page didn't have the absolute path.
I'm trying to use the useSnack hook from notistack library but I keep getting this error
TypeError: Cannot destructure property 'enqueueSnackbar' of 'Object(...)(...)' as it is undefined.
Here is the code:
import React, { useContext, useEffect } from "react";
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider, useSnackbar } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
// This line below is where the error seems to be
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
alertContext.msg !== "" &&
enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider maxSnack={4}>{props.children}</SnackbarProvider>;
};
export default Alerts;
useSnackbar hook accessible anywhere down the tree from SnackbarProvider.
So you cannot use it in the same component as SnackbarProvier.
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
const providerRef = React.useRef();
useEffect(() => {
alertContext.msg !== "" &&
providerRef.current.enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider ref={providerRef} maxSnack={4}>
{props.children}
</SnackbarProvider>;
};
export default Alerts;
Wrap you index file with SnapBar provider:
index.js
import { SnackbarProvider } from "notistack";
const Index = () => (
<SnackbarProvider maxSnack={1} preventDuplicate>
index
</SnackbarProvider>
)
export default Index
jsx file
import { useSnackbar } from "notistack";
const Logs = () => {
const { enqueueSnackbar } = useSnackbar();
const handler = () => {
enqueueSnackbar(`Successful.`, { variant: "success" });
};
return <span onClick={handler}>"Logs loading"</span>;
};
export default Logs;
I'm trying to use themes in Rebass, and it suggested Theme UI for theming. After following the guide on the following, I cannot get setColorMode to work in my storybook.
import useColorMode
import React from 'react'
import { ColorMode, ThemeProvider, useColorMode } from 'theme-ui'
const ThemeWrapper = (props) => {
const [colorMode, setColorMode] = useColorMode() // error
//...
}
I receive this as an error instead: [useColorMode] requires the ThemeProvider component
import useThemeUI
import { ColorMode, ThemeProvider, useThemeUI } from 'theme-ui'
const ThemeWrapper = (props) => {
const context = useThemeUI()
const { setColorMode } = context
//...
}
Later on, I have setColorMode is not a function
Examining this context using console.log, it contains the following:
{
components: Object { p: {…}, b: {…}, i: {…}, … }
emotionVersion: "10.0.27"
theme: null
}
useColorMode is nowhere to be found.
What am I doing wrong?
My current code:
.storybook/config.js
import React, { useEffect } from 'react'
import addons from '#storybook/addons';
import { addDecorator, configure } from '#storybook/react';
import { ColorMode, ThemeProvider, useThemeUI } from 'theme-ui'
import theme from '../theme'
const channel = addons.getChannel();
const ThemeWrapper = (props) => {
const context = useThemeUI()
const { setColorMode } = context
console.log(context)
const setDarkMode = isDark => setColorMode(isDark ? 'dark' : 'default')
useEffect(() => {
channel.on('DARK_MODE', setDarkMode);
return () => channel.removeListener('DARK_MODE', setDarkMode);
}, [channel, setColorMode]);
return (
<ThemeProvider theme={theme}>
<ColorMode/>
{props.children}
</ThemeProvider>
);
}
addDecorator(renderStory => <ThemeWrapper>{renderStory()}</ThemeWrapper>);
configure([
require.context('../components', true, /\.stories\.(jsx?|mdx)$/),
require.context('../stories', true, /\.stories\.(jsx?|mdx)$/)
], module);
I asked here: https://github.com/system-ui/theme-ui/issues/537 and I managed to correct my problematic code.
The error arises from the function useColorMode not being called inside a <ThemeProvider>.
I changed my config file to the following to mitigate the issue. And it fixed my problem.
import React, { useEffect } from 'react'
import addons from '#storybook/addons';
import { addDecorator, configure } from '#storybook/react';
import { ColorMode, ThemeProvider, useColorMode } from 'theme-ui'
import theme from '../theme'
const channel = addons.getChannel();
const ThemeChanger = () => {
const [colorMode, setColorMode] = useColorMode();
const setDarkMode = isDark => setColorMode(isDark ? 'dark' : 'default')
useEffect(() => {
channel.on('DARK_MODE', setDarkMode);
return () => channel.removeListener('DARK_MODE', setDarkMode);
}, [channel, setColorMode]);
return <div/>
}
const ThemeWrapper = ({ children }) => {
return (
<ThemeProvider theme={theme}>
<ThemeChanger/>
<ColorMode/>
{children}
</ThemeProvider>
);
}
addDecorator(renderStory => <ThemeWrapper>{renderStory()}</ThemeWrapper>);
configure([
require.context('../components', true, /\.stories\.(jsx?|mdx)$/),
require.context('../stories', true, /\.stories\.(jsx?|mdx)$/)
], module);