I have this React component that has a block that renders based on an API response.
const SomeComponent = () => {
const [render, setRender] = useState(false)
const handleClick = async () => {
const someFunction = await callToAPI()
if (someFunction) {
setRender(true)
}
}
return (
<main>
<button onClick={handleClick}></button>
<ShouldRender when={render} data-test-id='should-render'>
Something
</ShouldRender>
</main>
)
}
As I'm writing the test for it, I'm confused on how to test this conditional rendering.
I'd like to know if there's a way to use something like the fireEvent to set this state in my SomeComponent.test.js file.
So far the test looks like this:
test('renders component when button is clicked', () => {
render(<SomeComponent />)
const button = screen.queryByRole('button')
fireEvent.click(button)
const conditionalComponent = screen.getByTestId('should-render')
expect(conditionalComponent).toBeInTheDocument()
})
Related
I have react component, for example something like this:
const MyComponent = (props) => {
const [state, setState] = useState(true);
const {data} = useContext(myContext);
const location = useLocation();
//A lot of code here
const myFunction = () => {
return { dataFromContext: data, locationFromUseLocation: location, state: state }
}
return <>A lot of other components here</>
}
And I'm trying to write test that should looks like this:
describe('Component test', () => {
it('myFunction test', () => {
const wrapper = shallow(<MyComponent/>);
const expectedResult = {
dataFromContext: 'any data here',
locationFromUseLocation: 'any location here',
state: false
};
expect(wrapper.dive().instance().myFunction()).toEqual(expectedResult);
})
})
Can I mock useState, useContext and useLocation from <MyComponent/> and pass my custom data instead of real data from real component?
After deeper researching I've understood, that in such situation I can't write test only for function in my component. So, I've created unit-test, that tests all my component, not only one function.
I have my state and I want to display the component if the value is true but in the console I receive the error message Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state my code
import React, { useState} from "react";
import { useToasts } from "react-toast-notifications";
const Index = () => {
const [test, setTest]= useState(true);
const { addToast } = useToasts();
function RenderToast() {
return (
<div>
{ addToast('message') }
</div>
)}
return (
<div>
{test && <RenderToast /> }
</div>
)
}
You cannot set state during a render. And I'm guessing that addToast internally sets some state.
And looking at the docs for that library, you don't explicitly render the toasts. You just call addToast and then the <ToastProvider/> farther up in the tree shows them.
So to make this simple example works where a toast is shown on mount, you should use an effect to add the toast after the first render, and make sure your component is wrapped by <ToastProvider>
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
addToast('message')
}, [])
return <>Some Content here</>
}
// Example app that includes the toast provider
const MyApp = () => {
<ToastProvider>
<Index />
</ToastProvider>
}
how i can display the toast based on a variable for exemple display toast after receive error on backend?
You simply call addToast where you are handling your server communication.
For example:
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
fetchDataFromApi()
.then(data => ...)
.catch(error => addToast(`error: ${error}`))
}, [])
//...
}
my question is if it is possible to trigger useEffect with a variable from outside the component.
In my case i have this main component that has the useEffect responsible to update info every time the variable "refresh" changes.
function Main() {
const [data, setData] = useState([])
const [refresh, setRefresh] = useState(false)
useEffect(async () => {
await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(res => {
setData(res.data);
})
}, [refresh]);
And then i have a function that i can invoke inside the component or child that triggers the useEffect, updating the data.
const refreshData = () => setRefresh(!refresh);
So far so good, it works as i wanted but now i needed to export this function to use in a component not related to this one, but i know that you cannot export a function declared inside a component.
So my idea was to create this same function outside the component, like so:
let refreshOutside = false;
export const refreshMainFromOutside = () => {
refreshOutside = !refreshOutside;
}
So now i can add the variable "refreshOutside" to the useEffect, like so:
useEffect(async () => {
await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(res => {
setData(res.data);
})
}, [refresh, refreshOutside]);
But if i import it in other component and invoke the method it does not trigger the useEffect, i am kinda new to react but i think its because the component is not re-rendering.
Is there any solution that might work on my case?
Thanks in advance.
I suggest you put your hooks inside another file for example useComponent.js and export your refreshData as const inside it, then use that hook inside any component you wish:
const useComponent = () => {
const [data, setData] = useState([])
const [refresh, setRefresh] = useState(false)
useEffect(async () => {
await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(res => {
setData(res.data);
})
}, [refresh, refreshOutside]);
const refreshData = () => setRefresh(!refresh);
export { refreshData }
}
export default useComponent
import the hook inside any component then destructure functions and use them:
import useComponent from '../hooks/useComponent'
const MyComponent = () => {
const { refreshData } = useComponent()
return <button onClick={refreshData}>Click to refresh!</button>
}
export default MyComponent
As mentioned in the comment, you can simply define a separate function to fetch the data (and memoize it with the useCallback() hook), then you can use that function wherever you want in your Main component and in any Child component to whom you pass it as prop.
Maybe an example would make it easier to understand:
const Main = () => {
const [data, setData] = React.useState([]);
const updateData = React.useCallback((startIndex) => {
/*
axios.get(`${process.env.REACT_APP_SERVER_URL}/api/data/`)
.then(result => {
setData(result.data);
})
*/
axios.get('https://jsonplaceholder.typicode.com/posts/')
.then((result) => {
console.log('fetching data...');
setData(result.data.slice(startIndex, startIndex + 5));
console.log('... data updated');
});
}, [setData]);
React.useEffect(() => {
updateData(0); // fetch & update `data` when the Main component mounts
}, [])
return (
<div>
<h1>Main</h1>
{
data.length > 0
? <ul>{data.map(item => <li>{item.title}</li>)}</ul>
: <p>'There are no data'</p>
}
<button
onClick={() => updateData(5)} // fetch & update `data` on request, on button click
>Refresh from Main</button>
<Child handleClick={updateData} />
</div>
)
}
const Child = ({handleClick}) => {
return (
<React.Fragment>
<h1>Child</h1>
<button
onClick={() => handleClick(10)} // fetch & update `data` on request, on button click
>Refresh from Child</button>
</React.Fragment>
)
}
ReactDOM.render(<Main />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.js"></script>
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>
P.S. in the above example I used a parameter (startIndex) for the updateData() function just to keep the data state short and make it change when different buttons are clicked (because the API I used always returns the same data). In a real case use you are unlikely to do something like that because you can implement pagination on API side (if needed) and your API response is supposed to change over time (or you would not need a way to refresh your data).
Anyway, the point is that what I did inside the updateData() function body is mostly irrelevant; the main take away of the snippet is supposed to be how you can handle a function that needs to be called inside hooks, inside the main component and by child components.
Essentially, I'm trying to test if my modal opens when my button is clicked & I'm a noob with Jest & Enzyme.
I am hitting an issue accessing the props on the button & I'm not sure if it's because it's nested inside of a third party package or if I'm not importing it correctly into my test. Please see my condensed(ish) code below as I wasn't about to recreate the code on CodePen..
DataTable.jsx
const UploadDownloadComponent = ({ handleOpen }) => (
<UploadDownloadButtonContainer>
<PrimaryButton
id="bulk-add-button"
onClick={handleOpen} //this is what I need to access
>
Bulk Add Members
</PrimaryButton>
<SecondaryButton id="email-csv-button">
</SecondaryButton>
</UploadDownloadButtonContainer>
);
//beginning of data table component
export const Table = () => {
const [bulkUpload, setBulkUpload] = useState(false);
//upload modal
const openUpload = () => {
setBulkUpload(true);
};
const closeUpload = () => {
setBulkUpload(false);
};
//query container
const subHeaderComponentMemo = useMemo(() => {
{*/ other code /*}
return (
<div>
<UploadDownloadComponent
handleOpen={openUpload}
bulkUpload={bulkUpload}
/>
</div>
);
}, []);
return (
<React.Fragment>
<DataTable
{*/ bunch of other things unrelated /*}
subHeaderComponent={subHeaderComponentMemo}
/>
<UploadModal closeModal={closeUpload} open={bulkUpload} />
</React.Fragment>
);
};
DataTable.test.js
import React from "react";
import { configure, mount, shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { Table } from "../components/MemberView/DataTable";
import { UploadModal } from "../components/MemberView/UploadModal";
import Modal from "react-modal";
configure({ adapter: new Adapter() });
describe("<Table />", () => {
const wrapper = mount(<Table />);
//using .upDate() is the only way I can get this test to pass
it("should have upload button", () => {
const uploadButton = wrapper.find("#bulk-add-button").update();
expect(uploadButton).toHaveLength(1);
});
//this passes
it("renders Upload Modal", () => {
const upModal = shallow(<UploadModal />);
expect(upModal.find(Modal)).toHaveLength(1);
});
it("opens Upload Modal when state is changed", () => {
const modal = mount(<UploadModal />);
expect(modal.find(Modal).prop("isOpen")).toBe(false);
const uploadButton = wrapper.find("#bulk-add-button").update();
expect(uploadButton.length).toEqual(1);
//this is where my test fails as it cannot simulate click on uploadButton
uploadButton.simulate("click");
//if I change it to:
wrapper.find("#bulk-add-button").simulate("click')
//my error message says it expected 1 Node. ) found instead.
//I don't make it this far
expect(modal.find(Modal).prop("isOpen")).toBe(true);
});
});
I am also using Hooks if that makes any difference...
Any and all help/advice welcome!
Thanks
When accessing props the following approach works for me.
expect(modal.find(Modal).props().isOpen).toBe(false);
Hope this works!
I have the following code in plain JS and HTML
<div id="sketchWrapper">
<div id="initiatorContainer"></div>
</div>
const sketchWrapper = document.getElementById("sketchWrapper");
const sketch = initSketchWrapper(sketchWrapper).sketch;
const session = IK.initSession(APP_ID);
session.on("drawingCreated", (event) => {
session.insert(event.drawing, "sketchWrapper");
sketch();
}).connect(() => {
session.initiate("initiatorContainer");
sketch();
});
This code works and I want to use it in my React project. Since I need access to the HTML elements my first thought was to use useEffect because it's executed after the component was rendered.
const MyComponent = () => {
const sketchEl = useRef();
const initiatorEl = useRef();
useEffect(() => {
const sketch = initSketchWrapper(sketchEl.current).sketch;
const session = IK.initSession(APP_ID);
session
.on("drawingCreated", (event) => {
session.insert(event.drawing, sketchEl.current);
sketch();
})
.connect(() => {
session.initiate(initiatorEl.current);
sketch();
});
});
return (
<div ref={sketchEl}>
<div ref={initiatorEl} />
</div>
);
};
The code works but I'm not sure if this is the correct way to do it. And if I want to extend the component to add a function that needs access to the defined sketch constant how could this be done? Because currently the sketch constant is only accessible inside the useEffect but I can't define it outside of it because it needs access to the rendered output of the component.
const handleButtonClick = () => {
// ... some code
sketch() // <- can't access this because it's inside the useEffect function
}