React elements are removed on state update - javascript

I'm supposed to have a modal appear with an image in it. There are next and previous buttons which controls which image you are currently viewing. The modal is rendered in a portal. That in itself is working correctly. However, when I add children, and those childrens are updated, the modal only (not the portal) gets removed from the flow. In the React DevTools, the "isOpen" state of the modal is still set to true. I am using React 17.0.2 with NextJS 12.0.4 and Styled Components 5.3.3.
I have tried:
memoizing my components (as you can see there are some remnants of those trials) but this did not work
extracting the state of the modal to the parent and passing it as props and it didn't work either
I know there must be something wrong that I'm doing here so if you could help me find it that would be much appreciated!
Here is the controller where the modal is rendered:
import { FC, MouseEventHandler, useEffect, useState } from "react";
import { Photo } from "services/Images/Images.interfaces";
import { useGetNextPhoto, useGetPhotos, useGetPreviousPhoto } from "state";
import SlideshowContextProvider from "./Context/SlideshowContext";
import SlideShowModal from "./SlideShowModal";
const SlideshowController: FC = () => {
const photos = useGetPhotos();
const [currentlyViewedPhoto, setCurrentlyViewedPhoto] = useState<Photo | null>(null);
const nextPhoto = useGetNextPhoto(currentlyViewedPhoto?.id);
const previousPhoto = useGetPreviousPhoto(currentlyViewedPhoto?.id);
const onPreviousRequest: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(previousPhoto);
};
const onNextRequest: MouseEventHandler<HTMLButtonElement> = async (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(nextPhoto);
};
useEffect(() => {
setCurrentlyViewedPhoto(photos[0]);
}, [photos]);
return (
<SlideshowContextProvider
currentlyViewing={currentlyViewedPhoto}
onNextSlideRequest={onNextRequest}
onPreviousSlideRequest={onPreviousRequest}
>
<SlideShowModal />
</SlideshowContextProvider>
);
};
export default SlideshowController;
The SlideshowModal:
import { Modal } from "components";
import { FC } from "react";
import SlideshowControlBar from "./SlideshowControlBar";
import SlideshowImage from "./SlideshowImage";
const SlideShowModal: FC = () => {
return (
<Modal uniqueKey="slideshow">
<SlideshowImage />
<SlideshowControlBar />
</Modal>
);
};
export default SlideShowModal;
The modal in itself:
import Portal from "components/Portal/Portal";
import { FC, useEffect, useMemo, useState } from "react";
import { useRegisterModal } from "state";
import styled from "styled-components";
import useWindowScrollLock from "./hook/UseWindowScrollLock";
interface Props {
uniqueKey: string;
isBackgroundOpaque?: boolean;
}
... Styled elements
const Modal: FC<Props> = ({ uniqueKey, isBackgroundOpaque = true, children }) => {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const register = useRegisterModal(uniqueKey);
const isModalOpen = useMemo(() => isOpen, [isOpen]);
useEffect(() => {
register({ open, close });
}, [register]);
useWindowScrollLock(isModalOpen);
return isModalOpen ? (
<Portal>
<Container>
<InnerModal>
<Close onClick={close}>X</Close>
{children}
</InnerModal>
</Container>
<Background onClick={close} opaque={isBackgroundOpaque} />
</Portal>
) : null;
};
export default Modal;

Related

test if a modal disappears after clicking Delete

