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
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 :)
}
// ...
}
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);
}
};
}
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.
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.
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.