const FlashMessage = (props) => {
const [isOpen, setIsOpen] = useState(true);
const hideComponent = () => {
setisOpen(false);
};
useEffect(() => {
setIsOpen(true);
setTimeout(() => hideComponent(), 9000);
}, [props]);
return (
(props.flashMessage === true && isOpen) ?
<View style={styles.main}>
<Text style={styles.message}>{props.message}</Text>
</View>
: null
);
}
I have this Flash Message component in my React Native app, and sometimes, the Flash Message disappears after 2 second. It seems to appear on a random basis and it's probably due to a problem with useEffect and setTimeout, but I have not been able to figure out what might be causing this.
The effect you have with [props] as dependency doesn't make sense to me.
But you can have an isolated effect for the isOpen boolean.
useEffect(() => {
setTimeout(() => {
setIsOpen(false);
}, 9000);
}, [isOpen]);
Here is a full working example, simplified:
export default function App() {
const [show, setShow] = useState(false);
useEffect(() => {
setTimeout(() => {
setShow(false);
}, 2000);
}, [show]);
return (
<div className="App">
<button onClick={e => setShow(true)}>Show</button>
{show && <div>hello</div>}
</div>
);
}
Related
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
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>
);
}
const fetchMusic= () => {
return new Promise((resolve) =>
setTimeout(() => {
const music = musicList.sort(() => 0.5 - Math.random()).slice(0, 4);
resolve({ data: music});
}, 300)
);
};
export default fetchMusic;
const getRandomMusic = () => {
return fetchMusic().then((result) => result.data);
};
const Button = (props) => {
return (
<div>
<Button {...props} onClick={getRandomMusic.bind(this)} />
<SomeComponent />
<p>Some text here</p>
</div>
);
};
I want add a spinner while waiting for the promise to resolve .
fetchMusic is in some other file.I m importing it in a component .
TLDR
How about use useState and useCallback for that action
Answer
At terms of react, use State for loading action is right use case.
So, When to start function, use can setLoading(true) and after action you can setLoading(false) for make loading effect
const fetchMusic= () => {
return new Promise((resolve) =>
setTimeout(() => {
const music = musicList.sort(() => 0.5 - Math.random()).slice(0, 4);
resolve({ data: music});
}, 300)
);
};
export default fetchMusic;
const Button = (props) => {
const [loaidng, setLoading] = useState(false);
const getRandomMusic = useCallback(() => {
setLoading(true)
return fetchMusic().then((result) => {
setLoading(false);
result.data
});
},[]);
return (
<div>
<Button {...props} onClick={getRandomMusic.bind(this)} />
{loading && <Sipinner/>}
<SomeComponent />
<p>Some text here</p>
</div>
);
};
Reference
Example of loading
ETC
If you have any other question. Just give me comment please.
At a certain place of my code I'm accessing a state variable of my component from a call back ( UserCallback ) and I find the state variable has not updated from the initial value and call back is referring to the initial value. As I read in the documentation when variable is passed as one of array items then it should update the function when it is updated. Following is a sample code.
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const node = useRef(null);
useImperativeHandle(ref, () => ({
increment() {
setCount(count + 1);
}
}));
const clickListener = useCallback(
e => {
if (!node.current.contains(e.target)) {
alert(count);
}
},
[count]
);
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
return (
<div
ref={node}
style={{ width: "500px", height: "100px", backgroundColor: "yellow" }}
>
<h1>Hi {count}</h1>
</div>
);
});
const Parent = () => {
const childRef = useRef();
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current.increment()}>Click</button>
</div>
);
};
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
What I'm originally building is a custom confirmation modal. I have a state variable which set either display:block or display:none to the root element. Then if there is a click outside the component I need to close the modal by setting state variable to false. Following is the original function.
const clickListener = useCallback(
(e: MouseEvent) => {
console.log('isVisible - ', isVisible, ' count - ', count, ' !node.current.contains(e.target) - ', !node.current.contains(e.target))
if (isVisible && !node.current.contains(e.target)) {
setIsVisible(false)
}
},
[node.current, isVisible],
)
It doesn't get closed because isVisible is always false which is the initial value.
What am I doing wrong here?
For further clarifications following is the full component.
const ConfirmActionModal = (props, ref) => {
const [isVisible, setIsVisible] = useState(false)
const [count, setCount] = useState(0)
const showModal = () => {
setIsVisible(true)
setCount(1)
}
useImperativeHandle(ref, () => {
return {
showModal: showModal
}
});
const node = useRef(null)
const stateRef = useRef(isVisible);
const escapeListener = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsVisible(false)
}
}, [])
useEffect(() => {
stateRef.current = isVisible;
}, [isVisible]);
useEffect(() => {
const clickListener = e => {
if (stateRef.current && !node.current.contains(e.target)) {
setIsVisible(false)
}
};
// Attach the listeners on component mount.
document.addEventListener('click', clickListener)
document.addEventListener('keyup', escapeListener)
// Detach the listeners on component unmount.
return () => {
document.removeEventListener('click', clickListener)
document.removeEventListener('keyup', escapeListener)
}
}, [])
return (
<div ref={node}>
<ConfirmPanel style={{ display : isVisible ? 'block': 'none'}}>
<ConfirmMessage>
Complete - {isVisible.toString()} - {count}
</ConfirmMessage>
<PrimaryButton
type="submit"
style={{
backgroundColor: "#00aa10",
color: "white",
marginRight: "10px",
margin: "auto"
}}
onClick={() => {console.log(isVisible); setCount(2)}}
>Confirm</PrimaryButton>
</ConfirmPanel>
</div>
)
}
export default forwardRef(ConfirmActionModal)
You assign a function clickListener to document.addEventListener on component mount, this function has a closure on count value.
On the next render, the count value will be stale.
One way to solve it is implementing a function with refernce closure instead:
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
// countRef.current always holds the most updated state
const clickListener = e => {
if (!node.current.contains(e.target)) {
alert(countRef.current);
}
};
document.addEventListener("click", clickListener);
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
...
}
You can pass a callback to setIsvisible so you don't need isVisible as a dependency of the useCallback. Adding node.current is pointless since node is a ref and gets mutated:
const clickListener = useCallback((e) => {
setIsVisible((isVisible) => {//pass callback to state setter
if (isVisible && !node.current.contains(e.target)) {
return false;
}
return isVisible;
});
}, []);//no dependencies needed
While your clickListener does change when count changes you only bind the initial clickListener once on mount because your useEffect dependency list is empty. You could ad clickListener to the dependency list as well:
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, [clickListener]);
Side note: using node.current in a dependency list doesn't do anything as react does not notice any changes to a ref. Dependencies can only be state or props.
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