Beginner with react testing,
I am using jest and react testing library, here I have a component 'A' which is a modal, I'm trying to implement a test to it, when the user clicks a button 'Delete Link' then this modal should disappear(function onDelete). As you can see I'm clicking the button using FireEvent.click() so after it when changing toHaveBeenCalledTimes(0) from 0 to 1, I'm getting Expected number of calls: 1 Received number of calls: 0, shouldn't be expected and received both be 1?
The end component(modal) should not be visible to the user after clicking Delete Link.
Can someone enlighten me with this?
Any suggestions/help is appreciated.
English is not my mother language so there might be mistakes.
my code:
import React from "react";
import { render, screen, cleanup, fireEvent } from "#testing-
library/react";
import { LinkForm } from "../forms/LinkForm";
import { Provider } from "react-redux";
import { store } from "../../redux/store";
import "#testing-library/jest-dom/extend-expect";
describe("Testing component", () => {
const onClickCallback = jest.fn();
test("Testing if link is deleted when button 'Delete Link' is clicked", () => {
const mockDelete = jest.fn();
const props = {
onDelete: mockDelete,
};
render(
<Provider store={store}>
<LinkForm
classes={{ button_basic: "", formControl: "" }}
key={""}
onSubmit={onClickCallback}
onCancel={onClickCallback}
// onClick={onClickCallback()}
{...props}
/>
</Provider>
);
const component = screen.getByTestId("LinkForm");
const deleteLinkButton = screen.getByRole("button", {
name: /Delete Link/i,
});
expect(deleteLinkButton).toBeVisible();
fireEvent.click(deleteLinkButton);
expect(mockDelete).toHaveBeenCalledTimes(0);
expect(component).toBeVisible();
});
});
import React, { useEffect, useState } from "react";
import { connect, RootStateOrAny, useDispatch, useSelector } from "react-redux";
import { Trans } from "react-i18next";
import { editLink, changeLink, removeLink } from "../../redux/actions";
import {Button} from "#material-ui/core/";
import { Done, Delete } from "#material-ui/icons";
interface AFormProps {
key: string;
onSubmit: () => void;
onCancel: () => void;
onClick?: () => void;
classes: {
button_basic: string;
formControl: string;
};
}
const A: React.FC<AFormProps> = (props) => {
const dispatch = useDispatch();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
dispatch(editLink(linkSettings));
externalOnSubmit();
};
const onDelete = () => {
// Delete selected link from graph
dispatch(removeLink(currentLink.id));
dispatch(changeLink(""));
};
const disabled = currentLink ? false : true;
return (
<form onSubmit={handleSubmit} data-testid="LinkForm">
<Button
id="delete"
type="button"
onClick={onDelete}
disabled={disabled}
variant="outlined"
color="secondary"
className={classes.button_basic}
startIcon={<Delete />}
>
<Trans i18nKey="form.linkForm.delete">Delete Link</Trans>
</Button>
</form>
);
};
export const LinkForm = connect(null, null)(A);
In the test, you are passing a mock function to the onDelete prop of the form but this is not defined in AFormProps nor is an onDelete prop being consumed in the component. The onDelete function is created within the component scope and set as the onClick for the button. The mock function will never be used in this case.

How do I get item from localstorage?

I try to get an item from my localstorage with next.js without rerender my page.
I use this code:
import { ThemeProvider } from "#material-ui/core";
import { FC, useEffect, useState } from "react";
import { useStore } from "../hooks/ContextStore";
import { darkTheme, theme } from "../theme/theme";
import Navbar from "./navbar/Navbar";
const Layout: FC = ({ children }) => {
const [darkMode, setDarkMode] = useState(false);
const store = useStore();
useEffect(() => {
setDarkMode(JSON.parse(window.localStorage.getItem("dark-mode")));
}, [store.darkMode]);
console.log("did render");
return (
<ThemeProvider theme={darkMode ? darkTheme : theme}>
<Navbar />
{children}
</ThemeProvider>
);
};
export default Layout;
Because my useEffect update my state it is rendering the page twice when I turn on or off the dark mode state.
Is there a way to prevent it from rerender that much and accessing the localstorage?
From the looks of it, because your state value is alway going to be from the localstorage, then you don't need to store it in the state.
You can just do something like so:
const isClient = typeof window !== undefined;
const Layout: FC = ({ children }) => {
const store = useStore();
const darkMode = isClient && JSON.parse(window.localStorage.getItem("dark-mode"));
return (
<ThemeProvider theme={darkMode ? darkTheme : theme}>
<Navbar />
{children}
</ThemeProvider>
);
}
export default Layout;
If you really want to store in the state, then you should update the dependency array in useEffect to look for updates in the localstorage value, not the state because you'll be updating the state.
const storageDarkMode = JSON.parse(window.localStorage.getItem("dark-mode"));
useEffect(() => {
setState(storageDarkMode);
}, [storageDarkMode])

Testing a button with a method callback in a functional component

