I have a component that looks like such:
export default class RowCell extends Component {
...
render() {
const { column } = this.props;
const colClass = column.toLowerCase();
return (
<td className={`cell cell-row ${colClass === 'someval' ? 'anotherval' : colClass}`}>
{ this.renderAppropriateCell(colClass) }
</td>
);
}
}
And I'm testing an onClick to see if it can properly fire the handler when a click is simulated. In this case the handler is setting/updating a piece of state.
it('Should fire the onClick handler', () => {
const componentWithHandlerWrapper = shallow(
<RowCell {...props} onClick={() => this.setState({ clicked: !this.state.clicked })} />
);
// In this case, we are passing a dummy function that sets a piece of state in the component
const rowCellInstance = componentWithHandlerWrapper.setState({ clicked: false });
expect(rowCellInstance.instance().state.clicked).toEqual(false);
rowCellInstance.simulate('click');
expect(rowCellInstance.instance().state.clicked).toEqual(true);
});
However I can't get the handler to set/update state on the shallow render of the component. The last expect is failing since either 1) the click simulation is not properly executing or 2) the click simulation works but cannot update state on a shallow render? I've tried it on an instance as well with no luck.
Advice is appreciated.
Related
Let's say I have two components Component1 and Component2. I have a button in Component1.
const Component1 = () => {
return(
<div>
<button onClick={()=>someExportFunc()}>Export</button>
<Component2 />
</div>
)
}
My Component2.jsx is something like this.
import {ChartComponent} from '#xyz/charts';
const Component2 = () => {
let chartInstance = new ChartComponent();
return (
<ChartComponent
id='chart1'
ref={chart = chartInstance => chart}
>
</ChartComponent>
)
}
Now the function someExportFunc() is defined inside the {ChartComponent} being imported in Component2.jsx.
Had I used the button inside Component2, it would have worked simply. As in,
import {ChartComponent} from '#xyz/charts'
const Component2 = () => {
let chartInstance = new ChartComponent();
return (
<button onClick={()=>someExportFunc()}>Export</button>
<ChartComponent
id='chart1'
ref={chart = chartInstance => chart}
>
</ChartComponent>
)
}
But how can I make it work as a button in Component1?
I am guessing it has something to do with defining state of the button and then passing the state down, but I don't exactly understand how the state of a button works. onClick() should be some instantaneous boolean, isn't it? By instantaneous, I mean it would change as soon as the 'click' is over.
Edit1: Okay, so the someExportFunc() is a library function defined in my import. It makes use of the chartInstance created in Component2. It's actual use case is
<button
value='export'
onClick={() => chartInstance.exportModule.export('PNG',
`image`)}>
Export
</button>
So basically I want it so that when I click the button within Component1, it sends the onClick event to my ChartComponent in Component2.
we need to pass the function as a prop to the second component.
FirstComponent
<SecondComponent { ...data } onClick={ () => manageClick() } />
in the second Component use the onClick prop on click.
SecondComponent
<button onClick={onClick}>Click</button>
React uses one-way data binding, meaning parent to child in this case.
If Component 2 is a child of Component 1, define the function in Component 1 and pass the function and Chart Component as props and children respectively.
Parent:
const Component1 = () => {
const someFunc = () => {...}
return <Component2 someFunc={someFunc}> <ChartComponent/> </Component2>
}
Child:
const Component2 = (props) => {
props.someFunc();
return <div> {props.children} </div>
}
I have the following component, where review assignments (props.peerReviewAssignmentIds) are loaded for a student's own work (related event is onClick_Submission) or a peer's work to review (related event is onClick_PeerReview ). These events work fine and the related data is loaded successfully. However, there is a problem with updating the content of the child components based on the value of the props.peerReviewAssignmentIds, which I elaborate below.
const AssignmentItem = (props) => {
const assignment = props.assignments[props.currentAssignmentId];
const onClick_Submission = (e) => {
e.preventDefault();
if (!st_showSubmission) {
props.fetchPeerReviewAssignmentForStudent(currentUserId, assignment.activeReviewRoundId);
}
set_showSubmission(!st_showSubmission);
set_isPeerReview(false);
}
const onClick_PeerReview = (e) => {
e.preventDefault();
if (!st_showPeerReviews) {
if (st_submissionContiues === false)
props.fetchPeerReviewAssignmentForReviewer(currentUserId, assignment.activeReviewRoundId);
}
set_showPeerReviews(!st_showPeerReviews);
set_isPeerReview(true);
}
return (
<>
{
st_showSubmission === true && props.peerReviewAssignmentIds.length > 0 &&
<ReviewPhaseInfoForSubmission isPeerReview={false} />
}
</>
)
}
const mapStateToProps = state => ({
peerReviewAssignmentIds: state.peerReviewAssignmentReducer.peerReviewAssignmentIds,
loading_pra: state.peerReviewAssignmentReducer.loading,
error_pra: state.peerReviewAssignmentReducer.error,
})
I will try to explain the problem with an example. When the first time onClick_Submission is triggered, props.peerReviewAssignmentIds[0] is set to 2, and all the sub components are loaded properly. Next, when onClick_PeerReview is triggered, props.peerReviewAssignmentIds[0] is set to 1, which is correct. But, the child components get updated according to the previous value of props.peerReviewAssignmentIds[0], which was 2. If the onClick_PeerReview event is triggered second time, then the child components get updated correctly according to the current value of props.peerReviewAssignmentIds[0], which is 1. Any ideas why this might be happening?
I further explain my code below.
Below is the ReviewPhaseInfoForSubmission component. In this component, based on props.peerReviewAssignmentIds[0] value (which gets updated in the parent component above) etherpadLaterSubmission variable is created with props.createSession_laterSubmission method.
const ReviewPhaseInfoForSubmission = (props) => {
const [st_is_discussFeedback, set_is_discussFeedback] = useState(false);
useEffect(() => {
set_is_discussFeedback(true);
if (props.etherpadLaterSubmission === null) {
props.createSession_laterSubmission(props.peerReviewAssignmentIds[0], discussFeedback.dueDate);
}
}, [])
return (
<div className="p-1">
{
st_is_discussFeedback === true &&
<ProvideOrDiscussFeedback provide0discuss1revise2={1} isPeerReview={props.isPeerReview} />
}
</div>
);
}
const mapStateToProps = (state) => ({
etherpadLaterSubmission: state.etherpadReducer.etherpadLaterSubmission,
loading_ep: state.etherpadReducer.loading,
})
Then, in a child component, ProvideOrDiscussFeedback (see below), the props.etherpadLaterSubmission value is used for display purposes.
const ProvideOrDiscussFeedback = (props) => {
return <div className="p-3 shadow">
{
props.etherpadLaterSubmission &&
<div>
<DisplayingEtherpad etherpadSession={props.etherpadLaterSubmission } />
</div>
}
</div>
}
TL;DR This is my Parent component:
const Parent = () => {
const [open, setOpen] = useState([]);
const handleExpand = panelIndex => {
if (open.includes(panelIndex)) {
// remove panelIndex from [...open]
// asign new array to variable: newOpen
// set the state
setOpen(newOpen);
} else {
setOpen([...open, panelIndex]);
}
}
return (
<div>
<Child expand={handleExpand} /> // No need to update
<Other isExpanded={open} /> // needs to update if open changed
</div>
)
}
And this is my Child component:
const Child = (props) => (
<button
type="button"
onClick={() => props.expand(1)}
>
EXPAND PANEL 1
</button>
);
export default React.memo(Child, () => true); // true means don't re-render
Those code are just an example. The main point is I don't need to update or re-render Child component because it just a button. But the second time I click the button it's not triggering Parent to re-render.
If I put console.log(open) inside handleExpand like so:
const handleExpand = panelIndex => {
console.log(open);
if (open.includes(panelIndex)) {
// remove panelIndex from [...open]
// asign new array to variable: newOpen
// set the state
setOpen(newOpen);
} else {
setOpen([...open, panelIndex]);
}
}
it printed out the same array everytime I clicked the button as if the value of open which is array never updated.
But if I let <Child /> component re-render when open changed, it works. Why is that? is this something as expected?
This is indeed expected behavior.
What you are experiencing here are function closures. When you pass handleExpand to Child all referenced variables are 'saved' with their current value. open = []. Since your component does not re-render it will not receive a 'new version' of your handleExpand callback. Every call will have the same result.
There are several ways of bypassing this. First obviously being letting your Child component re-render.
However if you strictly do not want to rerender you could use useRefwhich creates an object and access it's current property:
const openRef = useRef([])
const [open, setOpen] = useState(openRef.current);
// We keep our ref value synced with our state value
useEffect(() => {
openRef.current = open;
}, [open])
const handleExpand = panelIndex => {
if (openRef.current.includes(panelIndex)) {
setOpen(newOpen);
} else {
// Notice we use the callback version to get the current state
// and not a referenced state from the closure
setOpen(open => [...open, panelIndex]);
}
}
I'm trying to select a song from the playlist, but selection doesn't work as it should, because, when I click for the first time it returns "null" when I click on the second time it selects an item, but for some reason, when I keep clicking on different items I always get a previous item value, and I don't know why.
//Here is my action from redux:
import {FETCH_PLAYLIST_SUCCESS, FETCH_PLAYLIST_FAILURE, FETCH_PLAYLIST_REQUEST, SELECT_TRACK} from './types'
import {PREV, PLAY, PAUSE, NEXT, TOGGLE_TRACK} from './types';
import axios from "axios/index";
export const getPlayList = id => {
return function (dispatch) {
dispatch({type: FETCH_PLAYLIST_REQUEST});
axios.get('https://cors-anywhere.herokuapp.com/https://api.deezer.com/search/track?q=red+hot+chili+peppers').then(response => {
dispatch({
type: FETCH_PLAYLIST_SUCCESS,
payload: {
playlist: response.data.data,
currentTrack: response.data.data[0]
}
});
}).catch(err => {
dispatch({
type: FETCH_PLAYLIST_FAILURE,
payload: err,
});
})
}
};
//func which takes index of list item
export const selectTrack = (i) =>{
return function (dispatch) {
dispatch({
type: SELECT_TRACK,
payload: i
});
}
}
//reducer
case SELECT_TRACK:
return {
...state,
selectedTrack: state.playList[action.payload],
};
//click handler in container component
handleClick = (index) => {
this.props.selectTrack(index);
console.log(this.props.playlist.selectedTrack);
console.log(index);
};
//how I'm using it in playlist component
<Playlist handleClick={this.handleClick} tracks={this.props.playlist.playList}/>
//Playlist component
function PlayList(props) {
const {i,handleClick,currentSongIndex,tracks,isSelected,} = props;
return (
<div className="playlist-container">
<div className="playlist">
<ul>
{
tracks.map((track, index) => (
<Track
isSelected={isSelected}
index={index}
key={track.id}
id={track.id}
title_short={track.title}
duration={track.duration}
clickHandler={ () => handleClick(index)}
/>
))
}
</ul>
</div>
</div>
);
}
See how it works on screenshots
Upon item click, you are dispatching the action in the click handler of the container. The props in the handler at the time of console.log is still the old props. The new props will arrive only once redux updates state and component updates its props via the react lifecycle methods.
I am guessing you have not set any intial state in the reducer which might be the reason why the first value is coming as null.
Try adding the same statement : console.log(this.props.playlist.selectedTrack) in the render method. You should see the updated prop
#Easwar is correct (above) that your debugging logic is problematic.
With respect to the core problem, I don't see where some of your important props are passed in. You have:
const {i,handleClick,currentSongIndex,tracks,isSelected,} = props;
In your PlayList component, but you're invoking it like this:
<Playlist handleClick={this.handleClick} tracks={this.props.playlist.playList}/>
Where are currentSongIndex and isSelected passed in? Your trouble is likely there. It looks like they'll evaluate to null in <Playlist>'s render method.
This is a function used in a react component. As you can see I'm using ref to get the focus on a specific input element of another component.
myFunction (param) {
this.refInput && this.refInput.focus()
}
Now I would like to test via jestJS for the focus() to have been called.
it('myFunction() should call focus()', () => {
// SETUP
const refInput = { focus: jest.fn() }
wrapper = shallow(<Example
refInput={refInput}
/>)
// EXECUTE
wrapper.instance().myFunction('anything')
// VERIFY
expect(refInput.focus).toHaveBeenCalled()
})
But this is wrong, as I pass the refInput as a property. But it is not this.props.refInput, so this attempt is not working.
How do I setup a ref in my test?
Update
This is how my component looks like:
class Example extends Component {
setStep (state) {
state.term = ''
this.setState(state)
this.refInput && this.refInput.focus()
}
render () {
return (
<div>
<Step onClick={this.setStep.bind(this, this.state)}>
<Step.Content title='title' description='description' />
</Step>
<Input ref={input => { this.refInput = input }} />
</div>
)
}
}
Try doing something like this:
it('myFunction() should call focus()', () => {
// SETUP
wrapper = mount(<Example />)
// EXECUTE
wrapper.instance().myFunction('anything')
// VERIFY
const elem = wrapper.find('#foo');
const focusedElement = document.activeElement;
expect(elem.matchesElement(focusedElement)).to.equal(true);
})
Points to note:
Use Mount rather than Shallow, as #Marouen Mhiri commented, shallow rendering can't hold a ref
You don't need to pass ref as props (in fact it's wrong)
Where I have wrapper.find('#foo'), replace foo by class/id of the DOM element in Input