Restart react timer from parent component - reactjs

I have a small issue here:
I have two components, a parent, and a timer, which is one of the children of parent
the parent passes down to the child a delay and a callback. The timer will execute the callback every delay milliseconds.
this is the code for the timer:
interface TimerProps {
delayInMilliseconds: number;
callback: Function;
}
const Timer = (props: TimerProps) => {
const { delayInMilliseconds, callback } = props;
const [ timerId, setTimerId ] = React.useState(0);
React.useEffect(() => {
createTimer();
}, []);
const createTimer = () => {
setTimerId(setInterval(callback, delayInMilliseconds))
};
const stopTimer = () => {
clearInterval(timerId);
};
const restartTimer = () => {
stopTimer();
createTimer();
};
return <button onClick={restartTimer}>stop timer</button>;
};
So far, so good. The timer does what id needs to do, and the restartTimer function works.
What I'm trying do do now is to tie the restartTimer function to a button that is present on the parent component.
I've tried to use React.forwardRef with React.useImperativeHandle, but it's not entirely clear to me the mechanism behind it, and so far I haven't had any luck
Could anyone help me understand how to "expose" the child's restartTimer function to the parent element?

I ended up just making the functional component a class component, and using useRef to access it from the parent element
child:
interface TimerProps {
delayInMilliseconds: number;
callback: Function;
}
interface TimerState {
timerId: number;
}
class Timer extends React.Component<TimerProps, TimerState> {
constructor(props: TimerProps) {
super(props);
this.state = {
timerId: 0,
};
}
componentDidMount() {
this.createTimer();
}
createTimer = () => {
this.setState({
...this.state,
timerId: setInterval(this.props.callback, this.props.delayInMilliseconds),
});
};
stopTimer = () => {
clearInterval(this.state.timerId);
};
restartTimer = () => {
this.stopTimer();
this.createTimer();
};
render() {
return null;
}
}
export default Timer;
parent:
const handleRefreshButtonClick = () => {
if (timerRef) {
timerRef?.current?.restartTimer();
}
}
...
const timerRef = React.useRef();
return(
<Timer
ref={timerRef}
delayInMilliseconds={refreshDelay //5000}
callback={doOtherStuff}/>
)
...
<Button onClick={handleRefreshButtonClick} />

create the state and function in your parent component and pass that to your child component i.e timer component and handle it from your parent component.

Related

How to track app custom events in entire react app

I want to add functionality that will collect custom events in redux in entire react app.
What I want to achieve is to place all event functions it in one place and only use this functions in components in my app when I want to trigger some event.
interface IEventsLoggerContext {
[key: string]: (val?: any) => void
}
export const EventsLoggerContext = createContext<IEventsLoggerContext>({})
class EventsLogger extends Component<{}, any> {
constructor (props: Readonly<{}>) {
super(props)
}
// event methods like below
pageLoaded = () => {
// here will be saving the event to redux store
console.log('page loaded')
}
targetClick = (e: SyntheticEvent) => {
// here will be saving the event to redux store
console.log('target click', e.target)
}
// ...
render () {
return (
<EventsLoggerContext.Provider
value={{
pageLoaded: this.pageLoaded,
targetClick: this.targetClick
}}
>
{this.props.children}
</EventsLoggerContext.Provider>
)
}
}
export default EventsLogger
I want to make all event log actions available in app so I wrapped all into my event provider:
<EventsLogger>
...
</EventsLogger>
And using in component like this:
const MainApp: React.FC = () => {
const { pageLoaded } = useContext(EventsLoggerContext)
useEffect(() => {
pageLoaded()
}, [pageLoaded])
return (
<div>Main App page</div>
)
}
Is this correct way to do this or is there maybe better approach to get functionality like this?
Using React Context is a clever way to solve this although it will require more code when adding more events compared to simply using the browser native window.dispatchEvent() function.
// SomeComponent.tsx
export const SomeComponent : FC = props => {
useEffect(() => {
const pageLoadEvent = new CustomEvent(
"pageLoaded",
{
detail : {
at: Date.now()
}
}
);
window.dispatchEvent(pageLoadEvent);
}, []):
// ...
}
// SomeOtherComponent.tsx
export const SomeOtherComponent : FC = props => {
useEffect(() => {
window.addEventListener("pageLoaded", onPageLoaded);
return () => window.removeEventListener("pageLoaded", onPageLoaded);
}, []);
function onPageLoaded(e: CustomEvent) {
// Do whatever you want here :)
}
// ...
}

ReactJS: Trigger event from child in parent component

i have two components: 1.Parent 2.Child
there is an event in child component called onChange() which return a value.
i want to receive the value which was returned from OnChange() in componentDidMount() in parent component.
Example:
class Parent extends PureComponent {
componentDidMount() {
let value = CHILD.onChange(); //triggered when ever onChange()
}
render(){
return(
<Child />
)
}
}
const Child = () => {
const onChange = () => {
const value = 1
return value;
};
}
class Parent extends PureComponent {
handleChildChange = value => {
//Do your stuff with value, pass it to the state and take it from there if you like
}
render(){
return(
<Child handleChange={this.handleChildChange} />
)
}
}
const Child = (props) => {
const onChange = () => {
value = 1
props.handleChange(value);
}
};
}

