I've created a custom Hook that detects if a click was done outside of a component:
import { useEffect, useState } from 'react';
const useOutsideClick = (ref) => {
const [clickOutside, setClickOutside] = useState(false);
useEffect(() => {
const handleClick = (e) => {
ref.current?.contains(e.target)
? setClickOutside(false)
: setClickOutside(true);
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [ref]);
return clickOutside;
};
export default useOutsideClick;
I'm using the Hook on a main component. After the user clicks outside the component, the Hook needs to reset to its initial state (outsideClick = false):
const App = () => {
const [activeComponent, setActiveComponent] = useState(null);
const dropDownRef = useRef();
const outsideClick = useOutsideClick(dropDownRef);
useEffect( () => {
if(outsideClick){
setActiveComponent('WhatAreYouWorkingOn');
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// At this point, outsideClick needs to be false again
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
}, [outsideClick, setActiveComponent]);
return (
<div className = 'DropDown' ref = {dropDownRef}/>
);
}
export default App;
How can I reset useOutsideClick to its initial state?
Related
just started to learn Next Js. I wanna hide dropdown when i clicked outside the button. Code works fine in create-react-app. But i tried to implement in nextjs, it doesnt working.
const LanguageRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
link tag
<a onClick={() => setLanguageDD((prev) => !prev)}>Language </a>
Does useEffect work in Nextjs?
Working Solution:
const LanguageRef = useRef();
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
console.log("useeffect")
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target) &&
LanguageDDRef.current &&
!LanguageDDRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
<a onClick={() => setLanguageDD((prev) => !prev) ref={LanguageDDRef}}>Language </a>
I reworked and also splitted the clickOutside function to a Custom Hook, because it can reuse at the other components. Like this:
import React, { useEffect } from 'react';
export const useClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (e) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref?.current || ref.current.contains(e.target)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
};
}, [handler, ref]);
};
Finally, you just need import and use useClickOutside hook, like this:
import React, { useRef, useState } from 'react';
import { useClickOutside } from '../src/hooks';
export default function Home() {
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(true);
useClickOutside(LanguageDDRef, () => setLanguageDD(false));
const handleToggleButton = () => {
setLanguageDD((prev) => !prev);
};
return (
<button onClick={handleToggleButton} ref={LanguageDDRef}>
{languageDD ? 'Show' : 'Hide'}
</button>
);
}
I have created one wrapper component where I put my react context.
Inside that wrapper component I have used useEffect() hook where I fetch values from api and try to update context default values.
In my child component I tried to fetch context values but only default value of that context is fetched. So it seems that useEffect hook didnt updated my context object.
Here is wrapper component:
export const CorporateWrapper = ({ apiBaseUrl, children }) => {
const [corporateContextDefaults, setCorporateContextDefaults] = useState({});
useEffect(() => {
(async () => {
try {
const json = await fetchCorporateUserDetails(apiBaseUrl, getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log(json.message);
return {
notFound: true,
};
}
console.log(json.data);
setCorporateContextDefaults({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
} catch (e) {
console.log(e.message);
}
})();
}, []);
return (
<CorporateProvider value={corporateContextDefaults}>
{children}
</CorporateProvider>
);
};
Here is CorporateProvider component:
import React, { useState, useContext } from "react";
const CorporateContext = React.createContext({corporateId: null, corporateRole: null,
corporateAdmin: null, corporateSuperAdmin: null});
const UpdateCorporateContext = React.createContext({});
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const useUpdateCorporateContext = () => {
return useContext(UpdateCorporateContext);
};
export const CorporateProvider = ({ value, children }) => {
const [details, setDetails] = useState(value);
return (
<CorporateContext.Provider value={details}>
<UpdateCorporateContext.Provider value={setDetails}>
{children}
</UpdateCorporateContext.Provider>
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Here is how I try to fetch context value from child component which is wrapped under wrapper component:
const { corporateId } = useCorporateContext();
Besides prop value updates in a hook, I need to bind to events that get triggered in the hook too. So the consumer of the hook can bind to the event-like addEventListner, removeEventListener. How do I do this?
What I have so far:
import {useState, useEffect} from 'react';
interface MyHookProps {
name: string;
onChange: () => void;
}
const useNameHook = () : MyHookProps => {
const [name, setName] = useState<string>('Anakin');
const onChange = () => {
}
useEffect(() => {
setTimeout(() => {
setName('Vader');
// how to a raise an onChange event here that consumers could bind to?
}, 1000);
}, []);
return {
name,
onChange,
}
}
export default function App() {
const {name, onChange} = useNameHook();
const handleHookChange = () => {
console.info('hook changed', name);
}
return (
<div className="App">
<h1>Hello {name}</h1>
</div>
);
}
I think you can refer to the 'Declarative' pattern here.
Reading this article about 'Making setInterval Declarative with React Hooks' from Dan Abramov really changed my ways of thinking about the React hooks.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
So, my attempts to make this useName hook declarative is like below:
// hooks/useName.ts
import { useEffect, useRef, useState } from "react";
type Callback = (name: string) => void;
const useName: (callback: Callback, active: boolean) => string = (
callback,
active
) => {
// "Listener"
const savedCallbackRef = useRef<Callback>();
// keep the listener fresh
useEffect(() => {
savedCallbackRef.current = callback;
}, [callback]);
// name state
const [internalState, setInternalState] = useState("anakin");
// change the name after 1 sec
useEffect(() => {
const timeoutID = setTimeout(() => {
setInternalState("vader");
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
// react to the 'name change event'
useEffect(() => {
if (active) {
savedCallbackRef.current?.(internalState);
}
}, [active, internalState]);
return internalState;
};
export default useName;
and you can use this hook like this:
// App.ts
import useName from "./hooks/useName";
function App() {
const name = useName(state => {
console.log(`in name change event, ${state}`);
}, true);
return <p>{name}</p>;
}
export default App;
Note that the 'callback' runs even with the initial value ('anakin' in this case), and if you want to avoid it you may refer to this thread in SO:
Make React useEffect hook not run on initial render
Can someone explain what am I'm doing wrong?
I have a react functional component, where I use useEffect hook to fetch some data from server and put that data to state value. Right after fetching data, at the same useHook I need to use that state value, but the value is clear for some reason. Take a look at my example, console has an empty string, but on the browser I can see that value.
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Link to codesandbox.
The useEffect hook will run after your component renders, and it will be re-run whenever one of the dependencies passed in the second argument's array changes.
In your effect, you are doing console.log(value) but in the dependency array you didn't pass value as a dependency. Thus, the effect only runs on mount (when value is still "") and never again.
By adding value to the dependency array, the effect will run on mount but also whenever value changes (which in a normal scenario you usually don't want to do, but that depends)
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Not sure exactly what you need to do, but if you need to do something with the returned value from your endpoint you should either do it with the endpoint returned value (instead of the one in the state) or handle the state value outside the hook
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
// handle the returned value here
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
};
fetchData();
}, []);
// Or handle the value stored in the state once is set
if(value) {
// do something
}
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});