This question already has answers here:
Mocking React custom hook with Jest
(3 answers)
Closed 7 months ago.
I have component and inside this component I have
custom hook
const Component = () => {
const { open } = useHook()
return (
<div>
<button onClick={open}/>
</div>
)
})
Is there any chance to test that open callback that come from useHook has been called after button click in react testing library, something like this
it('Open function is called when we click button', () => {
render(Component)
fireEvent.click(getByRole('button'))
// How to test that open has been called?
expect(open).hasBeenCalledTimes(1)
})
It is easy to test when we pass function as props, but how to test in such case
First of all your custom hooks must start with use, For example, useMyHook so that react can figure out a custom hook and apply hook-specific features to it
now you can figure out whether use callback has been called or not
let times = 0
const useMyHook = () => {
const use = () => {
times += 1
console.log("use function ran", times)
}
return (
{use: use}
)
}
I'm not sure what you plan on doing with this information, but this would count the amount of times its been called and set it into state so you can use the info for whatever you need.
const Component = () => {
const { open } = myHook()
const handleClick = () => {
open()
console.log("open was called")
}
return (
<div>
<button onClick={handleClick}/>
</div>
)
})
Related
I want to change State with child elements in React. However, when I click once, it is not immediately updated. Click twice, it shows the correct answer.
How to update async?
export default function Example() {
const onClick = async () => {
console.log('a', test)
// should be 'b', but console log 'a'
}
const [test, setTest] = useState('a')
return (
<ClickExample setTest={setTest} onClick={onClick} />
)
}
export default function ClickExample() {
const next = useCallback(
(alphabet: string) => {
setTest(alphabet)
onClick()
},
[onClick, setTest],
)
return <SelectButton onClick={() => next('b')} />
}
You can receive the value to be updated as an argument from the onClick callback. It'll be something like this:
export default function Example() {
const [test, setTest] = useState('a')
const handleClick = (newValue) => {
setTest(newValue);
}
return (
<ClickExample onClick={handleClick} />
)
}
export default function ClickExample({ onClick }) {
return <SelectButton onClick={() => onClick('b')} />
}
NOTE: You should avoid using useCallback() when it is not necessary. Read more over the web but this article from Kent C. Dodds is a good start. As a rule of thumb: Never use useCallback()/useMemo() unless you REALLY want to improve performance after needing that improvement.
In the first render, the value of test is equal to'a'. So when the console.log is executed, it has already captured 'a' as the value of test state. (See closures and stale closures).
One way to fix this would be to create a handleClick function in the parent component which receives the new value of test as its input and set the state and log the new value(which will be updated in the next render) using its argument.
// ClickExample
const handleClick = (alphabet) => {
setTest(alphabet);
console.log('a', alphabet);
};
codesandbox
I have read this post [ https://brettdewoody.com/accessing-component-methods-and-state-from-outside-react/ ]
But I don't understand.
that is not working on my source code.
it's my tsx file
declare global {
interface Window {
fn_test(): void;
childComponent: HTMLDivElement; <-- what the... ref type????
}
}
export default function Contact(): React.ReactElement {
....
function file_input_html( i:number ): React.ReactElement {
return (
<form id={`frm_write_file_${i}`} .... </form>
)
}
....
return (
<div ref={(childComponent) => {window.childComponent = childComponent}}>
....
)
it's my external javascript file
function fn_test(){
window.childComponent.file_input_html(3)
var element = document.getElementById("ifrm_write_file");
// element.value = "mystyle";
}
How can i call file_input_html function?
plase help me ...
You have some logic here that doesn't completely make sense.
In your class, you define file_input_html, which returns a component.
Then, in fn_test, you call attempt to call that function (which doesn't work -- I'll address that in a minute), but you don't do anything with the output.
The article that you linked to tells you how to get a ref to a component (eg the div in this case) -- not the actual Contact, which doesn't have a property named file_input_html anyway -- that's just a function you declared inside its scope.
What I'm assuming you want to happen (based on the code you shared) is for your external javascript file to be able to tell your component to render a form with a certain ID and then be able to get a reference to it. Here's an example of how to do this (it's a little convoluted, but it's a funny situation):
const { useState } = React
const App = (props) => {
const [formId, setFormId] = useState(2)
useEffect(() => {
window.alterFormId = setFormId
},[])
return (<div id={"form" + formId} ref={(ourComponent) => {window.ourComponent = ourComponent}}>
Text {formId}
</div>);
}
setTimeout(() => {
window.alterFormId(8);
setTimeout(() => {
console.log(window.ourComponent)
window.ourComponent.innerText = "Test"
}, 20)
}, 1000)
ReactDOM.render(<App />,
document.getElementById("root"))
What's happening here:
In useEffect, I set alterFormId on window so that it can be used outside of the React files
Using the technique you linked to, I get a ref to the div that's created. Note that I'm setting the ID here as well, based on the state of formId
The setTimeout function at the end tests all this:
a) It waits until the first render (the first setTimeout) and then calls alterFormId
b) Then, it waits again (just 20ms) so that the next run loop has happened and the React component has re-rendered, with the new formId and reference
c) From there, it calls a method on the div just to prove that the reference works.
I'm not exactly sure of your use case for all this and there are probably easier ways to architect things to avoid these issues, but this should get you started.
안녕하세요. 자바스크립트로 흐름만 알려드리겠습니다
아래 코드들을 참고해보세요.
iframe간 통신은
window.postMessage API와
window.addEventListener('message', handler) 메시지 수신 이벤트 리스너 로 구현할 수있습니다. 보안관련해서도 방어로직이 몇줄 필요합니다(origin 체크 등)
in parent
import React from 'react';
export function Parent () {
const childRef = React.useRef(null);
const handleMessage = (ev) => {
// 방어로직들
if (check ev.origin, check ev.source, ....) {
return false;
}
console.log('handleMessage(parent)', ev)
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<iframe ref="childRef" src="child_src" id="iframe"></iframe>
</div>
)
}
in child
import React from 'react';
export function Iframe () {
const handleMessage = (ev) => {
console.log('handleMessage(child)', ev)
}
const messagePush = () => {
window.parent.postMessage({ foo: 'bar' }, 'host:port')
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<button onClick={messagePush}>Push message</button>
</div>
)
}
My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}
I am trying to test a load more button call on an onClick fireEvent but I am having trouble simulating the click to trigger a load data.
component:
class Items extends Component {
// states
componentDidMount() {
this.getData()
}
getData() { ...
// get data from state - pagination # and data size
}
onLoadMore() {
// increment pagination & offset on states
this.getData()
}
render() {
return (
<div className='container'>
{items.map((item, i) => {
return (
<div className='item-box'>
// item info
</div>
)
}
)}
<button onClick={this.onLoadMore}>Load More</button>
</div>
)
}
}
test:
it('load more data on load more button click', () => {
const Items = require('./default').default
// set initial load values: initVals (2 items)
// set second call values: secondVals (4 items)
Items.prototype.getData = jest.fn()
Items.prototype.getData.mockReturnValue(initVals)
Items.prototype.getData.mockReturnValue(secondVals)
const { container } = render(
<Items
fields={{ loadMore: true }}
/>
)
const button = screen.getByText('Load More')
fireEvent.click(button)
expect(container.querySelectorAll('.item-box').length).toBe(2)
expect(container.querySelectorAll('.item-box').length).toBe(4)
})
So this only reads the last call, finding 4 items.
Calling .mockReturnValue() multiple times has only yielded me the last call instead of it consecutively. I know I am using it wrong but I can't figure out the sequence of running this. My goal is to initialize the component with first values (load 2 items), then on click, it loads more (4 items).
Help?
I think you need to call mockReturnValueOnce instead of mockReturnValue, and you definitely need to move your first assertion before clicking the event. Also, I think your second assertion should expect 6, not 4.
The order of operations for your test should be:
Set up mocks
Render component
Assert initial value on load
Simulate click event
Assert value after click.
Here is a simple example that demonstrates this concept:
// src/Demo.js
import React, { useState } from "react";
const Demo = ({ loadText }) => {
const [text, setText] = useState("");
return (
<div>
<button onClick={() => setText(loadText())}>Load</button>
<p data-testid="data">{text}</p>
</div>
);
};
export default Demo;
// src/Demo.test.js
import { fireEvent, render, screen } from "#testing-library/react";
import '#testing-library/jest-dom'
import Demo from "./Demo";
test("callbacks", () => {
const myMock = jest.fn();
myMock.mockReturnValueOnce(2);
myMock.mockReturnValueOnce(4);
render(<Demo loadText={myMock} />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("data")).toHaveTextContent("2");
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("data")).toHaveTextContent("4");
});
Also, the way you are mocking this function (Items.prototype.getData = jest.fn()) seems odd to me. I recommend you explore other options, such as (1) mocking out axios or similar library, (2) mocking out a redux store, or (3) mocking out props that you pass to this component.
Sorry if it's a beginner question>
I am trying to use Functional Component, as I was doing Class Component all the time.
I have a simple component that should load a list from a server, and display it.
The component looks like this (I simplified a bit so sorry if there is a type) :
const ItemRelationsList = (props: ItemRelationsListProps): JSX.Element => {
const [getList, setList] = useState([]);
const loadRelation = (): void => {
HttpService.GetAsync<getListRequest, getListResponse>('getList',{
// params
}).subscribe(res => {
setList(res.data.list);
});
}
loadRelation();
return (
<>
<Table
columns={columns}
dataSource={getList}
>
</Table>
</>
)
}
thew problem I face is that everytime I use setList, the component is redraw, so the http call is reexecute.
Is there a way to prevent that other than use a class component ?
use useEffect
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
const ItemRelationsList = (props: ItemRelationsListProps): JSX.Element => {
const [getList, setList] = useState([]);
// componentDidMount
useEffect(() => {
loadRelation()
}, [])
const loadRelation = (): void => {
HttpService.GetAsync<getListRequest, getListResponse>('getList',{
// params
}).subscribe(res => {
setList(res.data.list);
});
}
return (
<>
<Table
columns={columns}
dataSource={getList}
>
</Table>
</>
)
}
useEffect(yourCallback, []) - will trigger the callback only after the
first render.
Read the Docs hooks-effect
This is related to How to call loading function with React useEffect only once