Jest: How to test function that calls bind(this)

I have a Parent component that looks like this:
export class Header extends Component {
constructor(props) {
super(props)
this.state = { activeTab: TAB_NAMES.NEEDS_REVIEW }
}
filterByNeedsReview() {
const { filterByNeedsReviewFn } = this.props
this.setState({ activeTab: TAB_NAMES.NEEDS_REVIEW })
filterByNeedsReviewFn()
}
render() {
return (
...
<FilterTab
...
onClick={this.filterByNeedsReview.bind(this)}
/>
...
)
}
}
I'm trying to test that the child was loaded with the right props. Originally I had it as an anonymous function: onClick={ () => this.filterByNeedsReview() } but I couldn't test that so I tried to move on to bind(this) instead.
However, I'm having issues mocking out the bind function:
it('renders a filter tab with the right props for needs review', () => {
const bounded = jest.fn()
const boundedFilterByNeedsReview = jest.fn(() => {
return { bind: bounded }
})
Header.prototype.filterByNeedsReview = boundedFilterByNeedsReview
expect(
shallowRender()
.find(FilterTab)
.findWhere(node =>
_.isMatch(node.props(), {
... // other props
onClick: bounded, //<-------------- FAILS WHEN I ADD THIS LINE
})
)
).toHaveLength(1)
})
Bind the method in the constructor to prevent the method from re-binding every time the component renders:
constructor(props) {
super(props)
this.state = { activeTab: TAB_NAMES.NEEDS_REVIEW }
this.filterByNeedsReview = this.filterByNeedsReview.bind(this)
}
Then pass the bound method as a prop to the child:
<FilterTab
...
onClick={this.filterByNeedsReview}
/>
You don't need to use an anonymous function here.

Switching between two components in React

rotateRender() {
if(false) {
return(
<TimerPage></TimerPage>
);
} else {
return(
<RepoPage></RepoPage>
);
}
}
I have two components called TimerPage and RepoPage.
I created a simple conditional render function as above, but cannot come up with a condition to make it render iteratively after a certain amount of time.
For example, I first want to render RepoPage and switch to TimerPage after 5 minutes and then stay in TimerPage for 15 mins before I switch again to the RepoPage.
Any way to do this?
Might not be that elegant, but this works
Actually I was thinking that this block might be more elegant than the first one
const FIRST_PAGE = '5_SECONDS';
const SECOND_PAGE = '15_SECONDS';
const FirstComponent = () => (
<div>5 SECONDS</div>
);
const SecondComponent = () => (
<div>15 SECONDS</div>
);
class App extends Component {
state = {
currentPage: FIRST_PAGE
};
componentDidUpdate() {
const {currentPage} = this.state;
const isFirst = currentPage === FIRST_PAGE;
if (isFirst) {
this._showSecondPageDelayed();
} else {
this._showFirstPageDelayed();
}
}
componentDidMount() {
this._showSecondPageDelayed();
};
_showSecondPageDelayed = () => setTimeout(() => {this.setState({currentPage: SECOND_PAGE})}, 5000);
_showFirstPageDelayed = () => setTimeout(() => {this.setState({currentPage: FIRST_PAGE})}, 15000);
render() {
const {currentPage} = this.state;
const isFirst = currentPage === FIRST_PAGE;
const ComponentToRender = isFirst ? FirstComponent : SecondComponent;
return <ComponentToRender/>;
}
}
As stated in the comment section, you can create a higher order component that will cycle through your components based on the state of that component. Use setTimeout to handle the timer logic for the component.
state = {
timer: true
}
componentDidMount = () => {
setInterval(
() => {
this.setState({ timer: !this.state.timer })
}, 30000)
}
render(){
const {timer} = this.state
if(timer){
return <TimerPage />
} else {
return <RepoPage />
}
}
Edit
Changed setTimeout to setInterval so that it will loop every 5 minutes instead of just calling setState once
You could use the new context API to achieve this. The benefit is now I have a configurable, reusable provider to play with throughout my application. Here is a quick demo:
https://codesandbox.io/s/k2vvy54r8o
import React, { Component, createContext } from "react";
import { render } from "react-dom";
const ThemeContext = createContext({ alternativeTheme: false });
class ThemeWrapper extends Component {
state = {
alternativeTheme: false
};
themeInterval = null;
componentDidMount() {
this.themeInterval = setInterval(
() =>
this.setState(({ alternativeTheme }) => ({
alternativeTheme: !alternativeTheme
})),
this.props.intervalLength
);
}
componentWillUnmount() {
if (this.themeInterval) {
clearInterval(this.themeInterval);
}
}
render() {
return (
<ThemeContext.Provider value={this.state}>
{this.props.children}
</ThemeContext.Provider>
);
}
}
const App = () => (
<ThemeWrapper intervalLength={2000}>
<ThemeContext.Consumer>
{({ alternativeTheme }) =>
alternativeTheme ? <p>Alternative Theme</p> : <p>Common Theme</p>
}
</ThemeContext.Consumer>
</ThemeWrapper>
);
render(<App />, document.getElementById("root"));
Whatever you do make sure on componentWillUnmount to clear your interval or timeout to avoid a memory leak.

