Error importing custom hooks in React 16.7.0-alpha - reactjs

Been playing around with the new hook RFC in react and can't get my custom hook working properly. Not sure if what is going on is on my end or a bug with the React alpha itself.
I've been trying to create a click outside hook. I was able to get it working with this code.
./dropdown_builtin_hooks
const DropDownWrapper = React.memo(props => {
const { user, className } = props;
const ref = useRef(null);
const [active, setActive] = useState(false);
useEffect(() => {
const handleDOMClick = event => {
console.log(event.target);
if (active && !!ref && !(event.target === ref.current || ref.current.contains(event.target))) {
console.log("Clicked outside of wrapped component");
setActive(false);
}
};
window.addEventListener("click", handleDOMClick);
return () => {
window.removeEventListener("click", handleDOMClick);
};
});
const handleDropDown = (): void => {
setActive(true);
};
return (
<div ref={ref} className={className} >
<Toggler onClick={handleDropDown}>
{active ? (
<StyledDropUpArrow height="1.5em" filled={false} />
) : (
<StyledDropDownArrow height="1.5em" filled={false} />
)}
</Toggler>
{active && (
<DropDown/>
)}
</div>
);
});
export default DropDownWrapper;
However when I try to wrap this in a custom hook that I can reuse and import it into my component. Something like this...
./hooks
export function useClickedOutside<RefType = any>(
initialState: boolean = false,
): [React.RefObject<RefType>, boolean, Function] {
const ref = useRef(null);
const [active, setActive] = useState(initialState);
useEffect(() => {
const handleDOMClick = event => {
console.log(event.target);
if (active && !!ref && !(event.target === ref.current || ref.current.contains(event.target))) {
console.log("Clicked outside of wrapped component");
setActive(false);
}
};
window.addEventListener("click", handleDOMClick);
return () => {
window.removeEventListener("click", handleDOMClick);
};
});
return [ref, active, setActive];
}
./dropdown_custom_hook
const DropDownWrapper = React.memo(props => {
const { user, className } = props;
const [ref, active, setActive] = useClickedOutside(false);
const handleDropDown = (): void => {
setActive(true);
};
return (
<div ref={ref} className={className} >
<Toggler onClick={handleDropDown}>
{active ? (
<StyledDropUpArrow height="1.5em" filled={false} />
) : (
<StyledDropDownArrow height="1.5em" filled={false} />
)}
</Toggler>
{active && (
<DropDown/>
)}
</div>
);
});
export default DropDownWrapper;
At first I figured it was an issue with hot reloading, but after removing that I am still getting this error:
Uncaught Error: Hooks can only be called inside the body of a function
component.
I only get this issue when I use imports and exports. If I copy the same custom hook function and paste it above my component it works properly.
I assume I'm doing something dumb or haven't read the docs well enough.
Cheers

Related

How to scroll into child component in a list from parent in react?