I am trying to test a react component using Enzyme. I am not able to test the click on the IconButton component and the function doesn't get called when i simulate a click.
This is how IconButton is defined on an external component.
var IconButton = function (props) {
return (React.createElement(IconButton$1, { color: 'default', onClick: props.onClick, disabled: props.disabled, size: props.size, onMouseDown: props.onMouseDown }, props.children));
};export{Button,IconButton};
This is how I am using it in my app.
import React, {useState, useEffect} from 'react';
import { Drawer } from '#material-ui/core';
import ExpandLessIcon from '#material-ui/icons/ExpandLess';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import { IconButton } from '#mycomponent/button';
export default function Component1 {
const classes = useStyles();
const [open, setOpen] = useState(true);
const handleClick = function (event) {
if (event) {
setOpen(!open);
}
else {
return;
}
};
return (
<Drawer>
<div className="classname1">
<IconButton onClick={(e) => handleClick(e)} className={classes.button, "iconBtn"}>
{open ? <ExpandLessIcon data-test="lessIcon" /> : <ExpandMoreIcon data-test="moreIcon" />}
</IconButton>
</div>
</Drawer>
);
}
Here is my test for simulating the click on the Icon Button. I also tried another way to check that the handleClick was getting called but it still fails.
const wrapper = shallow(<Component1 />);
it('Test the button click', () => {
expect(wrapper.containsMatchingElement(<ExpandMoreIcon />)).toBeTruthy()
const element = wrapper.find(".iconBtn")
const mockEvent = {target: {}};
element.simulate('click', mockEvent)
expect(wrapper.containsMatchingElement(<ExpandLessIcon />)).toBeTruthy()
})
Try changing this line:
const element = wrapper.find("button").at(0);
or you could find it by it's className from debug():
const element = wrapper.find(".MuiButtonBase-root MuiIconButton-root");
Notice that you'd simulate a click on an actual html button in such case.

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>

React: Context to pass state between two hierarchies of components

I am developing a website in which I want to be able to access the state information anywhere in the app. I have tried several ways of implementing state but I always get following error message:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of SOS.
Here is my SOS->index.js file:
import React, { useContext } from 'react';
import axios from 'axios';
import CONST from '../utils/Constants';
import { Grid, Box, Container } from '#material-ui/core';
import { styled } from '#material-ui/styles';
import { Header } from '../Layout';
import ListItem from './ListItem';
import SOSButton from './SOSButton';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import StateContext from '../App';
import Context from '../Context';
export default function SOS() {
const { componentType, setComponentType } = useContext(Context);
const timerOn = false;
//'type_of_person',
const ambulance = false;
const fire_service = false;
const police = false;
const car_service = false;
//static contextType = StateContext;
const showSettings = event => {
event.preventDefault();
};
const handleComponentType = e => {
console.log(e);
//this.setState({ componentType: 'type_of_emergency' });
setComponentType('type_of_emergency');
};
const handleEmergencyType = new_emergency_state => {
console.log(new_emergency_state);
// this.setState(new_emergency_state);
};
const onSubmit = e => {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state //TODO
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
};
let component;
if (componentType == 'type_of_person') {
component = (
<FormPersonType handleComponentType={this.handleComponentType} />
);
} else if (componentType == 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
}
return (
<React.Fragment>
<Header title="Send out SOS" />
<StateContext.Provider value="type_of_person" />
<Container component="main" maxWidth="sm">
{component}
</Container>
{/*component = (
<HorizontalNonLinearStepWithError
handleComponentType={this.handleComponentType}
/>*/}
</React.Fragment>
);
}
I would really appreciate your help!
Just for reference, the Context file is defined as follows:
import React, { useState } from 'react';
export const Context = React.createContext();
const ContextProvider = props => {
const [componentType, setComponentType] = useState('');
setComponentType = 'type_of_person';
//const [storedNumber, setStoredNumber] = useState('');
//const [functionType, setFunctionType] = useState('');
return (
<Context.Provider
value={{
componentType,
setComponentType
}}
>
{props.children}
</Context.Provider>
);
};
export default ContextProvider;
EDIT: I have changed my code according to your suggestions (updated above). But now I get following error:
TypeError: Cannot read property 'componentType' of undefined
Context is not the default export from your ../Context file so you have to import it as:
import { Context } from '../Context';
Otherwise, it's trying to import your Context.Provider component.
For your file structure/naming, the proper usage is:
// Main app file (for example)
// Wraps your application in the context provider so you can access it anywhere in MyApp
import ContextProvider from '../Context'
export default () => {
return (
<ContextProvider>
<MyApp />
</ContextProvider>
)
}
// File where you want to use the context
import React, { useContext } from 'react'
import { Context } from '../Context'
export default () => {
const myCtx = useContext(Context)
return (
<div>
Got this value - { myCtx.someValue } - from context
</div>
)
}
And for godsakes...rename your Context file, provider, and everything in there to something more explicit. I got confused even writing this.

Categories

Resources