Closing Multiple React Components - one after another - javascript

I read through a lot of "close react component on escape / outside click" threads and am able to close all opened react components at once.
But: When 2 popups are opened, i want to first close the one "on top", then the one behind it, not both at the same time. Here is a pic of the application:
Screenshot Application with Popups to close
My code for a custom hook to close the components:
import { useRef, useEffect } from 'react';
export const useOutsideClick = (ref: any, callback: any, when: boolean) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
});
const handler = (e: any) => {
// check if target event is within the ref
if (ref.current && !ref.current.contains(e.target)) {
console.log('Handler handling states!');
savedCallback.current();
}
};
useEffect(() => {
if (when) {
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
}
}, [when]);
};
With the useRef i am creating a connection to a div that needs to be closed an call the useOutsideClick() function.
Any ideas how i can achieve closing one component after another?
Thanks & Greetings!
EDIT: The thing is, i can also close just a single component. The problem is, that like this, the application doesnt know when i click on some other component, it just notices the click when i click on the space where no component is rendered.

Related

Getting a document and window is not defined error in NextJS [duplicate]

In my Next.js app I can't seem to access window:
Unhandled Rejection (ReferenceError): window is not defined
componentWillMount() {
console.log('window.innerHeight', window.innerHeight);
}
̶A̶n̶o̶t̶h̶e̶r̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶b̶y̶ ̶u̶s̶i̶n̶g̶ ̶p̶r̶o̶c̶e̶s̶s̶.̶b̶r̶o̶w̶s̶e̶r ̶ ̶t̶o̶ ̶j̶u̶s̶t̶ ̶e̶x̶e̶c̶u̶t̶e̶ ̶ ̶y̶o̶u̶r̶ ̶c̶o̶m̶m̶a̶n̶d̶ ̶d̶u̶r̶i̶n̶g̶ ̶r̶e̶n̶d̶e̶r̶i̶n̶g̶ ̶o̶n̶ ̶t̶h̶e̶ ̶c̶l̶i̶e̶n̶t̶ ̶s̶i̶d̶e̶ ̶o̶n̶l̶y̶.
But process object has been deprecated in Webpack5 and also NextJS, because it is a NodeJS variable for backend side only.
So we have to use back window object from the browser.
if (typeof window !== "undefined") {
// Client-side-only code
}
Other solution is by using react hook to replace componentDidMount:
useEffect(() => {
// Client-side-only code
})
Move the code from componentWillMount() to componentDidMount():
componentDidMount() {
console.log('window.innerHeight', window.innerHeight);
}
In Next.js, componentDidMount() is executed only on the client where window and other browser specific APIs will be available. From the Next.js wiki:
Next.js is universal, which means it executes code first server-side,
then client-side. The window object is only present client-side, so if
you absolutely need to have access to it in some React component, you
should put that code in componentDidMount. This lifecycle method will
only be executed on the client. You may also want to check if there
isn't some alternative universal library which may suit your needs.
Along the same lines, componentWillMount() will be deprecated in v17 of React, so it effectively will be potentially unsafe to use in the very near future.
If you use React Hooks you can move the code into the Effect Hook:
import * as React from "react";
export const MyComp = () => {
React.useEffect(() => {
// window is accessible here.
console.log("window.innerHeight", window.innerHeight);
}, []);
return (<div></div>)
}
The code inside useEffect is only executed on the client (in the browser), thus it has access to window.
With No SSR
https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
The error occurs because window is not yet available, while component is still mounting. You can access window object after component is mounted.
You can create a very useful hook for getting dynamic window.innerHeight or window.innerWidth
const useDeviceSize = () => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleWindowResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
}
useEffect(() => {
// component is mounted and window is available
handleWindowResize();
window.addEventListener('resize', handleWindowResize);
// unsubscribe from the event on component unmount
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
return [width, height]
}
export default useDeviceSize
Use case:
const [width, height] = useDeviceSize();
componentWillMount() lifecycle hook works both on server as well as client side. In your case server would not know about window or document during page serving, the suggestion is to move the code to either
Solution 1:
componentDidMount()
Or, Solution 2
In case it is something that you only want to perform in then you could write something like:
componentWillMount() {
if (typeof window !== 'undefined') {
console.log('window.innerHeight', window.innerHeight);
}
}
In the constructor of your class Component you can add
if (typeof window === 'undefined') {
global.window = {}
}
Example:
import React, { Component } from 'react'
class MyClassName extends Component {
constructor(props){
super(props)
...
if (typeof window === 'undefined') {
global.window = {}
}
}
This will avoid the error (in my case, the error would occur after I would click reload of the page).
Best solution ever
import dynamic from 'next/dynamic';
const Chart = dynamic(()=> import('react-apexcharts'), {
ssr:false,
})
A bit late but you could also consider using Dynamic Imports from next turn off SSR for that component.
You can warp the import for your component inside a dynamic function and then, use the returned value as the actual component.
import dynamic from 'next/dynamic'
const BoardDynamic = dynamic(() => import('../components/Board.tsx'), {
ssr: false,
})
<>
<BoardDynamic />
</>
global?.window && window.innerHeight
It's important to use the operator ?., otherwise the build command might crash.
I have to access the hash from the URL so I come up with this
const hash = global.window && window.location.hash;
Here's an easy-to-use workaround that I did.
const runOnClient = (func: () => any) => {
if (typeof window !== "undefined") {
if (window.document.readyState == "loading") {
window.addEventListener("load", func);
} else {
func();
}
}
};
Usage:
runOnClient(() => {
// access window as you like
})
// or async
runOnClient(async () => {
// remember to catch errors that might be raised in promises, and use the `await` keyword wherever needed
})
This is better than just typeof window !== "undefined", because if you just check that the window is not undefined, it won't work if your page was redirected to, it just works once while loading. But this workaround works even if the page was redirected to, not just once while loading.
I was facing the same problem when i was developing a web application in next.js This fixed my problem, you have to refer to refer the window object in a life cycle method or a react Hook. For example lets say i want to create a store variable with redux and in this store i want to use a windows object i can do it as follows:
let store
useEffect(()=>{
store = createStore(rootReducers, window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__())
}, [])
....
So basically, when you are working with window's object always use a hook to play around or componentDidMount() life cycle method
I wrapped the general solution (if (typeof window === 'undefined') return;) in a custom hook, that I am very pleased with. It has a similiar interface to reacts useMemo hook which I really like.
import { useEffect, useMemo, useState } from "react";
const InitialState = Symbol("initial");
/**
*
* #param clientFactory Factory function similiar to `useMemo`. However, this function is only ever called on the client and will transform any returned promises into their resolved values.
* #param deps Factory function dependencies, just like in `useMemo`.
* #param serverFactory Factory function that may be called server side. Unlike the `clientFactory` function a resulting `Promise` will not be resolved, and will continue to be returned while the `clientFactory` is pending.
*/
export function useClientSideMemo<T = any, K = T>(
clientFactory: () => T | Promise<T>,
deps: Parameters<typeof useMemo>["1"],
serverFactory?: () => K
) {
const [memoized, setMemoized] = useState<T | typeof InitialState>(
InitialState
);
useEffect(() => {
(async () => {
setMemoized(await clientFactory());
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return typeof window === "undefined" || memoized === InitialState
? serverFactory?.()
: memoized;
}
Usage Example:
I am using it to dynamically import libaries that are not compatible with SSR in next.js, since its own dynamic import is only compatible with components.
const renderer = useClientSideMemo(
async () =>
(await import("#/components/table/renderers/HighlightTextRenderer"))
.HighlightTextRendererAlias,
[],
() => "text"
);
As you can see I even implemented a fallback factory callback, so you may provide a result when initially rendering on the server aswell. In all other aspects this hook should behave similiar to reacts useMemo hook. Open to feedback.
For such cases, Next.js has Dynamic Import.
A module that includes a library that only works in the browser, it's suggested to use Dynamic Import. Refer
Date: 06/08/2021
Check if the window object exists or not and then follow the code along with it.
function getSelectedAddress() {
if (typeof window === 'undefined') return;
// Some other logic
}
For Next.js version 12.1.0, I find that we can use process.title to determine whether we are in browser or in node side. Hope it helps!
export default function Projects(props) {
console.log({ 'process?.title': process?.title });
return (
<div></div>
);
}
1. From the terminal, I receive { 'process?.title': 'node' }
2. From Chrome devtool, I revice { 'process?.title': 'browser' }
I had this same issue when refreshing the page (caused by an import that didn't work well with SSR).
What fixed it for me was going to pages where this was occurring and forcing the import to be dynamic:
import dynamic from 'next/dynamic';
const SomeComponent = dynamic(()=>{return import('../Components/SomeComponent')}, {ssr: false});
//import SomeComponent from '../Components/SomeComponent'
Commenting out the original import and importing the component dynamically forces the client-side rendering of the component.
The dynamic import is covered in Nextjs's documentation here:
https://nextjs.org/docs/advanced-features/dynamic-import
I got to this solution by watching the youtube video here:
https://www.youtube.com/watch?v=DA0ie1RPP6g
You can define a state var and use the window event handle to handle changes like so.
const [height, setHeight] = useState();
useEffect(() => {
if (!height) setHeight(window.innerHeight - 140);
window.addEventListener("resize", () => {
setHeight(window.innerHeight - 140);
});
}, []);
You can try the below code snippet for use-cases such as - to get current pathname (CurrentUrl Path)
import { useRouter } from "next/router";
const navigator = useRouter()
console.log(navigator.pathname);
For anyone who somehow cannot use hook (for example, function component):
Use setTimeout(() => yourFunctionWithWindow()); will allow it get the window instance. Guess it just need a little more time to load.
I want to leave this approach that I found interesting for future researchers. It's using a custom hook useEventListener that can be used in so many others needs.
Note that you will need to apply a little change in the originally posted one, like I suggest here.
So it will finish like this:
import { useRef, useEffect } from 'react'
export const useEventListener = (eventName, handler, element) => {
const savedHandler = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
element = !element ? window : element
const isSupported = element && element.addEventListener
if (!isSupported) return
const eventListener = (event) => savedHandler.current(event)
element.addEventListener(eventName, eventListener)
return () => {
element.removeEventListener(eventName, eventListener)
}
}, [eventName, element])
}
If it is NextJS app and inside _document.js, use below:
<script dangerouslySetInnerHTML={{
__html: `
var innerHeight = window.innerHeight;
`
}} />

Reset isLoading property of NativeBase button when on-press handler fails

I have made a "one shot" button wrapper component which intercepts the onPress event, sets the isLoading property and then calls the original onPress handler. This sets the button to the disabled loading spinner state while a slow API call is made, provides nice UI feedback and possibly prevents the user from double-clicking.
The original onPress handler has form-field validation, and if this fails I want to cancel the process and let the user correct the input data. So I am returning false to indicate this, which my wrapper catches, however I find I cannot set the button's isLoading back to false - it doesn't change the button state nor remove the disabled, so it's stuck spinning forever.
My button wrapper is this:
import React, {useState} from "react"
import {StyleSheet} from "react-native";
import {Button as ButtonNB, IButtonProps, useTheme} from "native-base"
interface IMyButtonProps extends IButtonProps {
oneShot?: boolean
}
const Button = (props: IMyButtonProps) => {
const [isLoading, setIsLoading] = useState(false)
const {colors} = useTheme()
return <ButtonNB px="25"
style={styles.button}
isLoading={isLoading} spinnerPlacement="end" isLoadingText="Saving..." backgroundColor={colors['primary']['500']}
{...props}
onPress={event => {
if (props.oneShot) setIsLoading(true)
if (props.onPress) {
if(!props.onPress(event)) {
console.debug('cancelled loader')
setIsLoading(false) // <--- DOESN'T WORK
}
}
}}
/>
}
export default Button
Calling code simplified:
<Button
onPress={() => onSave()}
oneShot={true}
testID="prepare-submit-button"
>
{saveSubjectButtonText}
</Button>
async function onSave(){
// on validation failure, just stop and return false
if(!validate()){
return false
}
else {
// do api stuff...
// update local state...
navigation.navigate('Home')
}
}
When validation fails, I do get the 'cancelled loader' log, but the setIsLoading(false) has no effect.
I am viewing in iOS, package versions:
"native-base": "~3.4",
"react": "17.0.2",
"react-dom": "~17.0.2",
"react-native": "0.67.4",
Their documentation: https://docs.nativebase.io/button#h3-loading
I've looked at their issues: https://github.com/GeekyAnts/NativeBase/issues?q=is%3Aissue+isLoading+button+is%3Aclosed
I tried an alternative approach which worked, and now I think I have realised the original problem. I think because the setIsLoading(true) in the onPress handler is re-rendering the button due to state change, the remainder of the function closure is the old state scope when the button wasn't spinning anyway.
So this was nothing to do with NativeBase nor ReactNative but a React gotcha which I think has caught me out before. Each time the render function is called, a new scope becomes active, and even though the old scope may still be finishing running code threads in memory they aren't "attached" to the display/DOM any more. At least that's how I picture it.
I changed it to use reference functions to be able to call the "setLoading" from the parent component. Thanks to this answer: Call child function from parent component in React Native
My button component is now defined like this:
import React, {forwardRef, useImperativeHandle, useState} from "react"
const Button = (props: IMyButtonProps, ref) => {
const [isLoading, setIsLoading] = useState(false)
useImperativeHandle(ref, () => ({
cancelLoading: () => { setIsLoading(false) },
}))
...
export default forwardRef(Button)
and called like this from the parent view:
const submitButtonRef = useRef()
async function onSave(){
// on validation failure, tell the button to cancel loading
if(!validate()){
submitButtonRef.current.cancelLoading()
return
}
...
<Button
onPress={() => onSave()}
ref={submitButtonRef}
And now if the validation fails, the spinner is stopped and you can click the button again.

Invalid hook call error when trying to set state

I have a scenario where I am forced to call a trigger method to show a modal from two different places, one using a hotkey combination and another by clicking on a toolbar button. In order to do so I have the following code, where I call the triggerCustomLinkModal to set the state but then I am hit with the Invalid Hook call error.
import { useState, useCallback, useEffect } from "react"
import { Dialog } from "#blueprintjs/core"
const useLocalState = () => {
const [isShown, setIsShown] = useState(false)
const setState = useCallback((state) => {
setIsShown(state)
})
const getState = useCallback(() => {
return isShown
})
return {
setState,
getState
}
}
export const CustomLinkModalUI = () => {
const { getState } = useLocalState()
return (
<>
<Dialog isOpen={getState()} />
</>
)
}
export const triggerCustomLinkModal = () => {
const { setState } = useLocalState()
setState()
}
Expanding from Chris answer in the comments ( You can't use hooks outside React components. -> so you can't call useLocalState() inside triggerCustomLinkModal since triggerCustomLinkModal is not a React component ):
You don't really need the useCallback hook or even the functions itself. Aaccording to react docs :
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
This also means that using useCallback hook to set a state it doesn't really make sense (because useCallback role is just to return a memoized callback)
What you basically need is a state set up in the closest parrent component and pass the setIsShown as a prop as well as the isShown function.
Your current implementation, even if it weren't for the error, it wouldn't refer to the same state since on each useLocalState() you are initializing a fresh new state (so you are not pointing to the same state in CustomLinkModalUI and triggerCustomLinkModal)

React - call function in child component if parent state changes

I've got an invisible fullscreen event processing component that catches and processes all mouse events. On certain events I want to call a particular function in a child component. I created a state variable in the parent component. I am listening with use effect in the child component if there are some changes to the state variable in the parent component. However the effect hook is being called to often. Is there a better way of doing this? In particular I want to set drawing=true on mouse down and drawing=false on mouse up. If drawing transitions from true to false I want so save something in the database. Therefore it's important that the function in useState is being called only once.
I have these two possible solutions.
If you want useEffect not to be called in the first render, you have to use a custom hook, probably called useUpdateEffect. Here is a usage guide from react-use package
use useImperativeHandle to run a function in child from parent.
example:
import React, { forwardRef, useRef } from 'react';
export default function Parent() {
const ref = useRef<Ref>(null!);
const handleCallFunction = (): void => ref?.current?.functionInChild();
return (
<div>
<button onClick={handleCallFunction}>
call a function in child from parent
</button>
<Child ref={ref} />
</div>
);
}
type Ref = {
functionInChild: () => void;
};
type Props = {};
const Child = forwardRef<Ref, Props>((props, ref) => {
const functionInChild = () => {
console.log('called from parent');
};
React.useImperativeHandle(ref, () => ({ functionInChild }));
return <div>Child!</div>;
});
You have to put this in your code
useEffect(()=>{
/*Query logic*/
console.log('i fire once');},[your_variable]);
you_variable which you want to change when this variable changes his value

pass custom event properties with fireEvent (testing-library and jest)

What I would like to do
I would like to pass some custom properties to an event during some tests (using react-testing-library and jest). I am using the fireEvent function. I understand from the docs that the properties in the second argument are added to the event. This is what I can't do at the moment.
Minimal reproducible example
import React from 'react'
import { render, fireEvent } from '#testing-library/react'
test('check event', () => {
const DOM = render(
<div
onClick={event => {
console.log(event.foo)
}}
>
Click Me
</div>
)
// here I am expecting foo to be a property on the event passed
// to the event handler. But that doesn't happen.
fireEvent.click(DOM.getByText('Click Me'), { foo: 'bar' })
})
The result is that undefined is logged.
Approaches I've tried / thoughts
I have tried various variations of this using different event types, using createEvent, using custom events, manually adding an event listener etc. and I can't seem to access any of the event properties I pass in with any of these variations.
I've looked under the cover a bit at what's going on in fireEvent here. It certainly looks like those additional properties should be added.
The fireEvent function allows initializing intrinsic properties of Event objects, but it doesn't add arbitrary properties. For example, calling
fireEvent.click(DOM.getByText('Click Me'), { button: 2 })
dispatches a MouseEvent with its button property set to 2.
Note that you may want to revisit how you're testing your component—passing custom properties to an event in a test runs counter to the guiding principle of the DOM Testing Library:
The more your tests resemble the way your software is used, the more confidence they can give you.
However, your workflow is technically possible by passing custom properties to the detail property of a CustomEvent. This approach could be feasible depending on your goals, and perhaps in conjunction with an onClick handler. For example, this logs bar:
import React, { useEffect, useRef } from 'react'
import { fireEvent, render } from '#testing-library/react'
test('custom event', () => {
const MyComponent = ({ customEventHandler, children }) => {
const ref = useRef(null)
useEffect(() => {
ref.current.addEventListener('my-event', customEventHandler)
return () => {
ref.current.removeEventListener('my-event', customEventHandler)
}
}, [customEventHandler])
return <div ref={ref}>{children}</div>
}
const customEventHandler = (event) => {
console.log(event.detail.foo)
}
const { getByText } = render(
<MyComponent customEventHandler={customEventHandler}>
Click Me
</MyComponent>
)
const elem = getByText('Click Me')
const event = createEvent(
'my-event',
elem,
{
detail: {
foo: 'bar',
},
},
{ EventType: 'CustomEvent' }
)
fireEvent(elem, event)
})

Categories

Resources