Hello guys I have an issue that may be simple but I'm stuck.
I have a parent that call an endpoint and render a list of child components once the data is received, at the same time in the URL could (or not) exists a parameter with the same name as the "name" property of one of the child components, so if parameter exists I need to scroll the page down until the children component that have the same "name" as id.
Here is part of the code:
const ParentView = () => {
const [wines, setWines] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const params = new URLSearchParams(document.location.search);
const isMx = params.get('lang') ? false : true;
const wineId = params.get('wine');
const ref = createRef();
const scroll = () => ref && ref.current && ref.current.scrollIntoView({ behavior: 'smooth' });
React.useEffect(() => {
retrieveData();
}, []);
React.useEffect(() => {
if (!isEmptyArray(wines) && !loading && wineId) scroll();
}, [wineId, wines, loading]);
function renderWines() {
if (loading) return <Loading />;
if (isEmptyArray(wines) && !loading) return <h2>No items found</h2>;
if (!isEmptyArray(wines) && !loading)
return (
<React.Fragment>
{wines
.filter(p => p.status === 'published')
.map((w, idx) => (
<ChildComponent
wine={w}
isMx={isMx}
idx={idx}
openModal={openModal}
ref={wineId === w.name.toLowerCase() ? ref : null}
/>
))}
</React.Fragment>
);
}
return (
<React.Fragment>
{renderWines()}
</React.Fragment>
);
};
And this is the child component...
import React, { forwardRef } from 'react';
import { Row,} from 'reactstrap';
const WineRow = forwardRef(({ wine, isMx, idx, openModal }, ref) => {
const {
name,
} = wine;
// const ref = React.useRef();
React.useEffect(() => {
// console.log({ ref, shouldScrollTo });
// shouldScrollTo && ref.current.scrollIntoView({ behavior: 'smooth' });
}, []);
return (
<Row id={name} ref={ref}>
...content that is irrelevant for this example
</Row>
);
});
Of course I remove a lot of irrelevant code like retrieveData() function and all the logic to handle the data from api
I've been trying many ways but I can't make it works :(
Well after a headache I just realized that I don't need react to do this 😂
so I just fixit with vanilla js 🤷🏻‍♂️
Parent:
const Public = () => {
const [wines, setWines] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const params = new URLSearchParams(document.location.search);
const isMx = params.get('lang') ? false : true;
const wineId = params.get('wine');
React.useEffect(() => {
retrieveData();
}, []);
React.useEffect(() => {
if (!isEmptyArray(wines) && !loading && wineId) scroll(wineId);
}, [wineId, wines, loading]);
const scroll = wineId => document.getElementById(wineId).scrollIntoView({ behavior: 'smooth' });
const retrieveData = async () => {
....logic to handle data
};
function renderWines() {
if (loading) return <Loading />;
if (isEmptyArray(wines) && !loading) return <h2>No items found</h2>;
if (!isEmptyArray(wines) && !loading)
return (
<React.Fragment>
{wines
.filter(p => p.status === 'published')
.map((w, idx) => (
<WineRow wine={w} isMx={isMx} idx={idx} />
))}
</React.Fragment>
);
}
return (
<React.Fragment>
{renderWines()}
</React.Fragment>
);
};
and children:
const WineRow =({ wine, isMx, idx,}) => {
const {
name,
} = wine;
return (
<Row id={name.toLowerCase()}>
...content that is irrelevant for this example
</Row>
);
};
And that's it 😂 sometimes we are used to do complex things that we forgot our basis 🤦🏻‍♂️
Hope this help someone in the future

How to Use componentDidMount() in Functional Component to execute a function

I have a functional component which had a button to call a method in it. Now i want to get rid of the button and call that method without any actions once the component loads.
I am making API calls inside this method and passing on the results to another component.
Also I am replacing the button with a progress bar meaning when a "search" is taking place, display the progress bar but I am having no luck. What am I doing wrong ?
export const Search = (props) => {
const { contacts, setContacts, onSearchComplete } = props;
const [msgBox, setMsgBox] = useState(null);
const [loading, setLoading] = useState(false);
const onSearch = async () => {
setLoading(true);
const emails = contacts
.filter(x => x.isChecked)
.map(item => item.emailAddress);
try {
const searchResults = await AppApi.searchMany(emails);
let userList = [];
for (let i = 0; i < searchResults.length; i++) {
//process the list and filter
}
userList = [...userList, ..._users];
}
onSearchComplete(userList); //passing the results.
} catch (err) {
console.log({ err });
setMsgBox({ message: `${err.message}`, type: 'error' });
}
setLoading(false);
}
return (
<Box>
{loading ? <LinearProgress /> : <Box>{msgBox && (<a style={{ cursor: 'pointer' }} onClick={() => setMsgBox(null)} title="Click to dismiss"><MessageBox type={msgBox.type || 'info'}>{msgBox.message}</MessageBox></a>)}</Box>}
/*{onSearch()}*/ // function that was executed onclick.
</Box>
);
}
You will want to use the useEffect hook with an empty dependency array which will make it act as componentDidMount source.
export const Search = (props) => {
const { contacts, setContacts, onSearchComplete } = props;
const [msgBox, setMsgBox] = useState(null);
const [loading, setLoading] = useState(false);
const onSearch = async () => {
...
}
useEffect(() => {
onSearch();
}, []);
return (
<Box>
{loading ? <LinearProgress /> : <Box>{msgBox && (<a style={{ cursor: 'pointer' }} onClick={() => setMsgBox(null)} title="Click to dismiss"><MessageBox type={msgBox.type || 'info'}>{msgBox.message}</MessageBox></a>)}</Box>}
</Box>
);
}

How to set a state that is on a component?