React Component instance method spy never getting called when triggered by window event

I have a component that triggers a component instance method on a window event (resize & scroll). I am trying to spy on the event handler, but the spy never gets called.
Here is my component:
class PopOverContainer extends PureComponent {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
transitionState: PropTypes.string,
animationTiming: PropTypes.object,
anchorEl: PropTypes.object,
hAlign: PropTypes.oneOf(['left', 'right'])
}
constructor(props) {
super(props)
this.state = {
mounted: false
}
this.setRef = this.setRef.bind(this)
this.forceReposition = this.forceReposition.bind(this)
}
componentDidMount() {
window.addEventListener('resize', this.forceReposition)
window.addEventListener('scroll', this.forceReposition)
// Don't normally do this! We need to do this here since our animate in
// does not work properly if the default styles are not in the DOM before we
// transition them
setTimeout(() => this.setState({ mounted: true }), 0)
}
componentWillUnmount() {
window.removeEventListener('resize', this.forceReposition)
window.removeEventListener('scroll', this.forceReposition)
}
getAnchorPosition() {
if (!this.state.mounted) return {}
if (!this.props.anchorEl) return {}
const { anchorEl, hAlign } = this.props
const popOverEl = this.popOverEl
const rect = anchorEl.getBoundingClientRect()
const calcHOffset = () => {
if (hAlign === 'right') {
return rect.left + (anchorEl.offsetWidth - popOverEl.offsetWidth)
}
return rect.left
}
const pos = {
top: rect.top + anchorEl.offsetHeight,
left: calcHOffset()
}
return pos
}
forceReposition() {
this.forceUpdate()
}
setRef(el) {
this.popOverEl = el
}
render() {
const {
transitionState,
animationTiming,
children,
...styledProps
} = this.props
return (
<StyledPopOver
innerRef={this.setRef}
transitionState={transitionState}
animationTiming={animationTiming}
style={this.getAnchorPosition()}
{...styledProps}
>
{children}
</StyledPopOver>
)
}
}
And here is the test:
it('should force an update on scroll', async () => {
const wrapper = mount(
<PopOverContainer anchorEl={anchorEl}>
<span>Hi</span>
</PopOverContainer>
)
await new Promise(resolve => setTimeout(resolve, 2))
const instance = wrapper.instance()
const forceRepositionSpy = jest.spyOn(instance, 'forceReposition')
wrapper.update()
window.dispatchEvent(new Event('resize'))
expect(forceRepositionSpy).toHaveBeenCalled()
})
Here are the test results I get:
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called.
I do have some oddness baked into the component, as it has some animations that need to start on the next tick after the component is mounted. (Note the setTimeout(() => this.setState({ mounted: true }), 0))
I don't know if that has anything to do with what I am seeing, but in the end, my spyOn(instance, 'forceReposition') is never getting called, even though I have added console logs and can see that the event handler this.forceReposition does indeed get called inside the instance.

Resources