On reactjs, how can I setNotify again a state that is on a component?
import React, { useState } from 'react'
const NotificationError = (props) => {
const [notify, setNotify] = useState(false);
// if (props.message === "") {
// props.message = "Some Error"
// }
// if (props.message !== "") {
// setNotify(false)
// }
// if (props) {
// const [notify] = useState(true)
// }
console.log("notify.state:", props)
const closeNotification = (e) => {
console.log("Should be closing notification")
setNotify(e)
}
return (
<div className="notification is-danger" style={notify ? {display: 'none'} : {display: 'block'}}>
<button className="delete" onClick={() => closeNotification(true)}></button>
Error: {props.message}
</div>
)
}
export default NotificationError
If I use the following:
if (props) {
const [notify] = useState(true)
}
I get the error,
Line 17:26: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
If I use the following
if (props.message !== "") {
setNotify(true)
}
It throws the following...
Error: Too many re-renders. React limits the number of renders to
prevent an infinite loop.
Simply, I am not understanding this. Can you please help? :(
Rewrite you logic to something like:
const NotificationError = (props) => {
const [notify, setNotify] = useState(false);
useEffect(() => {
if (props.message === "") {
props.setMessage('Some Error');
}
setNotify(false);
}, [props.message]);
return (
<div
className="notification is-danger"
style={notify ? { display: "none" } : { display: "block" }}
>
<button className="delete" onClick={() => setNotify(true)}></button>
Error: {props.message}
</div>
);
};
Props are immutable so if you want to change a message you should pass a callback.
Also, take a read about Rules of Hooks.
Use a useEffect hook for such cases. It works similar to componentDidMount and componentDidUpdate in class component. It means the function that you pass as the first argument of useEffect hook triggers the first time when your component mounts and then every time any of the elements of the array changes that you pass as the second argument.
Here is the code example:
const NotificationError = (props) => {
const [notify, setNotify] = useState(false);
useEffect(() => {
if(props.message != '') {
setNotify(false);
}
}, [props.message])
const closeNotification = (e) => {
console.log("Should be closing notification")
setNotify(e)
}
return (
<div className="notification is-danger" style={notify ? {display: 'none'} : {display: 'block'}}>
<button className="delete" onClick={() => closeNotification(true)}></button>
Error: {props.message}
</div>
)
}

React: save ref to state in a custom hook

I want to create a ref to an element, save it in state and use it somewhere else, down the line. Here is what I have so far:
const Header = () => {
const topElement = useRef();
const { setRootElement } = useScrollToTop();
useEffect(() => {
setRootElement(topElement);
}, []);
return (
<div ref={topElement}>
...
</div>
)
}
The useScrollToTop hook:
export const useScrollToTop = () => {
const [rootElement, setRootElement] = useState();
const scrollToTop = () => {
rootElement.current.scrollIntoView();
};
return {
scrollToTop: scrollToTop,
setRootElement: setRootElement
};
};
And in a different component:
const LongList = () => {
const { scrollToTop } = useScrollToTop();
return (
<div>
....
<button onClick={() => scrollToTop()} />
</div>
);
}
The setRootElemet works okay, it saves the element that I pass to it but when I call scrollToTop() the element is undefined. What am I missing here?
As hooks are essentially just functions, there is no state shared between calls. Each time you call useScrollToTop you are getting a new object with its own scrollToTop and setRootElement. When you call useScrollToTop in LongList, the returned setRootElement is never used and therefore that instance rootElement will never have a value.
What you need to do is have one call to useScrollToTop and pass the returned items to their respective components. Also, instead of using a state in the hook for the element, you can use a ref directly and return it.
Putting these together, assuming you have an App structure something like:
App
Header
LongList
Hook:
export const useScrollToTop = () => {
const rootElement = useRef();
const scrollToTop = () => {
rootElement.current.scrollIntoView();
};
return {
scrollToTop,
rootElement,
};
};
App:
...
const { scrollToTop, rootElement } = useScrollToTop();
return (
...
<Header rootElementRef={rootElement} />
<LongList scrollToTop={scrollToTop} />
...
);
Header:
const Header = ({ rootElementRef }) => {
return (
<div ref={rootElementRef}>
...
</div>
);
}
LongList:
const LongList = ({ scrollToTop }) => {
return (
<div>
...
<button onClick={() => scrollToTop()} />
</div>
);
}
The issue probably is topElement would be null initially and useEffect would trigger setRootElement with null. You would need to keep topElement in state variable and check when it changes and set the value inside your JSX as
const [topElement, setTopElement] = useState(null);
useEffect(() => {topElement && setRootElement(topElement);}, [topElement])
return (
<div ref={(ref) => setTopElement(ref)}>
...
</div>
);

How to avoid extra renders in my component to use react hooks

I try to use react hooks instead of class-based components and have some problem with performance.
Code:
import React, { memo, useCallback, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let counter = -1;
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(!toggleValue), [
toggleValue,
setToggleValue
]);
return [toggleValue, toggler];
}
const Header = memo(({ onClick }) => {
counter = counter + 1;
return (
<div>
<h1>HEADER</h1>
<button onClick={onClick}>Toggle Menu</button>
<div>Extra Render: {counter}</div>
</div>
);
});
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(
() => {
toggle(!visible);
},
[toggle, visible]
);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
export default Dashboard;
Here is an example of what I wanna do: Example.
As you see, there are extra renders in my Header component.
My question: Is it possible to avoid extra renders to use react-hooks?
Change your custom hook useToggle to use functional state setter, like this
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(toggleValue => !toggleValue));
return [toggleValue, toggler];
}
and use it like this :
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(
() => {
toggle();
}, []
);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
Complete example : https://codesandbox.io/s/z251qjvpw4
Edit
This can be simpler (thanks to #DoXicK)
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(toggleValue => !toggleValue), [setToggleValue]);
return [toggleValue, toggler];
}
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
return (
<>
<Header onClick={toggle} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
This is an issue with useCallback get invalidate too often. (there is a conversation about this on React repo here: https://github.com/facebook/react/issues/14099)
since useCallback will be invalidated every time toggle value change and return a new function, then passing a new handleMenu function to <Header /> cause it re-render.
A workaround solution is to create a custom useCallback hook:
(Copied from https://github.com/facebook/react/issues/14099#issuecomment-457885333)
function useEventCallback(fn) {
let ref = useRef();
useLayoutEffect(() => {
ref.current = fn;
});
return useMemo(() => (...args) => (0, ref.current)(...args), []);
}
Example: https://codesandbox.io/s/1o87xrnj37
If you use the callback pattern to update state, you would be able to avoid extra re-renders since the function need not be created again and again and you use just create handleMenu on first render
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(() => {
toggle(visible => !visible);
}, []);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
Working Demo

Resources